RecaptchaPasswordCheck (RecaptchaPasswordCheck v0.1.0)

Copy Markdown View Source

Client-side cryptography and canonicalization for Google Cloud Fraud Defense's private password leak check.

Google Cloud Fraud Defense uses canonicalized usernames.

  1. Everything after the first @ is trimmed.
  2. .'s are removed.
  3. Every ascii character is downcased.

For example: J.Doe@gmail.com and jdoe@hotmail.com both equate to jdoe for leak checks.

Example Usage

project = "my_super_cool_gcp_project"
token = "my_super_secret_bearer_token"

# https://www.youtube.com/watch?v=gYs9nS8LlZ8
{:ok, verification} =
  RecaptchaPasswordCheck.create("GabeN@valvesoftware.com", "MoolyFTW")

# Bring your own http/proto3 client
body = %{
  "privatePasswordLeakVerification" => %{
    "lookupHashPrefix" => Base.encode64(verification.lookup_hash_prefix),
    "encryptedUserCredentialsHash" =>
      Base.encode64(verification.encrypted_user_credentials_hash)
  },
  # "otherReCaptchaAssessmentFields" => "..."
}

response =
  Req.post!("https://recaptchaenterprise.googleapis.com/v1/projects/#{project}/assessments",
    json: body,
    headers: [{"authorization", "Bearer " <> token}],
    receive_timeout: 5_000
  )

# Short of adopting `:protobuf` to restore proto3's presence semantics,
# the response shapes have to be spelled out by hand.
case response do
  %{
    status: 200,
    body: %{
      "privatePasswordLeakVerification" => %{
        "reencryptedUserCredentialsHash" => reencrypted_user_credentials_hash,
        "encryptedLeakMatchPrefixes" => encrypted_leak_match_prefixes
      }
    }
  } ->
    RecaptchaPasswordCheck.leaked?(
      verification,
      Base.decode64!(reencrypted_user_credentials_hash),
      Enum.map(encrypted_leak_match_prefixes, &Base.decode64!/1)
    )

  %{status: 200, body: %{"privatePasswordLeakVerification" => _}} ->
    false

  %{status: status, body: body} ->
    {:error, {status, body}}
end

Get the token from :goth in an application or from gcloud auth print-access-token when trying this out by hand.

Summary

Functions

Builds a RecaptchaPasswordCheck struct for a username and password.

Unblinds the response fields using the verification, returning true if the credentials were found in a leak.

Functions

%RecaptchaPasswordCheck{}

(struct)

Defines the RecaptchaPasswordCheck struct.

Its fields are:

  • :username - the username as it was supplied.
  • :canonical_username - that username after canonicalization, which is what every hash here derives from.
  • :lookup_hash_prefix - four bytes holding the leading 26 bits of the canonical username's hash, zero-filled. It names the bucket of leaks the service searches, and is the only thing the service learns about the username.
  • :encrypted_user_credentials_hash - the scrypt hash of the canonical username and password, blinded by multiplication on P-256 and encoded as a 33-byte compressed point. The service re-encrypts it under its own key without being able to read it.
  • :private_key - the ephemeral scalar used for that blinding, and the only means of removing it from the reply. Never reused, never transmitted, and redacted when the struct is inspected.

create(username, password)

Builds a RecaptchaPasswordCheck struct for a username and password.

Runs scrypt and an elliptic curve multiplication, so expect low tens of milliseconds.

Returns {:ok, verification}, or {:error, :empty_canonical_username} when canonicalization consumes the username entirely — "@example.com" reduces to nothing, and a query on an empty username would match on the password alone.

Raises when either credential is empty or not a binary, which is a caller error rather than a property of the input.

leaked?(verification, reencrypted_hash, match_prefixes)

Unblinds the response fields using the verification, returning true if the credentials were found in a leak.