Skip to content

Commit 2579d91

Browse files
1 parent 330ba92 commit 2579d91

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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

Comments
 (0)