Skip to content

Commit 30fba87

Browse files
docs(samples): add password leak sample and test (#808)
* docs(samples): add password leak sample and test * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * docs(samples): added password-leak-helper dependency * Updated comment acc to review * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * docs(samples): refactored acc to review comments. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 035b611 commit 30fba87

File tree

3 files changed

+290
-33
lines changed

3 files changed

+290
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package passwordleak;
18+
19+
// [START recaptcha_enterprise_password_leak_verification]
20+
21+
import com.google.cloud.recaptcha.passwordcheck.PasswordCheckResult;
22+
import com.google.cloud.recaptcha.passwordcheck.PasswordCheckVerification;
23+
import com.google.cloud.recaptcha.passwordcheck.PasswordCheckVerifier;
24+
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient;
25+
import com.google.protobuf.ByteString;
26+
import com.google.recaptchaenterprise.v1.Assessment;
27+
import com.google.recaptchaenterprise.v1.CreateAssessmentRequest;
28+
import com.google.recaptchaenterprise.v1.Event;
29+
import com.google.recaptchaenterprise.v1.PrivatePasswordLeakVerification;
30+
import com.google.recaptchaenterprise.v1.TokenProperties;
31+
import java.io.IOException;
32+
import java.util.List;
33+
import java.util.concurrent.ExecutionException;
34+
import java.util.stream.Collectors;
35+
36+
public class CreatePasswordLeakAssessment {
37+
38+
public static void main(String[] args)
39+
throws IOException, ExecutionException, InterruptedException {
40+
// TODO(developer): Replace these variables before running the sample.
41+
// GCloud Project ID.
42+
String projectID = "project-id";
43+
44+
// Site key obtained by registering a domain/app to use recaptcha Enterprise.
45+
String recaptchaSiteKey = "recaptcha-site-key";
46+
47+
// The token obtained from the client on passing the recaptchaSiteKey.
48+
// To get the token, integrate the recaptchaSiteKey with frontend. See,
49+
// https://cloud.google.com/recaptcha-enterprise/docs/instrument-web-pages#frontend_integration_score
50+
String token = "recaptcha-token";
51+
52+
// Action name corresponding to the token.
53+
String recaptchaAction = "recaptcha-action";
54+
55+
checkPasswordLeak(projectID, recaptchaSiteKey, token, recaptchaAction);
56+
}
57+
58+
/*
59+
* Detect password leaks and breached credentials to prevent account takeovers (ATOs)
60+
* and credential stuffing attacks.
61+
* For more information, see: https://cloud.google.com/recaptcha-enterprise/docs/getting-started
62+
* and https://security.googleblog.com/2019/02/protect-your-accounts-from-data.html
63+
64+
* Steps:
65+
* 1. Use the 'createVerification' method to hash and Encrypt the hashed username and password.
66+
* 2. Send the hash prefix (2-byte) and the encrypted credentials to create the assessment.
67+
* (Hash prefix is used to partition the database.)
68+
* 3. Password leak assessment returns a database whose prefix matches the sent hash prefix.
69+
* Create Assessment also sends back re-encrypted credentials.
70+
* 4. The re-encrypted credential is then locally verified to see if there is a
71+
* match in the database.
72+
*
73+
* To perform hashing, encryption and verification (steps 1, 2 and 4),
74+
* reCAPTCHA Enterprise provides a helper library in Java.
75+
* See, https://github.com/GoogleCloudPlatform/java-recaptcha-password-check-helpers
76+
77+
* If you want to extend this behavior to your own implementation/ languages,
78+
* make sure to perform the following steps:
79+
* 1. Hash the credentials (First 2 bytes of the result is the 'lookupHashPrefix')
80+
* 2. Encrypt the hash (result = 'encryptedUserCredentialsHash')
81+
* 3. Get back the PasswordLeak information from reCAPTCHA Enterprise Create Assessment.
82+
* 4. Decrypt the obtained 'credentials.getReencryptedUserCredentialsHash()'
83+
* with the same key you used for encryption.
84+
* 5. Check if the decrypted credentials are present in 'credentials.getEncryptedLeakMatchPrefixesList()'.
85+
* 6. If there is a match, that indicates a credential breach.
86+
*/
87+
public static void checkPasswordLeak(
88+
String projectID, String recaptchaSiteKey, String token, String recaptchaAction)
89+
throws ExecutionException, InterruptedException, IOException {
90+
// Set the username and password to be checked.
91+
String username = "username";
92+
String password = "password123";
93+
94+
// Instantiate the java-password-leak-helper library to perform the cryptographic functions.
95+
PasswordCheckVerifier passwordLeak = new PasswordCheckVerifier();
96+
97+
// Create the request to obtain the hash prefix and encrypted credentials.
98+
PasswordCheckVerification verification =
99+
passwordLeak.createVerification(username, password).get();
100+
101+
byte[] lookupHashPrefix = verification.getLookupHashPrefix();
102+
byte[] encryptedUserCredentialsHash = verification.getEncryptedLookupHash();
103+
104+
// Pass the credentials to the createPasswordLeakAssessment() to get back
105+
// the matching database entry for the hash prefix.
106+
PrivatePasswordLeakVerification credentials =
107+
createPasswordLeakAssessment(
108+
projectID,
109+
recaptchaSiteKey,
110+
token,
111+
recaptchaAction,
112+
lookupHashPrefix,
113+
encryptedUserCredentialsHash);
114+
115+
// Convert to appropriate input format.
116+
List<byte[]> leakMatchPrefixes =
117+
credentials.getEncryptedLeakMatchPrefixesList().stream()
118+
.map(ByteString::toByteArray)
119+
.collect(Collectors.toList());
120+
121+
// Verify if the encrypted credentials are present in the obtained match list.
122+
PasswordCheckResult result =
123+
passwordLeak
124+
.verify(
125+
verification,
126+
credentials.getReencryptedUserCredentialsHash().toByteArray(),
127+
leakMatchPrefixes)
128+
.get();
129+
130+
// Check if the credential is leaked.
131+
boolean isLeaked = result.areCredentialsLeaked();
132+
System.out.printf("Is Credential leaked: %s", isLeaked);
133+
}
134+
135+
// Create a reCAPTCHA Enterprise assessment.
136+
// Returns: PrivatePasswordLeakVerification which contains
137+
// reencryptedUserCredentialsHash and credential breach database
138+
// whose prefix matches the lookupHashPrefix.
139+
private static PrivatePasswordLeakVerification createPasswordLeakAssessment(
140+
String projectID,
141+
String recaptchaSiteKey,
142+
String token,
143+
String recaptchaAction,
144+
byte[] lookupHashPrefix,
145+
byte[] encryptedUserCredentialsHash)
146+
throws IOException {
147+
try (RecaptchaEnterpriseServiceClient client = RecaptchaEnterpriseServiceClient.create()) {
148+
149+
// Set the properties of the event to be tracked.
150+
Event event = Event.newBuilder().setSiteKey(recaptchaSiteKey).setToken(token).build();
151+
152+
// Set the hashprefix and credentials hash.
153+
// Setting this will trigger the Password leak protection.
154+
PrivatePasswordLeakVerification passwordLeakVerification =
155+
PrivatePasswordLeakVerification.newBuilder()
156+
.setLookupHashPrefix(ByteString.copyFrom(lookupHashPrefix))
157+
.setEncryptedUserCredentialsHash(ByteString.copyFrom(encryptedUserCredentialsHash))
158+
.build();
159+
160+
// Build the assessment request.
161+
CreateAssessmentRequest createAssessmentRequest =
162+
CreateAssessmentRequest.newBuilder()
163+
.setParent(String.format("projects/%s", projectID))
164+
.setAssessment(
165+
Assessment.newBuilder()
166+
.setEvent(event)
167+
// Set request for Password leak verification.
168+
.setPrivatePasswordLeakVerification(passwordLeakVerification)
169+
.build())
170+
.build();
171+
172+
// Send the create assessment request.
173+
Assessment response = client.createAssessment(createAssessmentRequest);
174+
175+
// Check validity and integrity of the response.
176+
if (!checkTokenIntegrity(response.getTokenProperties(), recaptchaAction)) {
177+
return passwordLeakVerification;
178+
}
179+
180+
// Get the reCAPTCHA Enterprise score.
181+
float recaptchaScore = response.getRiskAnalysis().getScore();
182+
System.out.println("The reCAPTCHA score is: " + recaptchaScore);
183+
184+
// Get the assessment name (id). Use this to annotate the assessment.
185+
String assessmentName = response.getName();
186+
System.out.println(
187+
"Assessment name: " + assessmentName.substring(assessmentName.lastIndexOf("/") + 1));
188+
189+
return response.getPrivatePasswordLeakVerification();
190+
}
191+
}
192+
193+
// Check for token validity and action integrity.
194+
private static boolean checkTokenIntegrity(
195+
TokenProperties tokenProperties, String recaptchaAction) {
196+
// Check if the token is valid.
197+
if (!tokenProperties.getValid()) {
198+
System.out.println(
199+
"The Password check call failed because the token was: "
200+
+ tokenProperties.getInvalidReason().name());
201+
return false;
202+
}
203+
204+
// Check if the expected action was executed.
205+
if (!tokenProperties.getAction().equals(recaptchaAction)) {
206+
System.out.printf(
207+
"The action attribute in the reCAPTCHA tag '%s' does not match "
208+
+ "the action '%s' you are expecting to score",
209+
tokenProperties.getAction(), recaptchaAction);
210+
return false;
211+
}
212+
return true;
213+
}
214+
}
215+
// [END recaptcha_enterprise_password_leak_verification]

recaptcha_enterprise/cloud-client/src/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
<groupId>com.google.cloud</groupId>
5252
<artifactId>google-cloud-recaptchaenterprise</artifactId>
5353
</dependency>
54+
<dependency>
55+
<groupId>com.google.cloud</groupId>
56+
<artifactId>recaptcha-password-check-helpers</artifactId>
57+
<version>1.0.1</version>
58+
</dependency>
5459

5560

5661
<!-- [Start_Selenium_dependencies] -->

recaptcha_enterprise/cloud-client/src/test/java/app/SnippetsIT.java

+70-33
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ public class SnippetsIT {
7575
@LocalServerPort private int randomServerPort;
7676
private ByteArrayOutputStream stdOut;
7777

78+
@Test
79+
public void testCreateAnnotateAssessment()
80+
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException,
81+
ExecutionException {
82+
// Create an assessment.
83+
String testURL = "http://localhost:" + randomServerPort + "/";
84+
JSONObject createAssessmentResult =
85+
createAssessment(testURL, ByteString.EMPTY, AssessmentType.ASSESSMENT);
86+
String assessmentName = createAssessmentResult.getString("assessmentName");
87+
// Verify that the assessment name has been modified post the assessment creation.
88+
assertThat(assessmentName).isNotEmpty();
89+
90+
// Annotate the assessment.
91+
AnnotateAssessment.annotateAssessment(PROJECT_ID, assessmentName);
92+
assertThat(stdOut.toString()).contains("Annotated response sent successfully ! ");
93+
}
94+
7895
// Check if the required environment variables are set.
7996
public static void requireEnvVar(String envVarName) {
8097
assertWithMessage(String.format("Missing environment variable '%s' ", envVarName))
@@ -157,24 +174,10 @@ public void testDeleteSiteKey()
157174
assertThat(stdOut.toString()).contains("reCAPTCHA Site key successfully deleted !");
158175
}
159176

160-
@Test
161-
public void testCreateAnnotateAssessment()
162-
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException {
163-
// Create an assessment.
164-
String testURL = "http://localhost:" + randomServerPort + "/";
165-
JSONObject createAssessmentResult = createAssessment(testURL, ByteString.EMPTY);
166-
String assessmentName = createAssessmentResult.getString("assessmentName");
167-
// Verify that the assessment name has been modified post the assessment creation.
168-
assertThat(assessmentName).isNotEmpty();
169-
170-
// Annotate the assessment.
171-
AnnotateAssessment.annotateAssessment(PROJECT_ID, assessmentName);
172-
assertThat(stdOut.toString()).contains("Annotated response sent successfully ! ");
173-
}
174-
175177
@Test
176178
public void testCreateAnnotateAccountDefender()
177-
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException {
179+
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException,
180+
ExecutionException {
178181

179182
String testURL = "http://localhost:" + randomServerPort + "/";
180183
// Create a random SHA-256 Hashed account id.
@@ -186,7 +189,8 @@ public void testCreateAnnotateAccountDefender()
186189
ByteString hashedAccountId = ByteString.copyFrom(hashBytes);
187190

188191
// Create the assessment.
189-
JSONObject createAssessmentResult = createAssessment(testURL, hashedAccountId);
192+
JSONObject createAssessmentResult =
193+
createAssessment(testURL, hashedAccountId, AssessmentType.ACCOUNT_DEFENDER);
190194
String assessmentName = createAssessmentResult.getString("assessmentName");
191195
// Verify that the assessment name has been modified post the assessment creation.
192196
assertThat(assessmentName).isNotEmpty();
@@ -219,33 +223,58 @@ public void testCreateAnnotateAccountDefender()
219223
"Finished searching related account group memberships for %s", hashedAccountId));
220224
}
221225

226+
@Test
222227
public void testGetMetrics() throws IOException {
223228
GetMetrics.getMetrics(PROJECT_ID, RECAPTCHA_SITE_KEY_1);
224229
assertThat(stdOut.toString())
225230
.contains("Retrieved the bucket count for score based key: " + RECAPTCHA_SITE_KEY_1);
226231
}
227232

228-
public JSONObject createAssessment(String testURL)
229-
throws IOException, JSONException, InterruptedException {
233+
@Test
234+
public void testPasswordLeakAssessment()
235+
throws JSONException, IOException, ExecutionException, InterruptedException {
236+
String testURL = "http://localhost:" + randomServerPort + "/";
237+
createAssessment(testURL, ByteString.EMPTY, AssessmentType.PASSWORD_LEAK);
238+
assertThat(stdOut.toString()).contains("Is Credential leaked: ");
239+
}
240+
241+
public JSONObject createAssessment(
242+
String testURL, ByteString hashedAccountId, AssessmentType assessmentType)
243+
throws IOException, JSONException, InterruptedException, ExecutionException {
230244

231245
// Setup the automated browser test and retrieve the token and action.
232246
JSONObject tokenActionPair = initiateBrowserTest(testURL);
233247

234248
// Send the token for analysis. The analysis score ranges from 0.0 to 1.0
235-
if (!hashedAccountId.isEmpty()) {
236-
AccountDefenderAssessment.accountDefenderAssessment(
237-
PROJECT_ID,
238-
RECAPTCHA_SITE_KEY_1,
239-
tokenActionPair.getString("token"),
240-
tokenActionPair.getString("action"),
241-
hashedAccountId);
242-
243-
} else {
244-
recaptcha.CreateAssessment.createAssessment(
245-
PROJECT_ID,
246-
RECAPTCHA_SITE_KEY_1,
247-
tokenActionPair.getString("token"),
248-
tokenActionPair.getString("action"));
249+
switch (assessmentType) {
250+
case ACCOUNT_DEFENDER:
251+
{
252+
AccountDefenderAssessment.accountDefenderAssessment(
253+
PROJECT_ID,
254+
RECAPTCHA_SITE_KEY_1,
255+
tokenActionPair.getString("token"),
256+
tokenActionPair.getString("action"),
257+
hashedAccountId);
258+
break;
259+
}
260+
case ASSESSMENT:
261+
{
262+
recaptcha.CreateAssessment.createAssessment(
263+
PROJECT_ID,
264+
RECAPTCHA_SITE_KEY_1,
265+
tokenActionPair.getString("token"),
266+
tokenActionPair.getString("action"));
267+
break;
268+
}
269+
case PASSWORD_LEAK:
270+
{
271+
passwordleak.CreatePasswordLeakAssessment.checkPasswordLeak(
272+
PROJECT_ID,
273+
RECAPTCHA_SITE_KEY_1,
274+
tokenActionPair.getString("token"),
275+
tokenActionPair.getString("action"));
276+
break;
277+
}
249278
}
250279

251280
// Assert the response.
@@ -274,6 +303,14 @@ public JSONObject createAssessment(String testURL)
274303
.put("assessmentName", assessmentName);
275304
}
276305

306+
enum AssessmentType {
307+
ASSESSMENT,
308+
ACCOUNT_DEFENDER,
309+
PASSWORD_LEAK;
310+
311+
AssessmentType() {}
312+
}
313+
277314
public JSONObject initiateBrowserTest(String testURL)
278315
throws IOException, JSONException, InterruptedException {
279316
// Construct the URL to call for validating the assessment.

0 commit comments

Comments
 (0)