Skip to content
This repository has been archived by the owner on Sep 16, 2023. It is now read-only.

docs(samples): add password leak sample and test #808

Merged
merged 8 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ If you are using Maven without BOM, add this to your dependencies:
If you are using Gradle 5.x or later, add this to your dependencies

```Groovy
implementation platform('com.google.cloud:libraries-bom:25.3.0')
implementation platform('com.google.cloud:libraries-bom:25.4.0')

implementation 'com.google.cloud:google-cloud-recaptchaenterprise'
```
Expand Down Expand Up @@ -123,6 +123,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-recaptchaente
| List Related Account Group Memberships | [source code](https://github.com/googleapis/java-recaptchaenterprise/blob/main/samples/snippets/cloud-client/src/main/java/recaptcha/account_defender/ListRelatedAccountGroupMemberships.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-recaptchaenterprise&page=editor&open_in_editor=samples/snippets/cloud-client/src/main/java/recaptcha/account_defender/ListRelatedAccountGroupMemberships.java) |
| List Related Account Groups | [source code](https://github.com/googleapis/java-recaptchaenterprise/blob/main/samples/snippets/cloud-client/src/main/java/recaptcha/account_defender/ListRelatedAccountGroups.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-recaptchaenterprise&page=editor&open_in_editor=samples/snippets/cloud-client/src/main/java/recaptcha/account_defender/ListRelatedAccountGroups.java) |
| Search Related Account Group Memberships | [source code](https://github.com/googleapis/java-recaptchaenterprise/blob/main/samples/snippets/cloud-client/src/main/java/recaptcha/account_defender/SearchRelatedAccountGroupMemberships.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-recaptchaenterprise&page=editor&open_in_editor=samples/snippets/cloud-client/src/main/java/recaptcha/account_defender/SearchRelatedAccountGroupMemberships.java) |
| Create Password Leak Assessment | [source code](https://github.com/googleapis/java-recaptchaenterprise/blob/main/samples/snippets/cloud-client/src/main/java/recaptcha/passwordleak/CreatePasswordLeakAssessment.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-recaptchaenterprise&page=editor&open_in_editor=samples/snippets/cloud-client/src/main/java/recaptcha/passwordleak/CreatePasswordLeakAssessment.java) |



Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package passwordleak;

// [START recaptcha_enterprise_password_leak_verification]

import com.google.cloud.recaptcha.passwordcheck.PasswordCheckResult;
import com.google.cloud.recaptcha.passwordcheck.PasswordCheckVerification;
import com.google.cloud.recaptcha.passwordcheck.PasswordCheckVerifier;
import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient;
import com.google.protobuf.ByteString;
import com.google.recaptchaenterprise.v1.Assessment;
import com.google.recaptchaenterprise.v1.CreateAssessmentRequest;
import com.google.recaptchaenterprise.v1.Event;
import com.google.recaptchaenterprise.v1.PrivatePasswordLeakVerification;
import com.google.recaptchaenterprise.v1.TokenProperties;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

public class CreatePasswordLeakAssessment {

public static void main(String[] args)
throws IOException, ExecutionException, InterruptedException {
// TODO(developer): Replace these variables before running the sample.
// GCloud Project ID.
String projectID = "project-id";

// Site key obtained by registering a domain/app to use recaptcha Enterprise.
String recaptchaSiteKey = "recaptcha-site-key";

// The token obtained from the client on passing the recaptchaSiteKey.
// To get the token, integrate the recaptchaSiteKey with frontend. See,
// https://cloud.google.com/recaptcha-enterprise/docs/instrument-web-pages#frontend_integration_score
String token = "recaptcha-token";

// Action name corresponding to the token.
String recaptchaAction = "recaptcha-action";

checkPasswordLeak(projectID, recaptchaSiteKey, token, recaptchaAction);
}

/*
* Detect password leaks and breached credentials to prevent account takeovers (ATOs)
* and credential stuffing attacks.
* For more information, see: https://cloud.google.com/recaptcha-enterprise/docs/getting-started
* and https://security.googleblog.com/2019/02/protect-your-accounts-from-data.html

* Steps:
* 1. Use the 'createVerification' method to hash and Encrypt the hashed username and password.
* 2. Send the hash prefix (2-byte) and the encrypted credentials to create the assessment.
* (Hash prefix is used to partition the database.)
* 3. Password leak assessment returns a database whose prefix matches the sent hash prefix.
* Create Assessment also sends back re-encrypted credentials.
* 4. The re-encrypted credential is then locally verified to see if there is a
* match in the database.
*
* To perform hashing, encryption and verification (steps 1, 2 and 4),
* reCAPTCHA Enterprise provides a helper library in Java.
* See, https://github.com/GoogleCloudPlatform/java-recaptcha-password-check-helpers

* If you want to extend this behavior to your own implementation/ languages,
* make sure to perform the following steps:
* 1. Hash the credentials (First 2 bytes of the result is the 'lookupHashPrefix')
* 2. Encrypt the hash (result = 'encryptedUserCredentialsHash')
* 3. Get back the PasswordLeak information from reCAPTCHA Enterprise Create Assessment.
* 4. Decrypt the obtained 'credentials.getReencryptedUserCredentialsHash()'
* with the same key you used for encryption.
* 5. Check if the decrypted credentials are present in 'credentials.getEncryptedLeakMatchPrefixesList()'.
* 6. If there is a match, that indicates a credential breach.
*/
public static void checkPasswordLeak(
String projectID, String recaptchaSiteKey, String token, String recaptchaAction)
throws ExecutionException, InterruptedException, IOException {
// Set the username and password to be checked.
String username = "username";
String password = "password123";

// Instantiate the java-password-leak-helper library to perform the cryptographic functions.
PasswordCheckVerifier passwordLeak = new PasswordCheckVerifier();

// Create the request to obtain the hash prefix and encrypted credentials.
PasswordCheckVerification verification =
passwordLeak.createVerification(username, password).get();

byte[] lookupHashPrefix = verification.getLookupHashPrefix();
byte[] encryptedUserCredentialsHash = verification.getEncryptedLookupHash();

// Pass the credentials to the createPasswordLeakAssessment() to get back
// the matching database entry for the hash prefix.
PrivatePasswordLeakVerification credentials =
createPasswordLeakAssessment(
projectID,
recaptchaSiteKey,
token,
recaptchaAction,
lookupHashPrefix,
encryptedUserCredentialsHash);

// Convert to appropriate input format.
List<byte[]> leakMatchPrefixes =
credentials.getEncryptedLeakMatchPrefixesList().stream()
.map(ByteString::toByteArray)
.collect(Collectors.toList());

// Verify if the encrypted credentials are present in the obtained match list.
PasswordCheckResult result =
passwordLeak
.verify(
verification,
credentials.getReencryptedUserCredentialsHash().toByteArray(),
leakMatchPrefixes)
.get();

// Check if the credential is leaked.
boolean isLeaked = result.areCredentialsLeaked();
System.out.printf("Is Credential leaked: %s", isLeaked);
}

// Create a reCAPTCHA Enterprise assessment.
// Returns: PrivatePasswordLeakVerification which contains
// reencryptedUserCredentialsHash and credential breach database
// whose prefix matches the lookupHashPrefix.
private static PrivatePasswordLeakVerification createPasswordLeakAssessment(
String projectID,
String recaptchaSiteKey,
String token,
String recaptchaAction,
byte[] lookupHashPrefix,
byte[] encryptedUserCredentialsHash)
throws IOException {
try (RecaptchaEnterpriseServiceClient client = RecaptchaEnterpriseServiceClient.create()) {

// Set the properties of the event to be tracked.
Event event = Event.newBuilder().setSiteKey(recaptchaSiteKey).setToken(token).build();

// Set the hashprefix and credentials hash.
// Setting this will trigger the Password leak protection.
PrivatePasswordLeakVerification passwordLeakVerification =
PrivatePasswordLeakVerification.newBuilder()
.setLookupHashPrefix(ByteString.copyFrom(lookupHashPrefix))
.setEncryptedUserCredentialsHash(ByteString.copyFrom(encryptedUserCredentialsHash))
.build();

// Build the assessment request.
CreateAssessmentRequest createAssessmentRequest =
CreateAssessmentRequest.newBuilder()
.setParent(String.format("projects/%s", projectID))
.setAssessment(
Assessment.newBuilder()
.setEvent(event)
// Set request for Password leak verification.
.setPrivatePasswordLeakVerification(passwordLeakVerification)
.build())
.build();

// Send the create assessment request.
Assessment response = client.createAssessment(createAssessmentRequest);

// Check validity and integrity of the response.
if (!checkTokenIntegrity(response.getTokenProperties(), recaptchaAction)) {
return passwordLeakVerification;
}

// Get the reCAPTCHA Enterprise score.
float recaptchaScore = response.getRiskAnalysis().getScore();
System.out.println("The reCAPTCHA score is: " + recaptchaScore);

// Get the assessment name (id). Use this to annotate the assessment.
String assessmentName = response.getName();
System.out.println(
"Assessment name: " + assessmentName.substring(assessmentName.lastIndexOf("/") + 1));

return response.getPrivatePasswordLeakVerification();
}
}

// Check for token validity and action integrity.
private static boolean checkTokenIntegrity(
TokenProperties tokenProperties, String recaptchaAction) {
// Check if the token is valid.
if (!tokenProperties.getValid()) {
System.out.println(
"The Password check call failed because the token was: "
+ tokenProperties.getInvalidReason().name());
return false;
}

// Check if the expected action was executed.
if (!tokenProperties.getAction().equals(recaptchaAction)) {
System.out.printf(
"The action attribute in the reCAPTCHA tag '%s' does not match "
+ "the action '%s' you are expecting to score",
tokenProperties.getAction(), recaptchaAction);
return false;
}
return true;
}
}
// [END recaptcha_enterprise_password_leak_verification]
5 changes: 5 additions & 0 deletions samples/snippets/cloud-client/src/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-recaptchaenterprise</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>recaptcha-password-check-helpers</artifactId>
<version>1.0.1</version>
</dependency>


<!-- [Start_Selenium_dependencies] -->
Expand Down
103 changes: 70 additions & 33 deletions samples/snippets/cloud-client/src/test/java/app/SnippetsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ public class SnippetsIT {
@LocalServerPort private int randomServerPort;
private ByteArrayOutputStream stdOut;

@Test
public void testCreateAnnotateAssessment()
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException,
ExecutionException {
// Create an assessment.
String testURL = "http://localhost:" + randomServerPort + "/";
JSONObject createAssessmentResult =
createAssessment(testURL, ByteString.EMPTY, AssessmentType.ASSESSMENT);
String assessmentName = createAssessmentResult.getString("assessmentName");
// Verify that the assessment name has been modified post the assessment creation.
assertThat(assessmentName).isNotEmpty();

// Annotate the assessment.
AnnotateAssessment.annotateAssessment(PROJECT_ID, assessmentName);
assertThat(stdOut.toString()).contains("Annotated response sent successfully ! ");
}

// Check if the required environment variables are set.
public static void requireEnvVar(String envVarName) {
assertWithMessage(String.format("Missing environment variable '%s' ", envVarName))
Expand Down Expand Up @@ -157,24 +174,10 @@ public void testDeleteSiteKey()
assertThat(stdOut.toString()).contains("reCAPTCHA Site key successfully deleted !");
}

@Test
public void testCreateAnnotateAssessment()
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException {
// Create an assessment.
String testURL = "http://localhost:" + randomServerPort + "/";
JSONObject createAssessmentResult = createAssessment(testURL, ByteString.EMPTY);
String assessmentName = createAssessmentResult.getString("assessmentName");
// Verify that the assessment name has been modified post the assessment creation.
assertThat(assessmentName).isNotEmpty();

// Annotate the assessment.
AnnotateAssessment.annotateAssessment(PROJECT_ID, assessmentName);
assertThat(stdOut.toString()).contains("Annotated response sent successfully ! ");
}

@Test
public void testCreateAnnotateAccountDefender()
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException {
throws JSONException, IOException, InterruptedException, NoSuchAlgorithmException,
ExecutionException {

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

// Create the assessment.
JSONObject createAssessmentResult = createAssessment(testURL, hashedAccountId);
JSONObject createAssessmentResult =
createAssessment(testURL, hashedAccountId, AssessmentType.ACCOUNT_DEFENDER);
String assessmentName = createAssessmentResult.getString("assessmentName");
// Verify that the assessment name has been modified post the assessment creation.
assertThat(assessmentName).isNotEmpty();
Expand Down Expand Up @@ -219,33 +223,58 @@ public void testCreateAnnotateAccountDefender()
"Finished searching related account group memberships for %s", hashedAccountId));
}

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

public JSONObject createAssessment(String testURL)
throws IOException, JSONException, InterruptedException {
@Test
public void testPasswordLeakAssessment()
throws JSONException, IOException, ExecutionException, InterruptedException {
String testURL = "http://localhost:" + randomServerPort + "/";
createAssessment(testURL, ByteString.EMPTY, AssessmentType.PASSWORD_LEAK);
assertThat(stdOut.toString()).contains("Is Credential leaked: ");
}

public JSONObject createAssessment(
Sita04 marked this conversation as resolved.
Show resolved Hide resolved
String testURL, ByteString hashedAccountId, AssessmentType assessmentType)
throws IOException, JSONException, InterruptedException, ExecutionException {

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

// Send the token for analysis. The analysis score ranges from 0.0 to 1.0
if (!hashedAccountId.isEmpty()) {
AccountDefenderAssessment.accountDefenderAssessment(
PROJECT_ID,
RECAPTCHA_SITE_KEY_1,
tokenActionPair.getString("token"),
tokenActionPair.getString("action"),
hashedAccountId);

} else {
recaptcha.CreateAssessment.createAssessment(
PROJECT_ID,
RECAPTCHA_SITE_KEY_1,
tokenActionPair.getString("token"),
tokenActionPair.getString("action"));
switch (assessmentType) {
case ACCOUNT_DEFENDER:
{
AccountDefenderAssessment.accountDefenderAssessment(
PROJECT_ID,
RECAPTCHA_SITE_KEY_1,
tokenActionPair.getString("token"),
tokenActionPair.getString("action"),
hashedAccountId);
break;
}
case ASSESSMENT:
{
recaptcha.CreateAssessment.createAssessment(
PROJECT_ID,
RECAPTCHA_SITE_KEY_1,
tokenActionPair.getString("token"),
tokenActionPair.getString("action"));
break;
}
case PASSWORD_LEAK:
{
passwordleak.CreatePasswordLeakAssessment.checkPasswordLeak(
PROJECT_ID,
RECAPTCHA_SITE_KEY_1,
tokenActionPair.getString("token"),
tokenActionPair.getString("action"));
break;
}
}

// Assert the response.
Expand Down Expand Up @@ -274,6 +303,14 @@ public JSONObject createAssessment(String testURL)
.put("assessmentName", assessmentName);
}

enum AssessmentType {
ASSESSMENT,
ACCOUNT_DEFENDER,
PASSWORD_LEAK;

AssessmentType() {}
}

public JSONObject initiateBrowserTest(String testURL)
throws IOException, JSONException, InterruptedException {
// Construct the URL to call for validating the assessment.
Expand Down