@f0rest8/metamorphic-crypto 0.3.2
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 +113 -0
- package/metamorphic_crypto.d.ts +178 -0
- package/metamorphic_crypto.js +1094 -0
- package/metamorphic_crypto_bg.wasm +0 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# metamorphic-crypto
|
|
2
|
+
|
|
3
|
+
Zero-knowledge end-to-end encryption library with post-quantum hybrid KEM.
|
|
4
|
+
|
|
5
|
+
Built for [Metamorphic](https://metamorphic.app) and [Mosslet](https://mosslet.app) — privacy-first apps by [Moss Piglet Corporation](https://mosspiglet.dev) where all user data is encrypted client-side and the server only stores opaque ciphertext.
|
|
6
|
+
|
|
7
|
+
## What this provides
|
|
8
|
+
|
|
9
|
+
- **Secretbox** (XSalsa20-Poly1305) — symmetric authenticated encryption
|
|
10
|
+
- **Sealed box** (X25519) — anonymous public-key encryption (libsodium-compatible)
|
|
11
|
+
- **Hybrid PQ KEM** (ML-KEM-768 + X25519) — NIST Cat-3 post-quantum key encapsulation (default)
|
|
12
|
+
- **Hybrid PQ KEM** (ML-KEM-1024 + X25519) — NIST Cat-5 post-quantum key encapsulation (opt-in)
|
|
13
|
+
- **Argon2id KDF** — password-based key derivation (libsodium INTERACTIVE parameters)
|
|
14
|
+
- **WASM bindings** — browser-ready via `wasm-pack`
|
|
15
|
+
- **Recovery keys** — human-readable base32 encoding for key backup
|
|
16
|
+
|
|
17
|
+
## Security levels
|
|
18
|
+
|
|
19
|
+
| Level | ML-KEM | NIST Category | Equivalent | Version Tag | Default |
|
|
20
|
+
|-------|--------|---------------|------------|-------------|---------|
|
|
21
|
+
| Cat-3 | 768 | 3 | ~AES-192 | `0x02` | Yes |
|
|
22
|
+
| Cat-5 | 1024 | 5 | ~AES-256 | `0x03` | No |
|
|
23
|
+
|
|
24
|
+
Both levels use the same combiner construction and X25519 classical fallback. `hybrid_open` auto-detects the level from the version tag byte — old and new ciphertext coexist seamlessly.
|
|
25
|
+
|
|
26
|
+
## Security properties
|
|
27
|
+
|
|
28
|
+
- `#![forbid(unsafe_code)]` — no unsafe anywhere in the crate
|
|
29
|
+
- All secret key material zeroized after use
|
|
30
|
+
- Constant-time MAC comparison via RustCrypto
|
|
31
|
+
- OS CSPRNG via `getrandom` (no userspace PRNG)
|
|
32
|
+
- Hybrid construction: both ML-KEM AND X25519 must be broken to compromise a sealed key
|
|
33
|
+
|
|
34
|
+
## Hybrid KEM construction
|
|
35
|
+
|
|
36
|
+
The hybrid combiner matches the format used by [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum)'s `ml_kem768_x25519`:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Seed expansion: SHAKE256(seed_32) → 96 bytes [ML-KEM seed (64) || X25519 sk (32)]
|
|
40
|
+
Combiner: SHA3-256(ss_mlkem || ss_x25519 || ct_x25519 || pk_x25519 || label)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Cat-3 (ML-KEM-768, default)
|
|
44
|
+
```
|
|
45
|
+
Public key: ML-KEM-768 ek (1184 B) || X25519 pk (32 B) = 1216 bytes
|
|
46
|
+
Ciphertext: 0x02 || ML-KEM-768 ct (1088 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Cat-5 (ML-KEM-1024, opt-in)
|
|
50
|
+
```
|
|
51
|
+
Public key: ML-KEM-1024 ek (1568 B) || X25519 pk (32 B) = 1600 bytes
|
|
52
|
+
Ciphertext: 0x03 || ML-KEM-1024 ct (1568 B) || X25519 eph pk (32 B) || nonce (24 B) || secretbox ct
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Targets
|
|
56
|
+
|
|
57
|
+
| Target | Build | Use case |
|
|
58
|
+
|--------|-------|----------|
|
|
59
|
+
| Native | `cargo build` | Tests, CLI tools, Elixir NIF (`metamorphic_crypto` Hex package) |
|
|
60
|
+
| WASM | `wasm-pack build --target web` | Browser (Phoenix LiveView, any SPA) |
|
|
61
|
+
| iOS | UniFFI (planned) | Native Swift apps |
|
|
62
|
+
| Android | UniFFI (planned) | Native Kotlin apps |
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```rust
|
|
67
|
+
use metamorphic_crypto::{generate_key, encrypt_secretbox_string, decrypt_secretbox_to_string};
|
|
68
|
+
use metamorphic_crypto::{generate_hybrid_keypair, hybrid_seal, hybrid_open};
|
|
69
|
+
use metamorphic_crypto::{generate_hybrid_keypair_1024, hybrid_seal_1024};
|
|
70
|
+
|
|
71
|
+
// Symmetric encryption
|
|
72
|
+
let key = generate_key();
|
|
73
|
+
let ciphertext = encrypt_secretbox_string("sensitive data", &key).unwrap();
|
|
74
|
+
let plaintext = decrypt_secretbox_to_string(&ciphertext, &key).unwrap();
|
|
75
|
+
assert_eq!(plaintext, "sensitive data");
|
|
76
|
+
|
|
77
|
+
// Hybrid PQ seal (Cat-3, default)
|
|
78
|
+
let kp = generate_hybrid_keypair();
|
|
79
|
+
let sealed = hybrid_seal(b"context_key_bytes", &kp.public_key).unwrap();
|
|
80
|
+
let opened = hybrid_open(&sealed, &kp.secret_key).unwrap();
|
|
81
|
+
|
|
82
|
+
// Hybrid PQ seal (Cat-5)
|
|
83
|
+
let kp5 = generate_hybrid_keypair_1024();
|
|
84
|
+
let sealed5 = hybrid_seal_1024(b"context_key_bytes", &kp5.public_key).unwrap();
|
|
85
|
+
let opened5 = hybrid_open(&sealed5, &kp5.secret_key).unwrap(); // auto-detects level
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## WASM (browser)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
wasm-pack build --target web --release
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import init, { deriveSessionKey, encryptSecretboxString } from './pkg/metamorphic_crypto.js';
|
|
96
|
+
|
|
97
|
+
await init('/path/to/metamorphic_crypto_bg.wasm');
|
|
98
|
+
|
|
99
|
+
const key = deriveSessionKey(password, saltBase64);
|
|
100
|
+
const ciphertext = encryptSecretboxString("hello", key);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Tests
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cargo test # unit + integration + cross-level compatibility
|
|
107
|
+
cargo clippy # zero warnings
|
|
108
|
+
cargo fmt --check # formatted
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at your option.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Seal plaintext (base64) to a recipient's public key. Returns base64 ciphertext.
|
|
6
|
+
*/
|
|
7
|
+
export function boxSeal(plaintext_b64: string, public_key_b64: string): string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open a sealed box. Returns base64-encoded plaintext.
|
|
11
|
+
*/
|
|
12
|
+
export function boxSealOpen(ciphertext_b64: string, public_key_b64: string, private_key_b64: string): string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Decrypt an encrypted private key with a session key. Returns base64 private key.
|
|
16
|
+
*/
|
|
17
|
+
export function decryptPrivateKey(ciphertext_b64: string, session_key_b64: string): string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decrypt private key from recovery backup. Returns base64 private key.
|
|
21
|
+
*/
|
|
22
|
+
export function decryptPrivateKeyWithRecovery(ciphertext_b64: string, recovery_secret_b64: string): string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decrypt base64 ciphertext, returning plaintext as base64.
|
|
26
|
+
*/
|
|
27
|
+
export function decryptSecretbox(ciphertext_b64: string, key_b64: string): string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decrypt base64 ciphertext to a UTF-8 string.
|
|
31
|
+
*/
|
|
32
|
+
export function decryptSecretboxToString(ciphertext_b64: string, key_b64: string): string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Derive a 32-byte session key from password + base64-encoded salt.
|
|
36
|
+
* Returns base64-encoded key.
|
|
37
|
+
*/
|
|
38
|
+
export function deriveSessionKey(password: string, salt_b64: string): string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Encrypt a base64 private key with a session key. Returns base64 ciphertext.
|
|
42
|
+
*/
|
|
43
|
+
export function encryptPrivateKey(private_key_b64: string, session_key_b64: string): string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Encrypt private key for recovery backup. Returns base64 ciphertext.
|
|
47
|
+
*/
|
|
48
|
+
export function encryptPrivateKeyForRecovery(private_key_b64: string, recovery_secret_b64: string): string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Encrypt raw bytes (as base64) with a base64 key. Returns base64 ciphertext.
|
|
52
|
+
*/
|
|
53
|
+
export function encryptSecretbox(plaintext_b64: string, key_b64: string): string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Encrypt a UTF-8 string with a base64 key. Returns base64 ciphertext.
|
|
57
|
+
*/
|
|
58
|
+
export function encryptSecretboxString(plaintext: string, key_b64: string): string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate a ML-KEM-768 keypair. Returns JSON: `{ publicKey, secretKey }`.
|
|
62
|
+
*/
|
|
63
|
+
export function generateHybridKeyPair(): any;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a ML-KEM-1024 + X25519 keypair (Cat-5). Returns JSON: `{ publicKey, secretKey }`.
|
|
67
|
+
*/
|
|
68
|
+
export function generateHybridKeyPair1024(): any;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a random 32-byte symmetric key (base64).
|
|
72
|
+
*/
|
|
73
|
+
export function generateKey(): string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate a random X25519 keypair. Returns JSON: `{ publicKey, privateKey }`.
|
|
77
|
+
*/
|
|
78
|
+
export function generateKeyPair(): any;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate a recovery key. Returns JSON: `{ recoveryKey, recoverySecretBase64 }`.
|
|
82
|
+
*/
|
|
83
|
+
export function generateRecoveryKey(): any;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a random 16-byte salt (base64).
|
|
87
|
+
*/
|
|
88
|
+
export function generateSalt(): string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a base64 ciphertext is hybrid (v2/v3) format.
|
|
92
|
+
*/
|
|
93
|
+
export function isHybridCiphertext(ciphertext_b64: string): boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse the salt (base64) from a key_hash string (`salt$argon2id`).
|
|
97
|
+
*/
|
|
98
|
+
export function parseSaltFromKeyHash(key_hash: string): string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Derive the 32-byte secret (base64) from a human-readable recovery key.
|
|
102
|
+
*/
|
|
103
|
+
export function recoveryKeyToSecret(recovery_key: string): string;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Seal plaintext bytes (base64) to a user's key(s). Uses hybrid PQ if pq_pk is provided.
|
|
107
|
+
*/
|
|
108
|
+
export function sealForUser(plaintext_b64: string, public_key_b64: string, pq_public_key_b64?: string | null): string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Seal plaintext bytes (base64) to a user's key(s) at a specific security level.
|
|
112
|
+
*
|
|
113
|
+
* `level` must be `"cat3"` (ML-KEM-768, default) or `"cat5"` (ML-KEM-1024).
|
|
114
|
+
* If `pq_public_key_b64` is absent or empty, falls back to legacy X25519.
|
|
115
|
+
*/
|
|
116
|
+
export function sealForUserWithLevel(plaintext_b64: string, public_key_b64: string, pq_public_key_b64: string | null | undefined, level: string): string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Unseal ciphertext using the user's keys. Auto-detects format.
|
|
120
|
+
* Returns base64-encoded plaintext.
|
|
121
|
+
*/
|
|
122
|
+
export function unsealFromUser(ciphertext_b64: string, public_key_b64: string, private_key_b64: string, pq_secret_key_b64?: string | null): string;
|
|
123
|
+
|
|
124
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
125
|
+
|
|
126
|
+
export interface InitOutput {
|
|
127
|
+
readonly memory: WebAssembly.Memory;
|
|
128
|
+
readonly deriveSessionKey: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
129
|
+
readonly encryptSecretbox: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
130
|
+
readonly decryptSecretbox: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
131
|
+
readonly boxSeal: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
132
|
+
readonly boxSealOpen: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
|
|
133
|
+
readonly sealForUser: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
|
|
134
|
+
readonly unsealFromUser: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
|
|
135
|
+
readonly sealForUserWithLevel: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
|
|
136
|
+
readonly generateHybridKeyPair: () => number;
|
|
137
|
+
readonly generateHybridKeyPair1024: () => number;
|
|
138
|
+
readonly isHybridCiphertext: (a: number, b: number) => number;
|
|
139
|
+
readonly generateKey: (a: number) => void;
|
|
140
|
+
readonly generateKeyPair: () => number;
|
|
141
|
+
readonly generateSalt: (a: number) => void;
|
|
142
|
+
readonly encryptPrivateKey: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
143
|
+
readonly decryptPrivateKey: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
144
|
+
readonly generateRecoveryKey: (a: number) => void;
|
|
145
|
+
readonly recoveryKeyToSecret: (a: number, b: number, c: number) => void;
|
|
146
|
+
readonly parseSaltFromKeyHash: (a: number, b: number, c: number) => void;
|
|
147
|
+
readonly encryptSecretboxString: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
148
|
+
readonly decryptSecretboxToString: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
149
|
+
readonly encryptPrivateKeyForRecovery: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
150
|
+
readonly decryptPrivateKeyWithRecovery: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
151
|
+
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
152
|
+
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
153
|
+
readonly __wbindgen_export3: (a: number) => void;
|
|
154
|
+
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
155
|
+
readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
162
|
+
* a precompiled `WebAssembly.Module`.
|
|
163
|
+
*
|
|
164
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
165
|
+
*
|
|
166
|
+
* @returns {InitOutput}
|
|
167
|
+
*/
|
|
168
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
172
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
175
|
+
*
|
|
176
|
+
* @returns {Promise<InitOutput>}
|
|
177
|
+
*/
|
|
178
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|