@structured-id/opaque 0.0.0-development → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @structured-id/opaque
2
+
3
+ [![CI/CD](https://github.com/structured-id/opaque/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/structured-id/opaque/actions/workflows/ci-cd.yml)
4
+ [![codecov](https://codecov.io/gh/structured-id/opaque/graph/badge.svg?token=923LHCL7KN)](https://codecov.io/gh/structured-id/opaque)
5
+ [![npm](https://img.shields.io/npm/v/@structured-id/opaque)](https://www.npmjs.com/package/@structured-id/opaque)
6
+
7
+ OPAQUE (RFC 9807) client library for browser and Node.js environments.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @structured-id/opaque
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { OpaqueClient } from '@structured-id/opaque';
19
+
20
+ const client = new OpaqueClient({ serverId: 'auth.example.com' });
21
+
22
+ // Registration
23
+ const reg = await client.registrationStart(password);
24
+ // Send `reg.request` to server, receive `serverResponse`
25
+ const { record, exportKey } = await client.registrationFinish(password, serverResponse, reg.state);
26
+
27
+ // Login
28
+ const login = await client.loginStart(password);
29
+ // Send `login.request` to server, receive `serverResponse`
30
+ const { finalization, sessionKey, exportKey: loginExportKey } = await client.loginFinish(
31
+ password,
32
+ serverResponse,
33
+ login.state,
34
+ );
35
+ ```
36
+
37
+ ## Status
38
+
39
+ > **NOT PRODUCTION READY** — uses placeholder, insecure cryptography. DO NOT USE for real user data. This is not a compliant OPAQUE (RFC 9807) implementation.
40
+
41
+ Early development. Crypto modules are temporary placeholders using WebCrypto HKDF/SHA-256. A full OPAQUE implementation with ristretto255 and 3DH AKE will be added via WASM.
42
+
43
+ ## License
44
+
45
+ Apache-2.0
@@ -1,24 +1,33 @@
1
+ 'use strict';
2
+
1
3
  // src/crypto/utils.ts
2
4
  var encoder = new TextEncoder();
3
5
  function encode(input) {
4
6
  return encoder.encode(input);
5
7
  }
6
8
  async function hash(data) {
7
- return crypto.subtle.digest("SHA-256", data);
9
+ const digest = await crypto.subtle.digest("SHA-256", data);
10
+ return new Uint8Array(digest);
8
11
  }
9
- async function hkdfExpand(prk, info, length) {
10
- const key = await crypto.subtle.importKey(
11
- "raw",
12
- prk,
13
- { name: "HKDF" },
14
- false,
15
- ["deriveBits"]
16
- );
12
+ function concat(...arrays) {
13
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
14
+ const result = new Uint8Array(totalLength);
15
+ let offset = 0;
16
+ for (const arr of arrays) {
17
+ result.set(arr, offset);
18
+ offset += arr.length;
19
+ }
20
+ return result;
21
+ }
22
+ async function hkdfDerive(ikm, info, length) {
23
+ const key = await crypto.subtle.importKey("raw", ikm, { name: "HKDF" }, false, ["deriveBits"]);
17
24
  const derived = await crypto.subtle.deriveBits(
18
25
  {
19
26
  name: "HKDF",
20
27
  hash: "SHA-256",
28
+ // Placeholder: zero salt — will use proper salt in WASM implementation
21
29
  salt: new Uint8Array(32),
30
+ // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)
22
31
  info: encode(info)
23
32
  },
24
33
  key,
@@ -31,23 +40,23 @@ async function hkdfExpand(prk, info, length) {
31
40
  async function oprfBlind(password) {
32
41
  const input = encode(password);
33
42
  const blind = crypto.getRandomValues(new Uint8Array(32));
34
- const combined = new Uint8Array([...input, ...blind]);
35
- const blindedElement = new Uint8Array(await hash(combined));
43
+ const combined = concat(input, blind);
44
+ const blindedElement = await hash(combined);
36
45
  return { blindedElement, blind };
37
46
  }
38
47
  async function oprfFinalize(password, evaluatedElement, blind) {
39
48
  const input = encode(password);
40
- const combined = new Uint8Array([...input, ...evaluatedElement, ...blind]);
41
- return new Uint8Array(await hash(combined));
49
+ const combined = concat(input, evaluatedElement, blind);
50
+ return hash(combined);
42
51
  }
43
52
 
44
53
  // src/crypto/envelope.ts
45
54
  async function buildEnvelope(oprfOutput, serverId) {
46
- const info = new TextEncoder().encode(`OPAQUE-Envelope-${serverId}`);
47
- const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));
48
- const envelopeKey = await hkdfExpand(prk, "envelope-key", 32);
49
- const exportKey = await hkdfExpand(prk, "export-key", 32);
50
- const envelope = new Uint8Array([...envelopeKey]);
55
+ const info = encode(`OPAQUE-Envelope-${serverId}`);
56
+ const ikm = await hash(concat(oprfOutput, info));
57
+ const envelopeKey = await hkdfDerive(ikm, "envelope-key", 32);
58
+ const exportKey = await hkdfDerive(ikm, "export-key", 32);
59
+ const envelope = envelopeKey.slice();
51
60
  return { envelope, exportKey };
52
61
  }
53
62
 
@@ -62,7 +71,7 @@ async function registrationStart(password, _serverId) {
62
71
  async function registrationFinish(password, serverResponse, state, serverId) {
63
72
  const oprfOutput = await oprfFinalize(password, serverResponse, state);
64
73
  const { envelope, exportKey } = await buildEnvelope(oprfOutput, serverId);
65
- const record = new Uint8Array([...envelope]);
74
+ const record = envelope.slice();
66
75
  return {
67
76
  record,
68
77
  exportKey
@@ -71,11 +80,11 @@ async function registrationFinish(password, serverResponse, state, serverId) {
71
80
 
72
81
  // src/crypto/ake.ts
73
82
  async function deriveKeys(oprfOutput, serverId) {
74
- const info = new TextEncoder().encode(`OPAQUE-AKE-${serverId}`);
75
- const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));
76
- const sessionKey = await hkdfExpand(prk, "session-key", 32);
77
- const exportKey = await hkdfExpand(prk, "export-key", 32);
78
- const finalization = await hkdfExpand(prk, "finalization", 32);
83
+ const info = encode(`OPAQUE-AKE-${serverId}`);
84
+ const ikm = await hash(concat(oprfOutput, info));
85
+ const sessionKey = await hkdfDerive(ikm, "session-key", 32);
86
+ const exportKey = await hkdfDerive(ikm, "export-key", 32);
87
+ const finalization = await hkdfDerive(ikm, "finalization", 32);
79
88
  return { sessionKey, exportKey, finalization };
80
89
  }
81
90
 
@@ -100,12 +109,19 @@ async function loginFinish(password, serverResponse, state, serverId) {
100
109
  // src/client.ts
101
110
  var OpaqueClient = class {
102
111
  serverId;
112
+ /**
113
+ * Create a new OPAQUE client.
114
+ * @param config - Client configuration including server identity.
115
+ */
103
116
  constructor(config) {
104
117
  this.serverId = config.serverId;
105
118
  }
106
119
  /**
107
120
  * Start the registration process.
108
121
  * Generates a registration request to send to the server.
122
+ *
123
+ * @param password - User password to register.
124
+ * @returns Blinded request and client state for the finish step.
109
125
  */
110
126
  async registrationStart(password) {
111
127
  return registrationStart(password, this.serverId);
@@ -113,6 +129,11 @@ var OpaqueClient = class {
113
129
  /**
114
130
  * Finish the registration process.
115
131
  * Processes the server's registration response and produces the final record.
132
+ *
133
+ * @param password - User password (used for OPRF finalization).
134
+ * @param serverResponse - Server's evaluated OPRF element.
135
+ * @param state - Client blind from registrationStart.
136
+ * @returns Registration record to store on server and export key.
116
137
  */
117
138
  async registrationFinish(password, serverResponse, state) {
118
139
  return registrationFinish(password, serverResponse, state, this.serverId);
@@ -120,6 +141,9 @@ var OpaqueClient = class {
120
141
  /**
121
142
  * Start the login process.
122
143
  * Generates a credential request to send to the server.
144
+ *
145
+ * @param password - User password to authenticate with.
146
+ * @returns Credential request and client state for the finish step.
123
147
  */
124
148
  async loginStart(password) {
125
149
  return loginStart(password, this.serverId);
@@ -127,12 +151,17 @@ var OpaqueClient = class {
127
151
  /**
128
152
  * Finish the login process.
129
153
  * Processes the server's credential response and derives session keys.
154
+ *
155
+ * @param password - User password (used for OPRF finalization).
156
+ * @param serverResponse - Server's evaluated OPRF element.
157
+ * @param state - Client blind from loginStart.
158
+ * @returns Finalization message, session key, and export key.
130
159
  */
131
160
  async loginFinish(password, serverResponse, state) {
132
161
  return loginFinish(password, serverResponse, state, this.serverId);
133
162
  }
134
163
  };
135
164
 
136
- export { OpaqueClient };
137
- //# sourceMappingURL=index.mjs.map
138
- //# sourceMappingURL=index.mjs.map
165
+ exports.OpaqueClient = OpaqueClient;
166
+ //# sourceMappingURL=index.cjs.map
167
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crypto/utils.ts","../src/crypto/oprf.ts","../src/crypto/envelope.ts","../src/registration.ts","../src/crypto/ake.ts","../src/login.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["const encoder = new TextEncoder();\n\n/**\n * Encode a string to UTF-8 bytes.\n * @param input - The string to encode.\n * @returns UTF-8 encoded bytes.\n */\nexport function encode(input: string): Uint8Array {\n return encoder.encode(input);\n}\n\n/**\n * Generate cryptographically secure random bytes.\n * @param length - Number of random bytes to generate.\n * @returns Random bytes from `crypto.getRandomValues`.\n */\nexport function randomBytes(length: number): Uint8Array {\n return crypto.getRandomValues(new Uint8Array(length));\n}\n\n/**\n * SHA-256 hash.\n * @param data - Input bytes to hash.\n * @returns 32-byte SHA-256 digest.\n */\nexport async function hash(data: Uint8Array): Promise<Uint8Array> {\n // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)\n const digest = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(digest);\n}\n\n/**\n * Concatenate multiple Uint8Arrays.\n * @param arrays - Arrays to concatenate.\n * @returns Single Uint8Array containing all input bytes in order.\n */\nexport function concat(...arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * HKDF key derivation using WebCrypto.\n *\n * Uses WebCrypto's HKDF which performs both extract and expand internally.\n * The `ikm` parameter is input keying material (NOT a pre-extracted PRK).\n * A zero-filled salt is used for the extract step — this is placeholder\n * behavior and will be replaced with proper salt handling in the WASM\n * implementation.\n *\n * Note: This is NOT equivalent to RFC 5869 HKDF-Expand alone.\n *\n * @param ikm - Input keying material (raw, not pre-extracted).\n * @param info - Context string for domain separation.\n * @param length - Desired output length in bytes.\n * @returns Derived key material.\n */\nexport async function hkdfDerive(\n ikm: Uint8Array,\n info: string,\n length: number,\n): Promise<Uint8Array> {\n // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)\n const key = await crypto.subtle.importKey('raw', ikm, { name: 'HKDF' }, false, ['deriveBits']);\n\n const derived = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n // Placeholder: zero salt — will use proper salt in WASM implementation\n salt: new Uint8Array(32),\n // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)\n info: encode(info),\n },\n key,\n length * 8,\n );\n\n return new Uint8Array(derived);\n}\n","import { concat, encode, hash } from './utils';\n\nexport interface OprfBlindResult {\n blindedElement: Uint8Array;\n blind: Uint8Array;\n}\n\n/**\n * Blind a password for OPRF evaluation.\n *\n * TODO: Replace with proper ristretto255/Pallas OPRF when WASM is ready.\n * Current implementation is a placeholder using SHA-256 hash.\n *\n * @param password - User password to blind.\n * @returns Blinded element to send to server and blind scalar to preserve.\n */\nexport async function oprfBlind(password: string): Promise<OprfBlindResult> {\n const input = encode(password);\n const blind = crypto.getRandomValues(new Uint8Array(32));\n\n // Placeholder: hash(password || blind) as blinded element\n const combined = concat(input, blind);\n const blindedElement = await hash(combined);\n\n return { blindedElement, blind };\n}\n\n/**\n * Finalize OPRF by unblinding the server's response.\n *\n * TODO: Replace with proper OPRF finalization when WASM is ready.\n *\n * @param password - Original user password.\n * @param evaluatedElement - Server's evaluated OPRF element.\n * @param blind - Client blind scalar from oprfBlind.\n * @returns OPRF output bytes (placeholder: SHA-256 of combined inputs).\n */\nexport async function oprfFinalize(\n password: string,\n evaluatedElement: Uint8Array,\n blind: Uint8Array,\n): Promise<Uint8Array> {\n const input = encode(password);\n const combined = concat(input, evaluatedElement, blind);\n return hash(combined);\n}\n","import { concat, encode, hash, hkdfDerive } from './utils';\n\nexport interface EnvelopeResult {\n envelope: Uint8Array;\n exportKey: Uint8Array;\n}\n\n/**\n * Build a credential envelope from the OPRF output.\n *\n * TODO: Replace with proper envelope construction per RFC 9807 Section 4.\n * Current implementation is a placeholder.\n *\n * @param oprfOutput - OPRF output bytes.\n * @param serverId - Server identity string for domain separation.\n * @returns Envelope and export key.\n */\nexport async function buildEnvelope(\n oprfOutput: Uint8Array,\n serverId: string,\n): Promise<EnvelopeResult> {\n const info = encode(`OPAQUE-Envelope-${serverId}`);\n const ikm = await hash(concat(oprfOutput, info));\n\n const envelopeKey = await hkdfDerive(ikm, 'envelope-key', 32);\n const exportKey = await hkdfDerive(ikm, 'export-key', 32);\n\n // Placeholder: envelope is the derived key material (no encryption yet)\n const envelope = envelopeKey.slice();\n\n return { envelope, exportKey };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { buildEnvelope } from './crypto/envelope';\n\nexport interface RegistrationStartResult {\n /** The blinded message to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface RegistrationFinishResult {\n /** The registration record to send to the server for storage. */\n record: Uint8Array;\n /** The export key derived during registration. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE registration (client side).\n *\n * 1. Sample a blind scalar r.\n * 2. Compute blindedElement = r * Hash-to-Group(password).\n * 3. Return { request: blindedElement, state: r }.\n *\n * @param password - User password to register.\n * @param _serverId - Reserved for server identity binding in full implementation.\n * @returns Blinded request and client blind for the finish step.\n */\nexport async function registrationStart(\n password: string,\n // Reserved for real OPAQUE: serverId will bind the request to server identity\n _serverId: string,\n): Promise<RegistrationStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE registration (client side).\n *\n * 1. Finalize the OPRF output: unblind server response.\n * 2. Derive keys from the OPRF output.\n * 3. Build envelope containing encrypted credentials.\n * 4. Return registration record + export key.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from registrationStart.\n * @param serverId - Server identity for domain separation.\n * @returns Registration record (placeholder: envelope bytes) and export key.\n */\nexport async function registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<RegistrationFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { envelope, exportKey } = await buildEnvelope(oprfOutput, serverId);\n\n // Placeholder: record is the envelope (public key will be added in full implementation)\n const record = envelope.slice();\n\n return {\n record,\n exportKey,\n };\n}\n","import { concat, encode, hash, hkdfDerive } from './utils';\n\nexport interface AkeResult {\n sessionKey: Uint8Array;\n exportKey: Uint8Array;\n finalization: Uint8Array;\n}\n\n/**\n * Derive session and export keys from OPRF output.\n *\n * TODO: Replace with proper 3DH AKE when full implementation is ready.\n * Current implementation uses HKDF as placeholder.\n *\n * @param oprfOutput - OPRF output bytes.\n * @param serverId - Server identity string for domain separation.\n * @returns Session key, export key, and finalization message.\n */\nexport async function deriveKeys(oprfOutput: Uint8Array, serverId: string): Promise<AkeResult> {\n const info = encode(`OPAQUE-AKE-${serverId}`);\n const ikm = await hash(concat(oprfOutput, info));\n\n const sessionKey = await hkdfDerive(ikm, 'session-key', 32);\n const exportKey = await hkdfDerive(ikm, 'export-key', 32);\n const finalization = await hkdfDerive(ikm, 'finalization', 32);\n\n return { sessionKey, exportKey, finalization };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { deriveKeys } from './crypto/ake';\n\nexport interface LoginStartResult {\n /** The credential request to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface LoginFinishResult {\n /** The credential finalization message to send to the server. */\n finalization: Uint8Array;\n /** The session key for subsequent communication. */\n sessionKey: Uint8Array;\n /** The export key derived during login. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE login (client side).\n *\n * 1. Blind the password (same as registration start).\n * 2. Return credential request + client state for loginFinish.\n *\n * TODO: Add ephemeral key exchange material when AKE is implemented.\n *\n * @param password - User password to authenticate with.\n * @param _serverId - Reserved for server identity binding in full implementation.\n * @returns Blinded credential request and client state for the finish step.\n */\nexport async function loginStart(\n password: string,\n // Reserved for real OPAQUE: serverId will bind the request to server identity\n _serverId: string,\n): Promise<LoginStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE login (client side).\n *\n * 1. Unblind the server's OPRF response.\n * 2. Recover credentials from the envelope.\n * 3. Perform authenticated key exchange.\n * 4. Derive session key.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from loginStart.\n * @param serverId - Server identity for domain separation.\n * @returns Finalization message, session key, and export key.\n */\nexport async function loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<LoginFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { sessionKey, exportKey, finalization } = await deriveKeys(oprfOutput, serverId);\n\n return {\n finalization,\n sessionKey,\n exportKey,\n };\n}\n","import { registrationStart, registrationFinish } from './registration';\nimport type { RegistrationStartResult, RegistrationFinishResult } from './registration';\nimport { loginStart, loginFinish } from './login';\nimport type { LoginStartResult, LoginFinishResult } from './login';\n\nexport interface OpaqueClientConfig {\n /** Server identity (usually the domain, e.g. \"sid.example.com\") */\n serverId: string;\n}\n\n/**\n * OPAQUE (RFC 9807) client for browser and Node.js environments.\n *\n * Handles the client side of OPAQUE registration and login flows\n * using the WebCrypto API for core operations.\n */\nexport class OpaqueClient {\n private readonly serverId: string;\n\n /**\n * Create a new OPAQUE client.\n * @param config - Client configuration including server identity.\n */\n constructor(config: OpaqueClientConfig) {\n this.serverId = config.serverId;\n }\n\n /**\n * Start the registration process.\n * Generates a registration request to send to the server.\n *\n * @param password - User password to register.\n * @returns Blinded request and client state for the finish step.\n */\n async registrationStart(password: string): Promise<RegistrationStartResult> {\n return registrationStart(password, this.serverId);\n }\n\n /**\n * Finish the registration process.\n * Processes the server's registration response and produces the final record.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from registrationStart.\n * @returns Registration record to store on server and export key.\n */\n async registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<RegistrationFinishResult> {\n return registrationFinish(password, serverResponse, state, this.serverId);\n }\n\n /**\n * Start the login process.\n * Generates a credential request to send to the server.\n *\n * @param password - User password to authenticate with.\n * @returns Credential request and client state for the finish step.\n */\n async loginStart(password: string): Promise<LoginStartResult> {\n return loginStart(password, this.serverId);\n }\n\n /**\n * Finish the login process.\n * Processes the server's credential response and derives session keys.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from loginStart.\n * @returns Finalization message, session key, and export key.\n */\n async loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<LoginFinishResult> {\n return loginFinish(password, serverResponse, state, this.serverId);\n }\n}\n"]}
@@ -38,25 +38,45 @@ interface OpaqueClientConfig {
38
38
  */
39
39
  declare class OpaqueClient {
40
40
  private readonly serverId;
41
+ /**
42
+ * Create a new OPAQUE client.
43
+ * @param config - Client configuration including server identity.
44
+ */
41
45
  constructor(config: OpaqueClientConfig);
42
46
  /**
43
47
  * Start the registration process.
44
48
  * Generates a registration request to send to the server.
49
+ *
50
+ * @param password - User password to register.
51
+ * @returns Blinded request and client state for the finish step.
45
52
  */
46
53
  registrationStart(password: string): Promise<RegistrationStartResult>;
47
54
  /**
48
55
  * Finish the registration process.
49
56
  * Processes the server's registration response and produces the final record.
57
+ *
58
+ * @param password - User password (used for OPRF finalization).
59
+ * @param serverResponse - Server's evaluated OPRF element.
60
+ * @param state - Client blind from registrationStart.
61
+ * @returns Registration record to store on server and export key.
50
62
  */
51
63
  registrationFinish(password: string, serverResponse: Uint8Array, state: Uint8Array): Promise<RegistrationFinishResult>;
52
64
  /**
53
65
  * Start the login process.
54
66
  * Generates a credential request to send to the server.
67
+ *
68
+ * @param password - User password to authenticate with.
69
+ * @returns Credential request and client state for the finish step.
55
70
  */
56
71
  loginStart(password: string): Promise<LoginStartResult>;
57
72
  /**
58
73
  * Finish the login process.
59
74
  * Processes the server's credential response and derives session keys.
75
+ *
76
+ * @param password - User password (used for OPRF finalization).
77
+ * @param serverResponse - Server's evaluated OPRF element.
78
+ * @param state - Client blind from loginStart.
79
+ * @returns Finalization message, session key, and export key.
60
80
  */
61
81
  loginFinish(password: string, serverResponse: Uint8Array, state: Uint8Array): Promise<LoginFinishResult>;
62
82
  }
package/dist/index.d.ts CHANGED
@@ -38,25 +38,45 @@ interface OpaqueClientConfig {
38
38
  */
39
39
  declare class OpaqueClient {
40
40
  private readonly serverId;
41
+ /**
42
+ * Create a new OPAQUE client.
43
+ * @param config - Client configuration including server identity.
44
+ */
41
45
  constructor(config: OpaqueClientConfig);
42
46
  /**
43
47
  * Start the registration process.
44
48
  * Generates a registration request to send to the server.
49
+ *
50
+ * @param password - User password to register.
51
+ * @returns Blinded request and client state for the finish step.
45
52
  */
46
53
  registrationStart(password: string): Promise<RegistrationStartResult>;
47
54
  /**
48
55
  * Finish the registration process.
49
56
  * Processes the server's registration response and produces the final record.
57
+ *
58
+ * @param password - User password (used for OPRF finalization).
59
+ * @param serverResponse - Server's evaluated OPRF element.
60
+ * @param state - Client blind from registrationStart.
61
+ * @returns Registration record to store on server and export key.
50
62
  */
51
63
  registrationFinish(password: string, serverResponse: Uint8Array, state: Uint8Array): Promise<RegistrationFinishResult>;
52
64
  /**
53
65
  * Start the login process.
54
66
  * Generates a credential request to send to the server.
67
+ *
68
+ * @param password - User password to authenticate with.
69
+ * @returns Credential request and client state for the finish step.
55
70
  */
56
71
  loginStart(password: string): Promise<LoginStartResult>;
57
72
  /**
58
73
  * Finish the login process.
59
74
  * Processes the server's credential response and derives session keys.
75
+ *
76
+ * @param password - User password (used for OPRF finalization).
77
+ * @param serverResponse - Server's evaluated OPRF element.
78
+ * @param state - Client blind from loginStart.
79
+ * @returns Finalization message, session key, and export key.
60
80
  */
61
81
  loginFinish(password: string, serverResponse: Uint8Array, state: Uint8Array): Promise<LoginFinishResult>;
62
82
  }
package/dist/index.js CHANGED
@@ -1,26 +1,31 @@
1
- 'use strict';
2
-
3
1
  // src/crypto/utils.ts
4
2
  var encoder = new TextEncoder();
5
3
  function encode(input) {
6
4
  return encoder.encode(input);
7
5
  }
8
6
  async function hash(data) {
9
- return crypto.subtle.digest("SHA-256", data);
7
+ const digest = await crypto.subtle.digest("SHA-256", data);
8
+ return new Uint8Array(digest);
10
9
  }
11
- async function hkdfExpand(prk, info, length) {
12
- const key = await crypto.subtle.importKey(
13
- "raw",
14
- prk,
15
- { name: "HKDF" },
16
- false,
17
- ["deriveBits"]
18
- );
10
+ function concat(...arrays) {
11
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
12
+ const result = new Uint8Array(totalLength);
13
+ let offset = 0;
14
+ for (const arr of arrays) {
15
+ result.set(arr, offset);
16
+ offset += arr.length;
17
+ }
18
+ return result;
19
+ }
20
+ async function hkdfDerive(ikm, info, length) {
21
+ const key = await crypto.subtle.importKey("raw", ikm, { name: "HKDF" }, false, ["deriveBits"]);
19
22
  const derived = await crypto.subtle.deriveBits(
20
23
  {
21
24
  name: "HKDF",
22
25
  hash: "SHA-256",
26
+ // Placeholder: zero salt — will use proper salt in WASM implementation
23
27
  salt: new Uint8Array(32),
28
+ // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)
24
29
  info: encode(info)
25
30
  },
26
31
  key,
@@ -33,23 +38,23 @@ async function hkdfExpand(prk, info, length) {
33
38
  async function oprfBlind(password) {
34
39
  const input = encode(password);
35
40
  const blind = crypto.getRandomValues(new Uint8Array(32));
36
- const combined = new Uint8Array([...input, ...blind]);
37
- const blindedElement = new Uint8Array(await hash(combined));
41
+ const combined = concat(input, blind);
42
+ const blindedElement = await hash(combined);
38
43
  return { blindedElement, blind };
39
44
  }
40
45
  async function oprfFinalize(password, evaluatedElement, blind) {
41
46
  const input = encode(password);
42
- const combined = new Uint8Array([...input, ...evaluatedElement, ...blind]);
43
- return new Uint8Array(await hash(combined));
47
+ const combined = concat(input, evaluatedElement, blind);
48
+ return hash(combined);
44
49
  }
45
50
 
46
51
  // src/crypto/envelope.ts
47
52
  async function buildEnvelope(oprfOutput, serverId) {
48
- const info = new TextEncoder().encode(`OPAQUE-Envelope-${serverId}`);
49
- const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));
50
- const envelopeKey = await hkdfExpand(prk, "envelope-key", 32);
51
- const exportKey = await hkdfExpand(prk, "export-key", 32);
52
- const envelope = new Uint8Array([...envelopeKey]);
53
+ const info = encode(`OPAQUE-Envelope-${serverId}`);
54
+ const ikm = await hash(concat(oprfOutput, info));
55
+ const envelopeKey = await hkdfDerive(ikm, "envelope-key", 32);
56
+ const exportKey = await hkdfDerive(ikm, "export-key", 32);
57
+ const envelope = envelopeKey.slice();
53
58
  return { envelope, exportKey };
54
59
  }
55
60
 
@@ -64,7 +69,7 @@ async function registrationStart(password, _serverId) {
64
69
  async function registrationFinish(password, serverResponse, state, serverId) {
65
70
  const oprfOutput = await oprfFinalize(password, serverResponse, state);
66
71
  const { envelope, exportKey } = await buildEnvelope(oprfOutput, serverId);
67
- const record = new Uint8Array([...envelope]);
72
+ const record = envelope.slice();
68
73
  return {
69
74
  record,
70
75
  exportKey
@@ -73,11 +78,11 @@ async function registrationFinish(password, serverResponse, state, serverId) {
73
78
 
74
79
  // src/crypto/ake.ts
75
80
  async function deriveKeys(oprfOutput, serverId) {
76
- const info = new TextEncoder().encode(`OPAQUE-AKE-${serverId}`);
77
- const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));
78
- const sessionKey = await hkdfExpand(prk, "session-key", 32);
79
- const exportKey = await hkdfExpand(prk, "export-key", 32);
80
- const finalization = await hkdfExpand(prk, "finalization", 32);
81
+ const info = encode(`OPAQUE-AKE-${serverId}`);
82
+ const ikm = await hash(concat(oprfOutput, info));
83
+ const sessionKey = await hkdfDerive(ikm, "session-key", 32);
84
+ const exportKey = await hkdfDerive(ikm, "export-key", 32);
85
+ const finalization = await hkdfDerive(ikm, "finalization", 32);
81
86
  return { sessionKey, exportKey, finalization };
82
87
  }
83
88
 
@@ -102,12 +107,19 @@ async function loginFinish(password, serverResponse, state, serverId) {
102
107
  // src/client.ts
103
108
  var OpaqueClient = class {
104
109
  serverId;
110
+ /**
111
+ * Create a new OPAQUE client.
112
+ * @param config - Client configuration including server identity.
113
+ */
105
114
  constructor(config) {
106
115
  this.serverId = config.serverId;
107
116
  }
108
117
  /**
109
118
  * Start the registration process.
110
119
  * Generates a registration request to send to the server.
120
+ *
121
+ * @param password - User password to register.
122
+ * @returns Blinded request and client state for the finish step.
111
123
  */
112
124
  async registrationStart(password) {
113
125
  return registrationStart(password, this.serverId);
@@ -115,6 +127,11 @@ var OpaqueClient = class {
115
127
  /**
116
128
  * Finish the registration process.
117
129
  * Processes the server's registration response and produces the final record.
130
+ *
131
+ * @param password - User password (used for OPRF finalization).
132
+ * @param serverResponse - Server's evaluated OPRF element.
133
+ * @param state - Client blind from registrationStart.
134
+ * @returns Registration record to store on server and export key.
118
135
  */
119
136
  async registrationFinish(password, serverResponse, state) {
120
137
  return registrationFinish(password, serverResponse, state, this.serverId);
@@ -122,6 +139,9 @@ var OpaqueClient = class {
122
139
  /**
123
140
  * Start the login process.
124
141
  * Generates a credential request to send to the server.
142
+ *
143
+ * @param password - User password to authenticate with.
144
+ * @returns Credential request and client state for the finish step.
125
145
  */
126
146
  async loginStart(password) {
127
147
  return loginStart(password, this.serverId);
@@ -129,12 +149,17 @@ var OpaqueClient = class {
129
149
  /**
130
150
  * Finish the login process.
131
151
  * Processes the server's credential response and derives session keys.
152
+ *
153
+ * @param password - User password (used for OPRF finalization).
154
+ * @param serverResponse - Server's evaluated OPRF element.
155
+ * @param state - Client blind from loginStart.
156
+ * @returns Finalization message, session key, and export key.
132
157
  */
133
158
  async loginFinish(password, serverResponse, state) {
134
159
  return loginFinish(password, serverResponse, state, this.serverId);
135
160
  }
136
161
  };
137
162
 
138
- exports.OpaqueClient = OpaqueClient;
163
+ export { OpaqueClient };
139
164
  //# sourceMappingURL=index.js.map
140
165
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/crypto/utils.ts","../src/crypto/oprf.ts","../src/crypto/envelope.ts","../src/registration.ts","../src/crypto/ake.ts","../src/login.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["const encoder = new TextEncoder();\n\n/** Encode a string to UTF-8 bytes. */\nexport function encode(input: string): Uint8Array {\n return encoder.encode(input);\n}\n\n/** Generate cryptographically secure random bytes. */\nexport function randomBytes(length: number): Uint8Array {\n return crypto.getRandomValues(new Uint8Array(length));\n}\n\n/** SHA-256 hash. */\nexport async function hash(data: Uint8Array): Promise<ArrayBuffer> {\n return crypto.subtle.digest('SHA-256', data as unknown as BufferSource);\n}\n\n/** Concatenate multiple Uint8Arrays. */\nexport function concat(...arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * HKDF-Expand using WebCrypto.\n *\n * Derives key material from a pseudorandom key (PRK) with the given info string.\n */\nexport async function hkdfExpand(\n prk: Uint8Array,\n info: string,\n length: number,\n): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n prk as unknown as BufferSource,\n { name: 'HKDF' },\n false,\n ['deriveBits'],\n );\n\n const derived = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new Uint8Array(32) as unknown as BufferSource,\n info: encode(info) as unknown as BufferSource,\n },\n key,\n length * 8,\n );\n\n return new Uint8Array(derived);\n}\n","import { encode, hash } from './utils';\n\nexport interface OprfBlindResult {\n blindedElement: Uint8Array;\n blind: Uint8Array;\n}\n\n/**\n * Blind a password for OPRF evaluation.\n *\n * TODO: Replace with proper ristretto255/Pallas OPRF when WASM is ready.\n * Current implementation is a placeholder using HKDF.\n */\nexport async function oprfBlind(password: string): Promise<OprfBlindResult> {\n const input = encode(password);\n const blind = crypto.getRandomValues(new Uint8Array(32));\n\n // Placeholder: hash(password || blind) as blinded element\n const combined = new Uint8Array([...input, ...blind]);\n const blindedElement = new Uint8Array(await hash(combined));\n\n return { blindedElement, blind };\n}\n\n/**\n * Finalize OPRF by unblinding the server's response.\n *\n * TODO: Replace with proper OPRF finalization when WASM is ready.\n */\nexport async function oprfFinalize(\n password: string,\n evaluatedElement: Uint8Array,\n blind: Uint8Array,\n): Promise<Uint8Array> {\n const input = encode(password);\n const combined = new Uint8Array([...input, ...evaluatedElement, ...blind]);\n return new Uint8Array(await hash(combined));\n}\n","import { hash, hkdfExpand } from './utils';\n\nexport interface EnvelopeResult {\n envelope: Uint8Array;\n exportKey: Uint8Array;\n}\n\n/**\n * Build a credential envelope from the OPRF output.\n *\n * TODO: Replace with proper envelope construction per RFC 9807 Section 4.\n * Current implementation is a placeholder.\n */\nexport async function buildEnvelope(\n oprfOutput: Uint8Array,\n serverId: string,\n): Promise<EnvelopeResult> {\n const info = new TextEncoder().encode(`OPAQUE-Envelope-${serverId}`);\n const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));\n\n const envelopeKey = await hkdfExpand(prk, 'envelope-key', 32);\n const exportKey = await hkdfExpand(prk, 'export-key', 32);\n\n // Placeholder: envelope is just the encrypted key material\n const envelope = new Uint8Array([...envelopeKey]);\n\n return { envelope, exportKey };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { buildEnvelope } from './crypto/envelope';\nimport { randomBytes } from './crypto/utils';\n\nexport interface RegistrationStartResult {\n /** The blinded message to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface RegistrationFinishResult {\n /** The registration record to send to the server for storage. */\n record: Uint8Array;\n /** The export key derived during registration. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE registration (client side).\n *\n * 1. Sample a blind scalar r.\n * 2. Compute blindedElement = r * Hash-to-Group(password).\n * 3. Return { request: blindedElement, state: r }.\n */\nexport async function registrationStart(\n password: string,\n _serverId: string,\n): Promise<RegistrationStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE registration (client side).\n *\n * 1. Finalize the OPRF output: unblind server response.\n * 2. Derive keys from the OPRF output.\n * 3. Build envelope containing encrypted credentials.\n * 4. Return registration record + export key.\n */\nexport async function registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<RegistrationFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { envelope, exportKey } = await buildEnvelope(oprfOutput, serverId);\n\n // The record contains the envelope and the client's public key\n const record = new Uint8Array([...envelope]);\n\n return {\n record,\n exportKey,\n };\n}\n","import { hash, hkdfExpand } from './utils';\n\nexport interface AkeResult {\n sessionKey: Uint8Array;\n exportKey: Uint8Array;\n finalization: Uint8Array;\n}\n\n/**\n * Derive session and export keys from OPRF output.\n *\n * TODO: Replace with proper 3DH AKE when full implementation is ready.\n * Current implementation uses HKDF as placeholder.\n */\nexport async function deriveKeys(oprfOutput: Uint8Array, serverId: string): Promise<AkeResult> {\n const info = new TextEncoder().encode(`OPAQUE-AKE-${serverId}`);\n const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));\n\n const sessionKey = await hkdfExpand(prk, 'session-key', 32);\n const exportKey = await hkdfExpand(prk, 'export-key', 32);\n const finalization = await hkdfExpand(prk, 'finalization', 32);\n\n return { sessionKey, exportKey, finalization };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { deriveKeys } from './crypto/ake';\n\nexport interface LoginStartResult {\n /** The credential request to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface LoginFinishResult {\n /** The credential finalization message to send to the server. */\n finalization: Uint8Array;\n /** The session key for subsequent communication. */\n sessionKey: Uint8Array;\n /** The export key derived during login. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE login (client side).\n *\n * 1. Blind the password (same as registration start).\n * 2. Generate ephemeral key exchange material.\n * 3. Return credential request + client state.\n */\nexport async function loginStart(\n password: string,\n _serverId: string,\n): Promise<LoginStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE login (client side).\n *\n * 1. Unblind the server's OPRF response.\n * 2. Recover credentials from the envelope.\n * 3. Perform authenticated key exchange.\n * 4. Derive session key.\n */\nexport async function loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<LoginFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { sessionKey, exportKey, finalization } = await deriveKeys(oprfOutput, serverId);\n\n return {\n finalization,\n sessionKey,\n exportKey,\n };\n}\n","import { registrationStart, registrationFinish } from './registration';\nimport type { RegistrationStartResult, RegistrationFinishResult } from './registration';\nimport { loginStart, loginFinish } from './login';\nimport type { LoginStartResult, LoginFinishResult } from './login';\n\nexport interface OpaqueClientConfig {\n /** Server identity (usually the domain, e.g. \"sid.example.com\") */\n serverId: string;\n}\n\n/**\n * OPAQUE (RFC 9807) client for browser and Node.js environments.\n *\n * Handles the client side of OPAQUE registration and login flows\n * using the WebCrypto API for core operations.\n */\nexport class OpaqueClient {\n private readonly serverId: string;\n\n constructor(config: OpaqueClientConfig) {\n this.serverId = config.serverId;\n }\n\n /**\n * Start the registration process.\n * Generates a registration request to send to the server.\n */\n async registrationStart(password: string): Promise<RegistrationStartResult> {\n return registrationStart(password, this.serverId);\n }\n\n /**\n * Finish the registration process.\n * Processes the server's registration response and produces the final record.\n */\n async registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<RegistrationFinishResult> {\n return registrationFinish(password, serverResponse, state, this.serverId);\n }\n\n /**\n * Start the login process.\n * Generates a credential request to send to the server.\n */\n async loginStart(password: string): Promise<LoginStartResult> {\n return loginStart(password, this.serverId);\n }\n\n /**\n * Finish the login process.\n * Processes the server's credential response and derives session keys.\n */\n async loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<LoginFinishResult> {\n return loginFinish(password, serverResponse, state, this.serverId);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/crypto/utils.ts","../src/crypto/oprf.ts","../src/crypto/envelope.ts","../src/registration.ts","../src/crypto/ake.ts","../src/login.ts","../src/client.ts"],"names":[],"mappings":";AAAA,IAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAOzB,SAAS,OAAO,KAAA,EAA2B;AAChD,EAAA,OAAO,OAAA,CAAQ,OAAO,KAAK,CAAA;AAC7B;AAgBA,eAAsB,KAAK,IAAA,EAAuC;AAEhE,EAAA,MAAM,SAAS,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AACzD,EAAA,OAAO,IAAI,WAAW,MAAM,CAAA;AAC9B;AAOO,SAAS,UAAU,MAAA,EAAkC;AAC1D,EAAA,MAAM,WAAA,GAAc,OAAO,MAAA,CAAO,CAAC,KAAK,GAAA,KAAQ,GAAA,GAAM,GAAA,CAAI,MAAA,EAAQ,CAAC,CAAA;AACnE,EAAA,MAAM,MAAA,GAAS,IAAI,UAAA,CAAW,WAAW,CAAA;AACzC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,IAAA,MAAA,CAAO,GAAA,CAAI,KAAK,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,GAAA,CAAI,MAAA;AAAA,EAChB;AACA,EAAA,OAAO,MAAA;AACT;AAkBA,eAAsB,UAAA,CACpB,GAAA,EACA,IAAA,EACA,MAAA,EACqB;AAErB,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,CAAO,MAAA,CAAO,UAAU,KAAA,EAAO,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAO,EAAG,KAAA,EAAO,CAAC,YAAY,CAAC,CAAA;AAE7F,EAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,MAAA,CAAO,UAAA;AAAA,IAClC;AAAA,MACE,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,SAAA;AAAA;AAAA,MAEN,IAAA,EAAM,IAAI,UAAA,CAAW,EAAE,CAAA;AAAA;AAAA,MAEvB,IAAA,EAAM,OAAO,IAAI;AAAA,KACnB;AAAA,IACA,GAAA;AAAA,IACA,MAAA,GAAS;AAAA,GACX;AAEA,EAAA,OAAO,IAAI,WAAW,OAAO,CAAA;AAC/B;;;ACrEA,eAAsB,UAAU,QAAA,EAA4C;AAC1E,EAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,EAAA,MAAM,QAAQ,MAAA,CAAO,eAAA,CAAgB,IAAI,UAAA,CAAW,EAAE,CAAC,CAAA;AAGvD,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,KAAA,EAAO,KAAK,CAAA;AACpC,EAAA,MAAM,cAAA,GAAiB,MAAM,IAAA,CAAK,QAAQ,CAAA;AAE1C,EAAA,OAAO,EAAE,gBAAgB,KAAA,EAAM;AACjC;AAYA,eAAsB,YAAA,CACpB,QAAA,EACA,gBAAA,EACA,KAAA,EACqB;AACrB,EAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,KAAA,EAAO,gBAAA,EAAkB,KAAK,CAAA;AACtD,EAAA,OAAO,KAAK,QAAQ,CAAA;AACtB;;;AC5BA,eAAsB,aAAA,CACpB,YACA,QAAA,EACyB;AACzB,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAA,gBAAA,EAAmB,QAAQ,CAAA,CAAE,CAAA;AACjD,EAAA,MAAM,MAAM,MAAM,IAAA,CAAK,MAAA,CAAO,UAAA,EAAY,IAAI,CAAC,CAAA;AAE/C,EAAA,MAAM,WAAA,GAAc,MAAM,UAAA,CAAW,GAAA,EAAK,gBAAgB,EAAE,CAAA;AAC5D,EAAA,MAAM,SAAA,GAAY,MAAM,UAAA,CAAW,GAAA,EAAK,cAAc,EAAE,CAAA;AAGxD,EAAA,MAAM,QAAA,GAAW,YAAY,KAAA,EAAM;AAEnC,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;;;ACHA,eAAsB,iBAAA,CACpB,UAEA,SAAA,EACkC;AAClC,EAAA,MAAM,EAAE,cAAA,EAAgB,KAAA,EAAM,GAAI,MAAM,UAAU,QAAQ,CAAA;AAE1D,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,cAAA;AAAA,IACT,KAAA,EAAO;AAAA,GACT;AACF;AAgBA,eAAsB,kBAAA,CACpB,QAAA,EACA,cAAA,EACA,KAAA,EACA,QAAA,EACmC;AACnC,EAAA,MAAM,UAAA,GAAa,MAAM,YAAA,CAAa,QAAA,EAAU,gBAAgB,KAAK,CAAA;AACrE,EAAA,MAAM,EAAE,QAAA,EAAU,SAAA,KAAc,MAAM,aAAA,CAAc,YAAY,QAAQ,CAAA;AAGxE,EAAA,MAAM,MAAA,GAAS,SAAS,KAAA,EAAM;AAE9B,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA;AAAA,GACF;AACF;;;ACrDA,eAAsB,UAAA,CAAW,YAAwB,QAAA,EAAsC;AAC7F,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,CAAA,WAAA,EAAc,QAAQ,CAAA,CAAE,CAAA;AAC5C,EAAA,MAAM,MAAM,MAAM,IAAA,CAAK,MAAA,CAAO,UAAA,EAAY,IAAI,CAAC,CAAA;AAE/C,EAAA,MAAM,UAAA,GAAa,MAAM,UAAA,CAAW,GAAA,EAAK,eAAe,EAAE,CAAA;AAC1D,EAAA,MAAM,SAAA,GAAY,MAAM,UAAA,CAAW,GAAA,EAAK,cAAc,EAAE,CAAA;AACxD,EAAA,MAAM,YAAA,GAAe,MAAM,UAAA,CAAW,GAAA,EAAK,gBAAgB,EAAE,CAAA;AAE7D,EAAA,OAAO,EAAE,UAAA,EAAY,SAAA,EAAW,YAAA,EAAa;AAC/C;;;ACIA,eAAsB,UAAA,CACpB,UAEA,SAAA,EAC2B;AAC3B,EAAA,MAAM,EAAE,cAAA,EAAgB,KAAA,EAAM,GAAI,MAAM,UAAU,QAAQ,CAAA;AAE1D,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,cAAA;AAAA,IACT,KAAA,EAAO;AAAA,GACT;AACF;AAgBA,eAAsB,WAAA,CACpB,QAAA,EACA,cAAA,EACA,KAAA,EACA,QAAA,EAC4B;AAC5B,EAAA,MAAM,UAAA,GAAa,MAAM,YAAA,CAAa,QAAA,EAAU,gBAAgB,KAAK,CAAA;AACrE,EAAA,MAAM,EAAE,YAAY,SAAA,EAAW,YAAA,KAAiB,MAAM,UAAA,CAAW,YAAY,QAAQ,CAAA;AAErF,EAAA,OAAO;AAAA,IACL,YAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AACF;;;ACxDO,IAAM,eAAN,MAAmB;AAAA,EACP,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,YAAY,MAAA,EAA4B;AACtC,IAAA,IAAA,CAAK,WAAW,MAAA,CAAO,QAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,kBAAkB,QAAA,EAAoD;AAC1E,IAAA,OAAO,iBAAA,CAAkB,QAAA,EAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,kBAAA,CACJ,QAAA,EACA,cAAA,EACA,KAAA,EACmC;AACnC,IAAA,OAAO,kBAAA,CAAmB,QAAA,EAAU,cAAA,EAAgB,KAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,WAAW,QAAA,EAA6C;AAC5D,IAAA,OAAO,UAAA,CAAW,QAAA,EAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,WAAA,CACJ,QAAA,EACA,cAAA,EACA,KAAA,EAC4B;AAC5B,IAAA,OAAO,WAAA,CAAY,QAAA,EAAU,cAAA,EAAgB,KAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,EACnE;AACF","file":"index.js","sourcesContent":["const encoder = new TextEncoder();\n\n/**\n * Encode a string to UTF-8 bytes.\n * @param input - The string to encode.\n * @returns UTF-8 encoded bytes.\n */\nexport function encode(input: string): Uint8Array {\n return encoder.encode(input);\n}\n\n/**\n * Generate cryptographically secure random bytes.\n * @param length - Number of random bytes to generate.\n * @returns Random bytes from `crypto.getRandomValues`.\n */\nexport function randomBytes(length: number): Uint8Array {\n return crypto.getRandomValues(new Uint8Array(length));\n}\n\n/**\n * SHA-256 hash.\n * @param data - Input bytes to hash.\n * @returns 32-byte SHA-256 digest.\n */\nexport async function hash(data: Uint8Array): Promise<Uint8Array> {\n // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)\n const digest = await crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(digest);\n}\n\n/**\n * Concatenate multiple Uint8Arrays.\n * @param arrays - Arrays to concatenate.\n * @returns Single Uint8Array containing all input bytes in order.\n */\nexport function concat(...arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * HKDF key derivation using WebCrypto.\n *\n * Uses WebCrypto's HKDF which performs both extract and expand internally.\n * The `ikm` parameter is input keying material (NOT a pre-extracted PRK).\n * A zero-filled salt is used for the extract step — this is placeholder\n * behavior and will be replaced with proper salt handling in the WASM\n * implementation.\n *\n * Note: This is NOT equivalent to RFC 5869 HKDF-Expand alone.\n *\n * @param ikm - Input keying material (raw, not pre-extracted).\n * @param info - Context string for domain separation.\n * @param length - Desired output length in bytes.\n * @returns Derived key material.\n */\nexport async function hkdfDerive(\n ikm: Uint8Array,\n info: string,\n length: number,\n): Promise<Uint8Array> {\n // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)\n const key = await crypto.subtle.importKey('raw', ikm, { name: 'HKDF' }, false, ['deriveBits']);\n\n const derived = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n // Placeholder: zero salt — will use proper salt in WASM implementation\n salt: new Uint8Array(32),\n // @ts-expect-error TS 5.7+ Uint8Array<ArrayBufferLike> vs BufferSource mismatch (microsoft/TypeScript#59451)\n info: encode(info),\n },\n key,\n length * 8,\n );\n\n return new Uint8Array(derived);\n}\n","import { concat, encode, hash } from './utils';\n\nexport interface OprfBlindResult {\n blindedElement: Uint8Array;\n blind: Uint8Array;\n}\n\n/**\n * Blind a password for OPRF evaluation.\n *\n * TODO: Replace with proper ristretto255/Pallas OPRF when WASM is ready.\n * Current implementation is a placeholder using SHA-256 hash.\n *\n * @param password - User password to blind.\n * @returns Blinded element to send to server and blind scalar to preserve.\n */\nexport async function oprfBlind(password: string): Promise<OprfBlindResult> {\n const input = encode(password);\n const blind = crypto.getRandomValues(new Uint8Array(32));\n\n // Placeholder: hash(password || blind) as blinded element\n const combined = concat(input, blind);\n const blindedElement = await hash(combined);\n\n return { blindedElement, blind };\n}\n\n/**\n * Finalize OPRF by unblinding the server's response.\n *\n * TODO: Replace with proper OPRF finalization when WASM is ready.\n *\n * @param password - Original user password.\n * @param evaluatedElement - Server's evaluated OPRF element.\n * @param blind - Client blind scalar from oprfBlind.\n * @returns OPRF output bytes (placeholder: SHA-256 of combined inputs).\n */\nexport async function oprfFinalize(\n password: string,\n evaluatedElement: Uint8Array,\n blind: Uint8Array,\n): Promise<Uint8Array> {\n const input = encode(password);\n const combined = concat(input, evaluatedElement, blind);\n return hash(combined);\n}\n","import { concat, encode, hash, hkdfDerive } from './utils';\n\nexport interface EnvelopeResult {\n envelope: Uint8Array;\n exportKey: Uint8Array;\n}\n\n/**\n * Build a credential envelope from the OPRF output.\n *\n * TODO: Replace with proper envelope construction per RFC 9807 Section 4.\n * Current implementation is a placeholder.\n *\n * @param oprfOutput - OPRF output bytes.\n * @param serverId - Server identity string for domain separation.\n * @returns Envelope and export key.\n */\nexport async function buildEnvelope(\n oprfOutput: Uint8Array,\n serverId: string,\n): Promise<EnvelopeResult> {\n const info = encode(`OPAQUE-Envelope-${serverId}`);\n const ikm = await hash(concat(oprfOutput, info));\n\n const envelopeKey = await hkdfDerive(ikm, 'envelope-key', 32);\n const exportKey = await hkdfDerive(ikm, 'export-key', 32);\n\n // Placeholder: envelope is the derived key material (no encryption yet)\n const envelope = envelopeKey.slice();\n\n return { envelope, exportKey };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { buildEnvelope } from './crypto/envelope';\n\nexport interface RegistrationStartResult {\n /** The blinded message to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface RegistrationFinishResult {\n /** The registration record to send to the server for storage. */\n record: Uint8Array;\n /** The export key derived during registration. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE registration (client side).\n *\n * 1. Sample a blind scalar r.\n * 2. Compute blindedElement = r * Hash-to-Group(password).\n * 3. Return { request: blindedElement, state: r }.\n *\n * @param password - User password to register.\n * @param _serverId - Reserved for server identity binding in full implementation.\n * @returns Blinded request and client blind for the finish step.\n */\nexport async function registrationStart(\n password: string,\n // Reserved for real OPAQUE: serverId will bind the request to server identity\n _serverId: string,\n): Promise<RegistrationStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE registration (client side).\n *\n * 1. Finalize the OPRF output: unblind server response.\n * 2. Derive keys from the OPRF output.\n * 3. Build envelope containing encrypted credentials.\n * 4. Return registration record + export key.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from registrationStart.\n * @param serverId - Server identity for domain separation.\n * @returns Registration record (placeholder: envelope bytes) and export key.\n */\nexport async function registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<RegistrationFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { envelope, exportKey } = await buildEnvelope(oprfOutput, serverId);\n\n // Placeholder: record is the envelope (public key will be added in full implementation)\n const record = envelope.slice();\n\n return {\n record,\n exportKey,\n };\n}\n","import { concat, encode, hash, hkdfDerive } from './utils';\n\nexport interface AkeResult {\n sessionKey: Uint8Array;\n exportKey: Uint8Array;\n finalization: Uint8Array;\n}\n\n/**\n * Derive session and export keys from OPRF output.\n *\n * TODO: Replace with proper 3DH AKE when full implementation is ready.\n * Current implementation uses HKDF as placeholder.\n *\n * @param oprfOutput - OPRF output bytes.\n * @param serverId - Server identity string for domain separation.\n * @returns Session key, export key, and finalization message.\n */\nexport async function deriveKeys(oprfOutput: Uint8Array, serverId: string): Promise<AkeResult> {\n const info = encode(`OPAQUE-AKE-${serverId}`);\n const ikm = await hash(concat(oprfOutput, info));\n\n const sessionKey = await hkdfDerive(ikm, 'session-key', 32);\n const exportKey = await hkdfDerive(ikm, 'export-key', 32);\n const finalization = await hkdfDerive(ikm, 'finalization', 32);\n\n return { sessionKey, exportKey, finalization };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { deriveKeys } from './crypto/ake';\n\nexport interface LoginStartResult {\n /** The credential request to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface LoginFinishResult {\n /** The credential finalization message to send to the server. */\n finalization: Uint8Array;\n /** The session key for subsequent communication. */\n sessionKey: Uint8Array;\n /** The export key derived during login. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE login (client side).\n *\n * 1. Blind the password (same as registration start).\n * 2. Return credential request + client state for loginFinish.\n *\n * TODO: Add ephemeral key exchange material when AKE is implemented.\n *\n * @param password - User password to authenticate with.\n * @param _serverId - Reserved for server identity binding in full implementation.\n * @returns Blinded credential request and client state for the finish step.\n */\nexport async function loginStart(\n password: string,\n // Reserved for real OPAQUE: serverId will bind the request to server identity\n _serverId: string,\n): Promise<LoginStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE login (client side).\n *\n * 1. Unblind the server's OPRF response.\n * 2. Recover credentials from the envelope.\n * 3. Perform authenticated key exchange.\n * 4. Derive session key.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from loginStart.\n * @param serverId - Server identity for domain separation.\n * @returns Finalization message, session key, and export key.\n */\nexport async function loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<LoginFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { sessionKey, exportKey, finalization } = await deriveKeys(oprfOutput, serverId);\n\n return {\n finalization,\n sessionKey,\n exportKey,\n };\n}\n","import { registrationStart, registrationFinish } from './registration';\nimport type { RegistrationStartResult, RegistrationFinishResult } from './registration';\nimport { loginStart, loginFinish } from './login';\nimport type { LoginStartResult, LoginFinishResult } from './login';\n\nexport interface OpaqueClientConfig {\n /** Server identity (usually the domain, e.g. \"sid.example.com\") */\n serverId: string;\n}\n\n/**\n * OPAQUE (RFC 9807) client for browser and Node.js environments.\n *\n * Handles the client side of OPAQUE registration and login flows\n * using the WebCrypto API for core operations.\n */\nexport class OpaqueClient {\n private readonly serverId: string;\n\n /**\n * Create a new OPAQUE client.\n * @param config - Client configuration including server identity.\n */\n constructor(config: OpaqueClientConfig) {\n this.serverId = config.serverId;\n }\n\n /**\n * Start the registration process.\n * Generates a registration request to send to the server.\n *\n * @param password - User password to register.\n * @returns Blinded request and client state for the finish step.\n */\n async registrationStart(password: string): Promise<RegistrationStartResult> {\n return registrationStart(password, this.serverId);\n }\n\n /**\n * Finish the registration process.\n * Processes the server's registration response and produces the final record.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from registrationStart.\n * @returns Registration record to store on server and export key.\n */\n async registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<RegistrationFinishResult> {\n return registrationFinish(password, serverResponse, state, this.serverId);\n }\n\n /**\n * Start the login process.\n * Generates a credential request to send to the server.\n *\n * @param password - User password to authenticate with.\n * @returns Credential request and client state for the finish step.\n */\n async loginStart(password: string): Promise<LoginStartResult> {\n return loginStart(password, this.serverId);\n }\n\n /**\n * Finish the login process.\n * Processes the server's credential response and derives session keys.\n *\n * @param password - User password (used for OPRF finalization).\n * @param serverResponse - Server's evaluated OPRF element.\n * @param state - Client blind from loginStart.\n * @returns Finalization message, session key, and export key.\n */\n async loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<LoginFinishResult> {\n return loginFinish(password, serverResponse, state, this.serverId);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@structured-id/opaque",
3
- "version": "0.0.0-development",
3
+ "version": "1.0.0",
4
+ "type": "module",
4
5
  "description": "OPAQUE (RFC 9807) client library with WebCrypto and WASM",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
11
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
13
14
  }
14
15
  },
15
16
  "files": [
@@ -51,18 +52,25 @@
51
52
  "node": ">=20"
52
53
  },
53
54
  "devDependencies": {
54
- "@semantic-release/changelog": "^6.0.0",
55
- "@semantic-release/git": "^10.0.0",
56
- "@types/node": "^20.0.0",
57
- "@vitest/coverage-v8": "^2.0.0",
58
- "eslint": "^9.0.0",
59
- "prettier": "^3.0.0",
60
- "semantic-release": "^24.0.0",
61
- "tsup": "^8.0.0",
62
- "typescript": "^5.5.0",
63
- "vitest": "^2.0.0"
55
+ "@commitlint/config-conventional": "^20.4.1",
56
+ "@eslint/js": "^10.0.1",
57
+ "@semantic-release/changelog": "^6.0.3",
58
+ "@semantic-release/git": "^10.0.1",
59
+ "@semantic-release/github": "^12.0.5",
60
+ "@semantic-release/npm": "^13.1.4",
61
+ "@types/node": "^25.2.2",
62
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
63
+ "@typescript-eslint/parser": "^8.55.0",
64
+ "@vitest/coverage-v8": "^4.0.18",
65
+ "conventional-changelog-conventionalcommits": "^9.1.0",
66
+ "eslint": "^10.0.0",
67
+ "prettier": "^3.8.1",
68
+ "semantic-release": "^25.0.3",
69
+ "tsup": "^8.5.1",
70
+ "typescript": "^5.9.3",
71
+ "vitest": "^4.0.18"
64
72
  },
65
- "packageManager": "yarn@4.0.0",
73
+ "packageManager": "yarn@4.12.0",
66
74
  "publishConfig": {
67
75
  "access": "public"
68
76
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/crypto/utils.ts","../src/crypto/oprf.ts","../src/crypto/envelope.ts","../src/registration.ts","../src/crypto/ake.ts","../src/login.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["const encoder = new TextEncoder();\n\n/** Encode a string to UTF-8 bytes. */\nexport function encode(input: string): Uint8Array {\n return encoder.encode(input);\n}\n\n/** Generate cryptographically secure random bytes. */\nexport function randomBytes(length: number): Uint8Array {\n return crypto.getRandomValues(new Uint8Array(length));\n}\n\n/** SHA-256 hash. */\nexport async function hash(data: Uint8Array): Promise<ArrayBuffer> {\n return crypto.subtle.digest('SHA-256', data as unknown as BufferSource);\n}\n\n/** Concatenate multiple Uint8Arrays. */\nexport function concat(...arrays: Uint8Array[]): Uint8Array {\n const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const arr of arrays) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n}\n\n/**\n * HKDF-Expand using WebCrypto.\n *\n * Derives key material from a pseudorandom key (PRK) with the given info string.\n */\nexport async function hkdfExpand(\n prk: Uint8Array,\n info: string,\n length: number,\n): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n prk as unknown as BufferSource,\n { name: 'HKDF' },\n false,\n ['deriveBits'],\n );\n\n const derived = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new Uint8Array(32) as unknown as BufferSource,\n info: encode(info) as unknown as BufferSource,\n },\n key,\n length * 8,\n );\n\n return new Uint8Array(derived);\n}\n","import { encode, hash } from './utils';\n\nexport interface OprfBlindResult {\n blindedElement: Uint8Array;\n blind: Uint8Array;\n}\n\n/**\n * Blind a password for OPRF evaluation.\n *\n * TODO: Replace with proper ristretto255/Pallas OPRF when WASM is ready.\n * Current implementation is a placeholder using HKDF.\n */\nexport async function oprfBlind(password: string): Promise<OprfBlindResult> {\n const input = encode(password);\n const blind = crypto.getRandomValues(new Uint8Array(32));\n\n // Placeholder: hash(password || blind) as blinded element\n const combined = new Uint8Array([...input, ...blind]);\n const blindedElement = new Uint8Array(await hash(combined));\n\n return { blindedElement, blind };\n}\n\n/**\n * Finalize OPRF by unblinding the server's response.\n *\n * TODO: Replace with proper OPRF finalization when WASM is ready.\n */\nexport async function oprfFinalize(\n password: string,\n evaluatedElement: Uint8Array,\n blind: Uint8Array,\n): Promise<Uint8Array> {\n const input = encode(password);\n const combined = new Uint8Array([...input, ...evaluatedElement, ...blind]);\n return new Uint8Array(await hash(combined));\n}\n","import { hash, hkdfExpand } from './utils';\n\nexport interface EnvelopeResult {\n envelope: Uint8Array;\n exportKey: Uint8Array;\n}\n\n/**\n * Build a credential envelope from the OPRF output.\n *\n * TODO: Replace with proper envelope construction per RFC 9807 Section 4.\n * Current implementation is a placeholder.\n */\nexport async function buildEnvelope(\n oprfOutput: Uint8Array,\n serverId: string,\n): Promise<EnvelopeResult> {\n const info = new TextEncoder().encode(`OPAQUE-Envelope-${serverId}`);\n const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));\n\n const envelopeKey = await hkdfExpand(prk, 'envelope-key', 32);\n const exportKey = await hkdfExpand(prk, 'export-key', 32);\n\n // Placeholder: envelope is just the encrypted key material\n const envelope = new Uint8Array([...envelopeKey]);\n\n return { envelope, exportKey };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { buildEnvelope } from './crypto/envelope';\nimport { randomBytes } from './crypto/utils';\n\nexport interface RegistrationStartResult {\n /** The blinded message to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface RegistrationFinishResult {\n /** The registration record to send to the server for storage. */\n record: Uint8Array;\n /** The export key derived during registration. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE registration (client side).\n *\n * 1. Sample a blind scalar r.\n * 2. Compute blindedElement = r * Hash-to-Group(password).\n * 3. Return { request: blindedElement, state: r }.\n */\nexport async function registrationStart(\n password: string,\n _serverId: string,\n): Promise<RegistrationStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE registration (client side).\n *\n * 1. Finalize the OPRF output: unblind server response.\n * 2. Derive keys from the OPRF output.\n * 3. Build envelope containing encrypted credentials.\n * 4. Return registration record + export key.\n */\nexport async function registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<RegistrationFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { envelope, exportKey } = await buildEnvelope(oprfOutput, serverId);\n\n // The record contains the envelope and the client's public key\n const record = new Uint8Array([...envelope]);\n\n return {\n record,\n exportKey,\n };\n}\n","import { hash, hkdfExpand } from './utils';\n\nexport interface AkeResult {\n sessionKey: Uint8Array;\n exportKey: Uint8Array;\n finalization: Uint8Array;\n}\n\n/**\n * Derive session and export keys from OPRF output.\n *\n * TODO: Replace with proper 3DH AKE when full implementation is ready.\n * Current implementation uses HKDF as placeholder.\n */\nexport async function deriveKeys(oprfOutput: Uint8Array, serverId: string): Promise<AkeResult> {\n const info = new TextEncoder().encode(`OPAQUE-AKE-${serverId}`);\n const prk = new Uint8Array(await hash(new Uint8Array([...oprfOutput, ...info])));\n\n const sessionKey = await hkdfExpand(prk, 'session-key', 32);\n const exportKey = await hkdfExpand(prk, 'export-key', 32);\n const finalization = await hkdfExpand(prk, 'finalization', 32);\n\n return { sessionKey, exportKey, finalization };\n}\n","import { oprfBlind, oprfFinalize } from './crypto/oprf';\nimport { deriveKeys } from './crypto/ake';\n\nexport interface LoginStartResult {\n /** The credential request to send to the server. */\n request: Uint8Array;\n /** Client state to preserve for the finish step. */\n state: Uint8Array;\n}\n\nexport interface LoginFinishResult {\n /** The credential finalization message to send to the server. */\n finalization: Uint8Array;\n /** The session key for subsequent communication. */\n sessionKey: Uint8Array;\n /** The export key derived during login. */\n exportKey: Uint8Array;\n}\n\n/**\n * Start OPAQUE login (client side).\n *\n * 1. Blind the password (same as registration start).\n * 2. Generate ephemeral key exchange material.\n * 3. Return credential request + client state.\n */\nexport async function loginStart(\n password: string,\n _serverId: string,\n): Promise<LoginStartResult> {\n const { blindedElement, blind } = await oprfBlind(password);\n\n return {\n request: blindedElement,\n state: blind,\n };\n}\n\n/**\n * Finish OPAQUE login (client side).\n *\n * 1. Unblind the server's OPRF response.\n * 2. Recover credentials from the envelope.\n * 3. Perform authenticated key exchange.\n * 4. Derive session key.\n */\nexport async function loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n serverId: string,\n): Promise<LoginFinishResult> {\n const oprfOutput = await oprfFinalize(password, serverResponse, state);\n const { sessionKey, exportKey, finalization } = await deriveKeys(oprfOutput, serverId);\n\n return {\n finalization,\n sessionKey,\n exportKey,\n };\n}\n","import { registrationStart, registrationFinish } from './registration';\nimport type { RegistrationStartResult, RegistrationFinishResult } from './registration';\nimport { loginStart, loginFinish } from './login';\nimport type { LoginStartResult, LoginFinishResult } from './login';\n\nexport interface OpaqueClientConfig {\n /** Server identity (usually the domain, e.g. \"sid.example.com\") */\n serverId: string;\n}\n\n/**\n * OPAQUE (RFC 9807) client for browser and Node.js environments.\n *\n * Handles the client side of OPAQUE registration and login flows\n * using the WebCrypto API for core operations.\n */\nexport class OpaqueClient {\n private readonly serverId: string;\n\n constructor(config: OpaqueClientConfig) {\n this.serverId = config.serverId;\n }\n\n /**\n * Start the registration process.\n * Generates a registration request to send to the server.\n */\n async registrationStart(password: string): Promise<RegistrationStartResult> {\n return registrationStart(password, this.serverId);\n }\n\n /**\n * Finish the registration process.\n * Processes the server's registration response and produces the final record.\n */\n async registrationFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<RegistrationFinishResult> {\n return registrationFinish(password, serverResponse, state, this.serverId);\n }\n\n /**\n * Start the login process.\n * Generates a credential request to send to the server.\n */\n async loginStart(password: string): Promise<LoginStartResult> {\n return loginStart(password, this.serverId);\n }\n\n /**\n * Finish the login process.\n * Processes the server's credential response and derives session keys.\n */\n async loginFinish(\n password: string,\n serverResponse: Uint8Array,\n state: Uint8Array,\n ): Promise<LoginFinishResult> {\n return loginFinish(password, serverResponse, state, this.serverId);\n }\n}\n"]}