Deriving The Login Bucket
How clients derive `login_bidx`, the privacy-preserving bucket used before registration and login.
Every login and registration requires a login_bidx — a 13-bit integer (0–8191) that tells the server which bucket of candidate records to return. Deriving it requires a single round-trip to an OPRF challenge endpoint.
What This Step Is For
login_bidx is the privacy-preserving bucket used before registration or login. It is derived through OPRF so the raw login identifier never reaches Literal.
This step happens before any OPAQUE handshake. The client cannot call authenticate-start or register-start without a valid login_bidx.
Privacy guarantee
The client blinds the normalized login identifier before sending it to Literal. Literal evaluates the blinded value and returns the result, but does not receive the raw email.
Overview
OPRF flow summary
The client normalizes the identifier, blinds it with a random scalar r, and sends only the
blinded ristretto255 point to the server. The server multiplies by its secret OPRF key and
returns the evaluated point — without ever seeing the raw identifier or the blinding scalar.
The client unblinds with the modular inverse of r, hashes with the
"literal-oprf-finalize-v1" domain separator, and truncates to 13 bits to derive login_bidx.
| Step | Where | What Happens |
|---|---|---|
| 1. Normalize | Client | Trim whitespace, apply Unicode NFC, lowercase. |
| 2. Blind | Client | Hash login identifier to a curve point, multiply by a random scalar. |
| 3. Evaluate | Literal | Multiply blinded point by the server’s secret key. |
| 4. Unblind | Client | Remove the blinding scalar, hash the result, truncate to 13 bits. |
Literal sees only a random-looking curve point. The client sees only Literal’s transformed result. The original login identifier and the server’s key remain private to their respective owners throughout.
Step 1 — Normalize The Login Identifier
Before blinding, normalize the login identifier so the same address always derives the same bucket regardless of how it was typed:
- Trim leading and trailing whitespace.
- Apply Unicode NFC normalization.
- Convert to lowercase.
Use the same normalization every time so the same login identifier derives the same bucket. The normalization must match the client behavior used during both registration and login. Do not strip + tags or Gmail dots — those are provider-specific and are preserved.
Step 2 — Blind The Identifier
Hash the normalized login identifier to a point on the ristretto255 group using hash-to-curve (RFC 9380, XMD:SHA-512) with the domain separation tag "literal-oprf-v1". Then multiply that point by a random scalar r:
blinded_element = r * hash_to_curve(normalized_identifier)r is the blinding scalar — keep it in memory. It is never sent anywhere. Without it, the server’s response cannot be unblinded.
Encode the resulting 32-byte point as standard base64 for transmission.
Step 3 — Evaluate The Blinded Element
The evaluation happens through:
POST /v1/auth/challenges
The endpoint accepts a blinded ristretto255 point and returns the server’s OPRF evaluation. No authentication is required. The endpoint is rate-limited.
Use the API Reference for the exact request and response schema.
Step 4 — Unblind And Derive The Login Bucket
Decode the evaluated element from base64. Remove the blinding scalar by multiplying by its modular inverse:
unblinded = (1/r) * evaluated_elementThen finalize to produce login_bidx:
digest = SHA-256(unblinded_bytes || "literal-oprf-finalize-v1")
login_bidx = (digest[0] | (digest[1] << 8)) & 0x1FFFTake the first two bytes of the SHA-256 digest as a little-endian 16-bit integer, then mask to 13 bits. The result is an integer in [0, 8191] — the derived login_bidx.
Pass this value as the login_bidx field in register-start or authenticate-start.
Troubleshooting
Invalid blinded elements return 400. Check point encoding and curve arithmetic before retrying.
The challenge endpoint is rate-limited. Back off on 429.
See the API Reference for complete error responses.
Security Properties
The raw login identifier never reaches Literal. The blinding scalar r transforms the identifier’s curve hash into a point indistinguishable from random. The server receives only a blinded group element derived by the client.
Literal observes the bucket number, not the identifier. login_bidx is sent in the clear to authenticate-start. The server uses it to return a padded set of candidate records — the response always contains the same number of entries regardless of how many real records exist in that bucket. The bucket number is non-unique and does not identify the login identifier by itself.
The client cannot learn the server’s key. The server multiplies by its secret scalar without exposing it. The client receives only the transformed output.
The login bucket is not a reusable identifier. The finalization step produces only a truncated bucket index, not a reusable identifier for the login identifier.
Cross-protocol separation. The domain separation tag "literal-oprf-v1" ensures that the OPRF output is cryptographically distinct from any other use of the same curve in the platform.
Related Resources
- Authentication — registration and login flows that consume
login_bidx. - Zero-Knowledge Model — why the server is structured not to receive plaintext credentials.
- Blind Routing — how other identifiers are similarly protected throughout the platform.
Last updated on