@f0rest8/metamorphic-crypto 0.4.0 → 0.5.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 CHANGED
@@ -176,6 +176,38 @@ Stable wire format (reproduce exactly to match native/Elixir):
176
176
  > right construction instead: this package's Argon2id `deriveSessionKey` for
177
177
  > password-based derivation, or a dedicated KDF/MAC.
178
178
 
179
+ ### Hybrid PQ signatures (ML-DSA + Ed25519)
180
+
181
+ Composite digital signatures: every message is signed by **both** ML-DSA
182
+ (FIPS 204) and Ed25519 (RFC 8032), and `verify` returns `true` only if **both**
183
+ component signatures are valid (strict AND). Keys and signatures are base64; the
184
+ message is **base64** and `context` is a UTF-8 string.
185
+
186
+ ```js
187
+ import { generateSigningKeyPair, sign, verify, deriveSigningPublicKey } from "@f0rest8/metamorphic-crypto";
188
+
189
+ const kp = generateSigningKeyPair("cat3"); // { publicKey, secretKey } — Cat-3 default
190
+ const msg = btoa("transparency log entry");
191
+ const sig = sign(msg, "metamorphic/sign/v1", kp.secretKey);
192
+ const ok = verify(msg, "metamorphic/sign/v1", sig, kp.publicKey); // true
193
+
194
+ // Re-derive the public key from a backed-up secret key:
195
+ const pk = deriveSigningPublicKey(kp.secretKey); // === kp.publicKey
196
+ ```
197
+
198
+ Levels are `"cat2"` (ML-DSA-44), `"cat3"` (ML-DSA-65, default), and `"cat5"`
199
+ (ML-DSA-87); `verify` auto-detects the level from the signature. ML-DSA uses the
200
+ hedged/randomized FIPS 204 mode, so signature **bytes are non-reproducible**,
201
+ but key derivation and the domain-separation framing
202
+ (`u64_be(len(context)) || context || message`) are deterministic and identical
203
+ across native Rust, WASM, and the Elixir NIF.
204
+
205
+ > **Audit posture.** `ed25519-dalek` (2.x) is mature and widely deployed.
206
+ > `ml-dsa` (0.1.x, RustCrypto FIPS 204) is new and **not yet independently
207
+ > audited** — it is included as defense-in-depth on top of Ed25519, so the
208
+ > composite remains at least as strong as Ed25519 even if a flaw is found in the
209
+ > young post-quantum implementation. Pinned and tracked for the FIPS-mode roadmap.
210
+
179
211
  ## Security levels
180
212
 
181
213
  | Level | ML-KEM | NIST Category | Equivalent | Default |
@@ -37,6 +37,11 @@ export function decryptSecretboxToString(ciphertext_b64: string, key_b64: string
37
37
  */
38
38
  export function deriveSessionKey(password: string, salt_b64: string): string;
39
39
 
40
+ /**
41
+ * Re-derive the base64 public key from a base64 hybrid secret key.
42
+ */
43
+ export function deriveSigningPublicKey(secret_key_b64: string): string;
44
+
40
45
  /**
41
46
  * Encrypt a base64 private key with a session key. Returns base64 ciphertext.
42
47
  */
@@ -87,6 +92,14 @@ export function generateRecoveryKey(): any;
87
92
  */
88
93
  export function generateSalt(): string;
89
94
 
95
+ /**
96
+ * Generate a hybrid signing keypair. Returns JSON: `{ publicKey, secretKey }`.
97
+ *
98
+ * `level` is `"cat2"` (ML-DSA-44), `"cat3"` (ML-DSA-65, default), or `"cat5"`
99
+ * (ML-DSA-87). Empty/null defaults to Cat-3.
100
+ */
101
+ export function generateSigningKeyPair(level: string): any;
102
+
90
103
  /**
91
104
  * Check if a base64 ciphertext is hybrid (v2/v3) format.
92
105
  */
@@ -148,12 +161,25 @@ export function sha3_512WithContext(context: string, data_b64: string): string;
148
161
  */
149
162
  export function sha512(data_b64: string): string;
150
163
 
164
+ /**
165
+ * Sign base64 `message` under `context` with a base64 hybrid `secret_key`.
166
+ * Returns the composite signature as base64.
167
+ */
168
+ export function sign(message_b64: string, context: string, secret_key_b64: string): string;
169
+
151
170
  /**
152
171
  * Unseal ciphertext using the user's keys. Auto-detects format.
153
172
  * Returns base64-encoded plaintext.
154
173
  */
155
174
  export function unsealFromUser(ciphertext_b64: string, public_key_b64: string, private_key_b64: string, pq_secret_key_b64?: string | null): string;
156
175
 
176
+ /**
177
+ * Verify a base64 composite `signature` over base64 `message`/`context`
178
+ * against a base64 `public_key`. Returns `true` only if **both** the Ed25519
179
+ * and ML-DSA components verify (strict AND).
180
+ */
181
+ export function verify(message_b64: string, context: string, signature_b64: string, public_key_b64: string): boolean;
182
+
157
183
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
158
184
 
159
185
  export interface InitOutput {
@@ -182,10 +208,14 @@ export interface InitOutput {
182
208
  readonly sha3_256: (a: number, b: number, c: number) => void;
183
209
  readonly sha256: (a: number, b: number, c: number) => void;
184
210
  readonly sha512: (a: number, b: number, c: number) => void;
211
+ readonly generateSigningKeyPair: (a: number, b: number, c: number) => void;
212
+ readonly deriveSigningPublicKey: (a: number, b: number, c: number) => void;
213
+ readonly sign: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
214
+ readonly verify: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
185
215
  readonly encryptSecretboxString: (a: number, b: number, c: number, d: number, e: number) => void;
186
216
  readonly encryptPrivateKeyForRecovery: (a: number, b: number, c: number, d: number, e: number) => void;
187
- readonly decryptPrivateKeyWithRecovery: (a: number, b: number, c: number, d: number, e: number) => void;
188
217
  readonly decryptSecretboxToString: (a: number, b: number, c: number, d: number, e: number) => void;
218
+ readonly decryptPrivateKeyWithRecovery: (a: number, b: number, c: number, d: number, e: number) => void;
189
219
  readonly __wbindgen_export: (a: number, b: number) => number;
190
220
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
191
221
  readonly __wbindgen_export3: (a: number) => void;
@@ -249,6 +249,38 @@ export function deriveSessionKey(password, salt_b64) {
249
249
  }
250
250
  }
251
251
 
252
+ /**
253
+ * Re-derive the base64 public key from a base64 hybrid secret key.
254
+ * @param {string} secret_key_b64
255
+ * @returns {string}
256
+ */
257
+ export function deriveSigningPublicKey(secret_key_b64) {
258
+ let deferred3_0;
259
+ let deferred3_1;
260
+ try {
261
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
262
+ const ptr0 = passStringToWasm0(secret_key_b64, wasm.__wbindgen_export, wasm.__wbindgen_export2);
263
+ const len0 = WASM_VECTOR_LEN;
264
+ wasm.deriveSigningPublicKey(retptr, ptr0, len0);
265
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
266
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
267
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
268
+ var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
269
+ var ptr2 = r0;
270
+ var len2 = r1;
271
+ if (r3) {
272
+ ptr2 = 0; len2 = 0;
273
+ throw takeObject(r2);
274
+ }
275
+ deferred3_0 = ptr2;
276
+ deferred3_1 = len2;
277
+ return getStringFromWasm0(ptr2, len2);
278
+ } finally {
279
+ wasm.__wbindgen_add_to_stack_pointer(16);
280
+ wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1);
281
+ }
282
+ }
283
+
252
284
  /**
253
285
  * Encrypt a base64 private key with a session key. Returns base64 ciphertext.
254
286
  * @param {string} private_key_b64
@@ -478,6 +510,32 @@ export function generateSalt() {
478
510
  }
479
511
  }
480
512
 
513
+ /**
514
+ * Generate a hybrid signing keypair. Returns JSON: `{ publicKey, secretKey }`.
515
+ *
516
+ * `level` is `"cat2"` (ML-DSA-44), `"cat3"` (ML-DSA-65, default), or `"cat5"`
517
+ * (ML-DSA-87). Empty/null defaults to Cat-3.
518
+ * @param {string} level
519
+ * @returns {any}
520
+ */
521
+ export function generateSigningKeyPair(level) {
522
+ try {
523
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
524
+ const ptr0 = passStringToWasm0(level, wasm.__wbindgen_export, wasm.__wbindgen_export2);
525
+ const len0 = WASM_VECTOR_LEN;
526
+ wasm.generateSigningKeyPair(retptr, ptr0, len0);
527
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
528
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
529
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
530
+ if (r2) {
531
+ throw takeObject(r1);
532
+ }
533
+ return takeObject(r0);
534
+ } finally {
535
+ wasm.__wbindgen_add_to_stack_pointer(16);
536
+ }
537
+ }
538
+
481
539
  /**
482
540
  * Check if a base64 ciphertext is hybrid (v2/v3) format.
483
541
  * @param {string} ciphertext_b64
@@ -807,6 +865,45 @@ export function sha512(data_b64) {
807
865
  }
808
866
  }
809
867
 
868
+ /**
869
+ * Sign base64 `message` under `context` with a base64 hybrid `secret_key`.
870
+ * Returns the composite signature as base64.
871
+ * @param {string} message_b64
872
+ * @param {string} context
873
+ * @param {string} secret_key_b64
874
+ * @returns {string}
875
+ */
876
+ export function sign(message_b64, context, secret_key_b64) {
877
+ let deferred5_0;
878
+ let deferred5_1;
879
+ try {
880
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
881
+ const ptr0 = passStringToWasm0(message_b64, wasm.__wbindgen_export, wasm.__wbindgen_export2);
882
+ const len0 = WASM_VECTOR_LEN;
883
+ const ptr1 = passStringToWasm0(context, wasm.__wbindgen_export, wasm.__wbindgen_export2);
884
+ const len1 = WASM_VECTOR_LEN;
885
+ const ptr2 = passStringToWasm0(secret_key_b64, wasm.__wbindgen_export, wasm.__wbindgen_export2);
886
+ const len2 = WASM_VECTOR_LEN;
887
+ wasm.sign(retptr, ptr0, len0, ptr1, len1, ptr2, len2);
888
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
889
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
890
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
891
+ var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
892
+ var ptr4 = r0;
893
+ var len4 = r1;
894
+ if (r3) {
895
+ ptr4 = 0; len4 = 0;
896
+ throw takeObject(r2);
897
+ }
898
+ deferred5_0 = ptr4;
899
+ deferred5_1 = len4;
900
+ return getStringFromWasm0(ptr4, len4);
901
+ } finally {
902
+ wasm.__wbindgen_add_to_stack_pointer(16);
903
+ wasm.__wbindgen_export4(deferred5_0, deferred5_1, 1);
904
+ }
905
+ }
906
+
810
907
  /**
811
908
  * Unseal ciphertext using the user's keys. Auto-detects format.
812
909
  * Returns base64-encoded plaintext.
@@ -848,6 +945,40 @@ export function unsealFromUser(ciphertext_b64, public_key_b64, private_key_b64,
848
945
  wasm.__wbindgen_export4(deferred6_0, deferred6_1, 1);
849
946
  }
850
947
  }
948
+
949
+ /**
950
+ * Verify a base64 composite `signature` over base64 `message`/`context`
951
+ * against a base64 `public_key`. Returns `true` only if **both** the Ed25519
952
+ * and ML-DSA components verify (strict AND).
953
+ * @param {string} message_b64
954
+ * @param {string} context
955
+ * @param {string} signature_b64
956
+ * @param {string} public_key_b64
957
+ * @returns {boolean}
958
+ */
959
+ export function verify(message_b64, context, signature_b64, public_key_b64) {
960
+ try {
961
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
962
+ const ptr0 = passStringToWasm0(message_b64, wasm.__wbindgen_export, wasm.__wbindgen_export2);
963
+ const len0 = WASM_VECTOR_LEN;
964
+ const ptr1 = passStringToWasm0(context, wasm.__wbindgen_export, wasm.__wbindgen_export2);
965
+ const len1 = WASM_VECTOR_LEN;
966
+ const ptr2 = passStringToWasm0(signature_b64, wasm.__wbindgen_export, wasm.__wbindgen_export2);
967
+ const len2 = WASM_VECTOR_LEN;
968
+ const ptr3 = passStringToWasm0(public_key_b64, wasm.__wbindgen_export, wasm.__wbindgen_export2);
969
+ const len3 = WASM_VECTOR_LEN;
970
+ wasm.verify(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
971
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
972
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
973
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
974
+ if (r2) {
975
+ throw takeObject(r1);
976
+ }
977
+ return r0 !== 0;
978
+ } finally {
979
+ wasm.__wbindgen_add_to_stack_pointer(16);
980
+ }
981
+ }
851
982
  function __wbg_get_imports() {
852
983
  const import0 = {
853
984
  __proto__: null,
Binary file
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@f0rest8/metamorphic-crypto",
3
3
  "type": "module",
4
4
  "description": "Zero-knowledge end-to-end encryption with post-quantum hybrid KEM (ML-KEM-768/1024 + X25519)",
5
- "version": "0.4.0",
5
+ "version": "0.5.0",
6
6
  "license": "MIT OR Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",