|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright 2023 Google LLC |
| 4 | + * |
| 5 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | + * you may not use this file except in compliance with the License. |
| 7 | + * You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | + |
| 18 | +// This is a generated sample, using the typeless sample bot. Please |
| 19 | +// look for the source TypeScript sample (.ts) for modifications. |
| 20 | +'use strict'; |
| 21 | + |
| 22 | +// sample-metadata: |
| 23 | +// title: Detect password leak with reCAPTCHA |
| 24 | +// description: Detect if the password credential has been compromised. |
| 25 | +// usage: node passwordLeakAssessment.ts [project-id] [sita-key] [token] [action] |
| 26 | + |
| 27 | +const args = process.argv.slice(2); |
| 28 | +const [projectId, siteKey, token, action, username, password] = args; |
| 29 | + |
| 30 | +// [START recaptcha_enterprise_password_leak_verification] |
| 31 | + |
| 32 | +const { |
| 33 | + RecaptchaEnterpriseServiceClient, |
| 34 | +} = require('@google-cloud/recaptcha-enterprise'); |
| 35 | +const {PasswordCheckVerification} = require('recaptcha-password-check-helpers'); |
| 36 | + |
| 37 | +// TODO(developer): Uncomment and set the following variables |
| 38 | +// Google Cloud Project ID. |
| 39 | +// const projectId: string = "PROJECT_ID" |
| 40 | + |
| 41 | +// Site key obtained by registering a domain/app to use recaptcha Enterprise. |
| 42 | +// const siteKey: string = "RECAPTCHA_SITE_KEY" |
| 43 | + |
| 44 | +// The token obtained from the client on passing the recaptchaSiteKey. |
| 45 | +// To get the token, integrate the recaptchaSiteKey with frontend. See, |
| 46 | +// https://cloud.google.com/recaptcha-enterprise/docs/instrument-web-pages#frontend_integration_score |
| 47 | +// const token: string = "TOKEN" |
| 48 | + |
| 49 | +// Action name corresponding to the token. |
| 50 | +// const action: string = "ACTION" |
| 51 | + |
| 52 | +// Username and password to be checked for credential breach. |
| 53 | +// const username: string = "user123"; |
| 54 | +// const password: string = "1234@abcd"; |
| 55 | + |
| 56 | +// Create a client. |
| 57 | +const client = new RecaptchaEnterpriseServiceClient(); |
| 58 | + |
| 59 | +/* |
| 60 | +* Detect password leaks and breached credentials to prevent account takeovers |
| 61 | +* (ATOs) and credential stuffing attacks. |
| 62 | +* For more information, see: |
| 63 | +* https://cloud.google.com/recaptcha-enterprise/docs/getting-started and |
| 64 | +* https://security.googleblog.com/2019/02/protect-your-accounts-from-data.html |
| 65 | +
|
| 66 | +* Steps: |
| 67 | +* 1. Use the 'create' method to hash and Encrypt the hashed username and |
| 68 | +* password. |
| 69 | +* 2. Send the hash prefix (2-byte) and the encrypted credentials to create |
| 70 | +* the assessment.(Hash prefix is used to partition the database.) |
| 71 | +* 3. Password leak assessment returns a database whose prefix matches the |
| 72 | +* sent hash prefix. |
| 73 | +* Create Assessment also sends back re-encrypted credentials. |
| 74 | +* 4. The re-encrypted credential is then locally verified to see if there is |
| 75 | +* a match in the database. |
| 76 | +* |
| 77 | +* To perform hashing, encryption and verification (steps 1, 2 and 4), |
| 78 | +* reCAPTCHA Enterprise provides a helper library in Typescript. |
| 79 | +* See, https://github.com/GoogleCloudPlatform/typescript-recaptcha-password-check-helpers |
| 80 | +
|
| 81 | +* If you want to extend this behavior to your own implementation/ languages, |
| 82 | +* make sure to perform the following steps: |
| 83 | +* 1. Hash the credentials (First 2 bytes of the result is the |
| 84 | +* 'lookupHashPrefix') |
| 85 | +* 2. Encrypt the hash (result = 'encryptedUserCredentialsHash') |
| 86 | +* 3. Get back the PasswordLeak information from |
| 87 | +* reCAPTCHA Enterprise Create Assessment. |
| 88 | +* 4. Decrypt the obtained 'credentials.getReencryptedUserCredentialsHash()' |
| 89 | +* with the same key you used for encryption. |
| 90 | +* 5. Check if the decrypted credentials are present in |
| 91 | +* 'credentials.getEncryptedLeakMatchPrefixesList()'. |
| 92 | +* 6. If there is a match, that indicates a credential breach. |
| 93 | +*/ |
| 94 | +async function checkPasswordLeak( |
| 95 | + projectId, |
| 96 | + recaptchaSiteKey, |
| 97 | + token, |
| 98 | + action, |
| 99 | + username, |
| 100 | + password |
| 101 | +) { |
| 102 | + // Instantiate the recaptcha-password-check-helpers library to perform the |
| 103 | + // cryptographic functions. |
| 104 | + // Create the request to obtain the hash prefix and encrypted credentials. |
| 105 | + const verification = await PasswordCheckVerification.create( |
| 106 | + username, |
| 107 | + password |
| 108 | + ); |
| 109 | + |
| 110 | + const lookupHashPrefix = Buffer.from( |
| 111 | + verification.getLookupHashPrefix() |
| 112 | + ).toString('base64'); |
| 113 | + const encryptedUserCredentialsHash = Buffer.from( |
| 114 | + verification.getEncryptedUserCredentialsHash() |
| 115 | + ).toString('base64'); |
| 116 | + console.log('Hashes created.'); |
| 117 | + |
| 118 | + // Pass the credentials to the createPasswordLeakAssessment() to get back |
| 119 | + // the matching database entry for the hash prefix. |
| 120 | + const credentials = await createPasswordLeakAssessment( |
| 121 | + projectId, |
| 122 | + recaptchaSiteKey, |
| 123 | + token, |
| 124 | + action, |
| 125 | + lookupHashPrefix, |
| 126 | + encryptedUserCredentialsHash |
| 127 | + ); |
| 128 | + |
| 129 | + // Convert to appropriate input format. |
| 130 | + const reencryptedUserCredentialsHash = Buffer.from( |
| 131 | + credentials.reencryptedUserCredentialsHash.toString(), |
| 132 | + 'base64' |
| 133 | + ); |
| 134 | + |
| 135 | + const encryptedLeakMatchPrefixes = credentials.encryptedLeakMatchPrefixes.map( |
| 136 | + prefix => { |
| 137 | + return Buffer.from(prefix.toString(), 'base64'); |
| 138 | + } |
| 139 | + ); |
| 140 | + |
| 141 | + // Verify if the encrypted credentials are present in the obtained |
| 142 | + // match list to check if the credential is leaked. |
| 143 | + const isLeaked = verification |
| 144 | + .verify(reencryptedUserCredentialsHash, encryptedLeakMatchPrefixes) |
| 145 | + .areCredentialsLeaked(); |
| 146 | + |
| 147 | + console.log(`Is Credential leaked: ${isLeaked}`); |
| 148 | +} |
| 149 | + |
| 150 | +// Create a reCAPTCHA Enterprise assessment. |
| 151 | +// Returns: PrivatePasswordLeakVerification which contains |
| 152 | +// reencryptedUserCredentialsHash and credential breach database whose prefix |
| 153 | +// matches the lookupHashPrefix. |
| 154 | +async function createPasswordLeakAssessment( |
| 155 | + projectId, |
| 156 | + recaptchaSiteKey, |
| 157 | + token, |
| 158 | + action, |
| 159 | + lookupHashPrefix, |
| 160 | + encryptedUserCredentialsHash |
| 161 | +) { |
| 162 | + // Build the assessment request. |
| 163 | + const createAssessmentRequest = { |
| 164 | + parent: `projects/${projectId}`, |
| 165 | + assessment: { |
| 166 | + // Set the properties of the event to be tracked. |
| 167 | + event: { |
| 168 | + siteKey: recaptchaSiteKey, |
| 169 | + token: token, |
| 170 | + }, |
| 171 | + // Set the hashprefix and credentials hash. |
| 172 | + // Setting this will trigger the Password leak protection. |
| 173 | + privatePasswordLeakVerification: { |
| 174 | + lookupHashPrefix: lookupHashPrefix, |
| 175 | + encryptedUserCredentialsHash: encryptedUserCredentialsHash, |
| 176 | + }, |
| 177 | + }, |
| 178 | + }; |
| 179 | + |
| 180 | + // Send the create assessment request. |
| 181 | + const [response] = await client.createAssessment(createAssessmentRequest); |
| 182 | + |
| 183 | + // Check validity and integrity of the response. |
| 184 | + await checkTokenIntegrity(response.tokenProperties, action); |
| 185 | + // Get the reCAPTCHA Enterprise score. |
| 186 | + console.log(`The reCAPTCHA score is: ${response.riskAnalysis.score}`); |
| 187 | + // Get the assessment name (id). Use this to annotate the assessment. |
| 188 | + console.log(`Assessment name: ${response.name}`); |
| 189 | + |
| 190 | + if (!response.privatePasswordLeakVerification) { |
| 191 | + throw `Error in obtaining response from Private Password Leak Verification ${response}`; |
| 192 | + } |
| 193 | + |
| 194 | + return response.privatePasswordLeakVerification; |
| 195 | +} |
| 196 | + |
| 197 | +// Check for token validity and action integrity. |
| 198 | +async function checkTokenIntegrity(tokenProperties, action) { |
| 199 | + if (!tokenProperties) { |
| 200 | + throw 'Token properties field is null or undefined.'; |
| 201 | + } |
| 202 | + |
| 203 | + // Check if the token is valid. |
| 204 | + if (!tokenProperties.valid) { |
| 205 | + throw `The Password check call failed because the token was: |
| 206 | + ${tokenProperties.invalidReason}`; |
| 207 | + } |
| 208 | + |
| 209 | + // Check if the expected action was executed. |
| 210 | + if (tokenProperties.action !== action) { |
| 211 | + throw `The action attribute in the reCAPTCHA tag |
| 212 | + ${tokenProperties.action} does not match the action ${action} you |
| 213 | + are expecting to score.`; |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +checkPasswordLeak(projectId, siteKey, token, action, username, password).catch( |
| 218 | + err => { |
| 219 | + console.error(err.message); |
| 220 | + process.exitCode = 1; |
| 221 | + } |
| 222 | +); |
| 223 | + |
| 224 | +// [END recaptcha_enterprise_password_leak_verification] |
0 commit comments