eter_oprf 1.0.0
eter_oprf: ^1.0.0 copied to clipboard
RFC 9497 (2HashDH) OPRF protocol on P-256 for Dart/Flutter. Interoperable with @noble/curves server implementations. Supports threshold 3-of-3 additive OPRF. No P-256 OPRF library exists for Dart.
eter_oprf #
RFC 9497 (2HashDH) Oblivious Pseudorandom Function (OPRF) on P-256 for Dart and Flutter.
Interoperable with the @eter/oprf TypeScript/Node.js package built on @noble/curves. Supports threshold 3-of-3 additive OPRF. No P-256 OPRF library previously existed for Dart.
What is this? #
An Oblivious PRF lets a client compute F(key, input) — a pseudorandom function keyed by the server's secret — without the server ever learning what input was.
This implementation uses the 2HashDH construction from RFC 9497 on the NIST P-256 curve:
H(x) = hash_to_group(username) # username → P-256 point
blind = r * H(x) # client blinds with random r
evaluate = skS * blind # server multiplies by secret key
unblind = r⁻¹ * evaluate = skS * H(x) # client removes blinding factor
token = SHA-256("Eter-OPRF-token|" ‖ username ‖ "|" ‖ hex(unblind))
The server sees only r * H(username), which is computationally indistinguishable from a uniformly random P-256 point. The server cannot determine which username was queried.
Why? #
Use case: private user lookup.
In a messaging app, checking whether a username exists leaks that username to the server. With OPRF, the client can look up a user without revealing the username — the server learns only that some lookup happened, not which user was queried.
This enables username privacy exceeding what's achievable with simple hashing or even SGX, using pure cryptography.
Platform support #
| Platform | Supported |
|---|---|
| Dart 3+ | Yes |
| Flutter (iOS, Android, macOS, Linux, Windows, Web) | Yes |
Pure Dart — no native code, no platform channels.
Installation #
Add to your pubspec.yaml:
dependencies:
eter_oprf: ^1.0.0
Then run:
dart pub get
Usage — Basic OPRF flow #
import 'package:eter_oprf/eter_oprf.dart';
// Step 1: Hash username to P-256 point
final H = OprfClient.hashToGroup('alice');
// Step 2: Generate a random blinding scalar
final r = OprfClient.randomScalar();
// Step 3: Blind the point (send blindedHex to your server)
final blinded = OprfClient.blind(H, r);
final blindedHex = OprfClient.serializePointToHex(blinded);
// Step 4: Server evaluates (skS * blinded) and returns evaluatedHex
// ... call your server here ...
final String evaluatedHex = await callServer(blindedHex);
// Step 5: Unblind the server's response
final evaluated = OprfClient.deserializePointFromHex(evaluatedHex);
final N = OprfClient.unblind(evaluated, r);
// Step 6: Compute the OPRF token
final token = OprfClient.computeToken('alice', N);
// token is a 64-char hex SHA-256 digest
// Use it to look up oprf_tokens/{token} in your database
Usage — Threshold 3-of-3 OPRF #
For higher security, split the server secret key skS additively across three servers (s1 + s2 + s3 ≡ skS mod n). Each server evaluates independently; the client combines the responses:
import 'package:eter_oprf/eter_oprf.dart';
final H = OprfClient.hashToGroup('alice');
final r = OprfClient.randomScalar();
final blinded = OprfClient.blind(H, r);
final blindedHex = OprfClient.serializePointToHex(blinded);
// Call all three share servers in parallel
final results = await Future.wait([
callShareServer1(blindedHex),
callShareServer2(blindedHex),
callShareServer3(blindedHex),
]);
// Deserialize each partial evaluation
final e1 = OprfClient.deserializePointFromHex(results[0]);
final e2 = OprfClient.deserializePointFromHex(results[1]);
final e3 = OprfClient.deserializePointFromHex(results[2]);
// Combine: (s1 + s2 + s3) * blind = skS * blind
final combined = OprfClient.addPoints(OprfClient.addPoints(e1, e2), e3);
// Unblind and compute token as usual
final N = OprfClient.unblind(combined, r);
final token = OprfClient.computeToken('alice', N);
Protocol constants #
The following domain separation strings are public protocol constants shared between this package and the @eter/oprf npm package. They must not be changed — doing so breaks cross-platform interoperability.
| Constant | Used in |
|---|---|
| `'Eter-OPRF-htg | '` |
| `'Eter-OPRF-token | '` |
These constants follow the RFC 9497 recommendation for domain separation in OPRF protocols.
Interoperability with @eter/oprf #
This package is byte-for-byte interoperable with the @eter/oprf TypeScript package, which uses @noble/curves for P-256 arithmetic. The two packages share:
- The same
hashToGroupalgorithm (try-and-increment with SHA-256) - The same domain strings
- The same compressed-point serialization format (SEC 1 §2.3.3)
- The same token derivation formula
A server running @eter/oprf can evaluate a blind sent from this Dart client, and the resulting token will match.
Security properties #
- Obliviousness: The server learns nothing about the client's input beyond the fact that an evaluation occurred.
- Pseudorandomness: The OPRF output is computationally indistinguishable from a random function, assuming the CDH assumption holds on P-256.
- One-more PRF security: An adversary cannot compute
F(skS, x*)for a freshx*even after polynomially many adaptive queries. - Threshold security (3-of-3): No single share server learns the full key. An attacker must compromise all three servers to break privacy.
References #
- RFC 9497 — Oblivious Pseudorandom Functions (OPRFs) Using Prime-Order Groups
- Companion npm package: @eter/oprf
- pointycastle — P-256 arithmetic
- crypto — SHA-256
Built by Eter — encrypted, anti-forensic messaging.