@abraca/dabra 1.3.1 → 1.3.3
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/dist/abracadabra-provider.cjs +2384 -14
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +2377 -15
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +120 -10
- package/package.json +2 -1
- package/src/AbracadabraProvider.ts +4 -0
- package/src/CryptoIdentityKeystore.ts +263 -15
- package/src/MnemonicKeyDerivation.ts +167 -0
- package/src/index.ts +10 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MnemonicKeyDerivation
|
|
3
|
+
*
|
|
4
|
+
* Pure, stateless functions for BIP-39 mnemonic-based Ed25519 identity
|
|
5
|
+
* derivation and AES-256-GCM seed wrapping. No IndexedDB, no WebAuthn,
|
|
6
|
+
* no DOM access — fully unit-testable.
|
|
7
|
+
*
|
|
8
|
+
* Derivation chain:
|
|
9
|
+
* BIP-39 Mnemonic (+optional passphrase)
|
|
10
|
+
* → PBKDF2-HMAC-SHA512 (2048 rounds, per BIP-39 spec)
|
|
11
|
+
* → first 32 bytes as IKM
|
|
12
|
+
* → HKDF-SHA256(salt="abracadabra-mnemonic-v1", info="abracadabra-identity-v1")
|
|
13
|
+
* → Ed25519 seed (32 bytes)
|
|
14
|
+
* → Ed25519 keypair + X25519 (Montgomery conversion)
|
|
15
|
+
*
|
|
16
|
+
* The HKDF salt differs from the passkey PRF path (which uses PRF_SALT),
|
|
17
|
+
* providing domain separation: identical raw input bytes can never produce
|
|
18
|
+
* the same Ed25519 seed across the two derivation methods.
|
|
19
|
+
*
|
|
20
|
+
* Dependencies: @scure/bip39, @noble/ed25519, @noble/hashes, @noble/curves
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as ed from "@noble/ed25519";
|
|
24
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
25
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
26
|
+
import { ed25519 as nobleEd25519Curves } from "@noble/curves/ed25519.js";
|
|
27
|
+
import { generateMnemonic as _generateMnemonic, validateMnemonic as _validateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
|
|
28
|
+
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
29
|
+
|
|
30
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** HKDF salt for mnemonic → Ed25519 seed derivation. */
|
|
33
|
+
const MNEMONIC_HKDF_SALT = /* @__PURE__ */ new TextEncoder().encode("abracadabra-mnemonic-v1");
|
|
34
|
+
|
|
35
|
+
/** HKDF info string — intentionally matches the passkey path's HKDF_INFO. */
|
|
36
|
+
const MNEMONIC_HKDF_INFO = /* @__PURE__ */ new TextEncoder().encode("abracadabra-identity-v1");
|
|
37
|
+
|
|
38
|
+
/** HKDF info string for deriving the AES-GCM seed-wrapping key from PRF output. */
|
|
39
|
+
export const SEED_WRAP_INFO = /* @__PURE__ */ new TextEncoder().encode("abracadabra-seed-wrap-v1");
|
|
40
|
+
|
|
41
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function toBase64url(bytes: Uint8Array): string {
|
|
44
|
+
return btoa(String.fromCharCode(...bytes))
|
|
45
|
+
.replace(/\+/g, "-")
|
|
46
|
+
.replace(/\//g, "_")
|
|
47
|
+
.replace(/=/g, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Mnemonic generation & validation ────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a new BIP-39 mnemonic phrase.
|
|
54
|
+
* @param strength 128 for 12 words (default), 256 for 24 words.
|
|
55
|
+
*/
|
|
56
|
+
export function generateMnemonic(strength: 128 | 256 = 128): string {
|
|
57
|
+
return _generateMnemonic(wordlist, strength);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a BIP-39 mnemonic (wordlist + checksum).
|
|
62
|
+
*/
|
|
63
|
+
export function validateMnemonic(mnemonic: string): boolean {
|
|
64
|
+
return _validateMnemonic(mnemonic, wordlist);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Re-export the English wordlist for UI auto-complete.
|
|
69
|
+
*/
|
|
70
|
+
export { wordlist as bip39Wordlist };
|
|
71
|
+
|
|
72
|
+
// ── Key derivation ──────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Derive a 32-byte Ed25519 seed from a BIP-39 mnemonic.
|
|
76
|
+
*
|
|
77
|
+
* Chain: mnemonic → PBKDF2-HMAC-SHA512 (2048 rounds) → first 32 bytes →
|
|
78
|
+
* HKDF-SHA256(salt, info) → 32-byte seed.
|
|
79
|
+
*
|
|
80
|
+
* @param mnemonic Valid BIP-39 mnemonic (12 or 24 words).
|
|
81
|
+
* @param passphrase Optional BIP-39 passphrase ("25th word").
|
|
82
|
+
* @returns 32-byte Ed25519 seed. Caller MUST wipe after use: `seed.fill(0)`.
|
|
83
|
+
*/
|
|
84
|
+
export function mnemonicToEd25519Seed(mnemonic: string, passphrase?: string): Uint8Array {
|
|
85
|
+
// BIP-39: PBKDF2-HMAC-SHA512(mnemonic, "mnemonic" + passphrase, 2048) → 64 bytes
|
|
86
|
+
const bip39Seed = mnemonicToSeedSync(mnemonic, passphrase ?? "");
|
|
87
|
+
// Take first 32 bytes as input key material
|
|
88
|
+
const ikm = bip39Seed.subarray(0, 32);
|
|
89
|
+
// HKDF-SHA256 with our domain-specific salt
|
|
90
|
+
const seed = hkdf(sha256, ikm, MNEMONIC_HKDF_SALT, MNEMONIC_HKDF_INFO, 32);
|
|
91
|
+
// Wipe intermediate material
|
|
92
|
+
bip39Seed.fill(0);
|
|
93
|
+
return seed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Derive the full Ed25519 + X25519 keypair from a BIP-39 mnemonic.
|
|
98
|
+
*
|
|
99
|
+
* @param mnemonic Valid BIP-39 mnemonic.
|
|
100
|
+
* @param passphrase Optional BIP-39 passphrase.
|
|
101
|
+
* @returns Keys and seed. Caller MUST wipe `seed` after use.
|
|
102
|
+
*/
|
|
103
|
+
export async function mnemonicToKeyPair(
|
|
104
|
+
mnemonic: string,
|
|
105
|
+
passphrase?: string,
|
|
106
|
+
): Promise<{
|
|
107
|
+
seed: Uint8Array;
|
|
108
|
+
publicKey: Uint8Array;
|
|
109
|
+
publicKeyB64: string;
|
|
110
|
+
x25519PublicKey: Uint8Array;
|
|
111
|
+
x25519PublicKeyB64: string;
|
|
112
|
+
}> {
|
|
113
|
+
const seed = mnemonicToEd25519Seed(mnemonic, passphrase);
|
|
114
|
+
const publicKey = await ed.getPublicKeyAsync(seed);
|
|
115
|
+
const publicKeyB64 = toBase64url(publicKey);
|
|
116
|
+
const x25519PublicKey = nobleEd25519Curves.utils.toMontgomery(publicKey);
|
|
117
|
+
const x25519PublicKeyB64 = toBase64url(x25519PublicKey);
|
|
118
|
+
return { seed, publicKey, publicKeyB64, x25519PublicKey, x25519PublicKeyB64 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Seed wrapping (AES-256-GCM via WebCrypto) ──────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Derive a 32-byte AES-256-GCM key from WebAuthn PRF output for seed wrapping.
|
|
125
|
+
*
|
|
126
|
+
* @param prfOutput Raw PRF output from WebAuthn assertion (32 bytes typical).
|
|
127
|
+
* @param wrapSalt Random 32-byte salt, stored alongside the ciphertext.
|
|
128
|
+
*/
|
|
129
|
+
export function deriveSeedWrappingKey(prfOutput: ArrayBuffer, wrapSalt: Uint8Array): Uint8Array {
|
|
130
|
+
return hkdf(sha256, new Uint8Array(prfOutput), wrapSalt, SEED_WRAP_INFO, 32);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Encrypt an Ed25519 seed with AES-256-GCM.
|
|
135
|
+
*
|
|
136
|
+
* @param seed 32-byte Ed25519 seed to protect.
|
|
137
|
+
* @param wrappingKeyBytes 32-byte AES key from `deriveSeedWrappingKey`.
|
|
138
|
+
* @returns Ciphertext (48 bytes: 32 plaintext + 16 auth tag) and 12-byte IV.
|
|
139
|
+
*/
|
|
140
|
+
export async function wrapSeed(
|
|
141
|
+
seed: Uint8Array,
|
|
142
|
+
wrappingKeyBytes: Uint8Array,
|
|
143
|
+
): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> {
|
|
144
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
145
|
+
const key = await crypto.subtle.importKey("raw", wrappingKeyBytes, "AES-GCM", false, ["encrypt"]);
|
|
146
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, seed);
|
|
147
|
+
return { ciphertext, iv };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Decrypt an Ed25519 seed from AES-256-GCM ciphertext.
|
|
152
|
+
*
|
|
153
|
+
* @param ciphertext Encrypted seed (48 bytes).
|
|
154
|
+
* @param iv 12-byte GCM nonce.
|
|
155
|
+
* @param wrappingKeyBytes 32-byte AES key from `deriveSeedWrappingKey`.
|
|
156
|
+
* @returns 32-byte Ed25519 seed. Caller MUST wipe after use.
|
|
157
|
+
* @throws If the auth tag is invalid (wrong key or tampered data).
|
|
158
|
+
*/
|
|
159
|
+
export async function unwrapSeed(
|
|
160
|
+
ciphertext: ArrayBuffer,
|
|
161
|
+
iv: Uint8Array,
|
|
162
|
+
wrappingKeyBytes: Uint8Array,
|
|
163
|
+
): Promise<Uint8Array> {
|
|
164
|
+
const key = await crypto.subtle.importKey("raw", wrappingKeyBytes, "AES-GCM", false, ["decrypt"]);
|
|
165
|
+
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
166
|
+
return new Uint8Array(plaintext);
|
|
167
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -36,3 +36,13 @@ export type {
|
|
|
36
36
|
DeviceTier,
|
|
37
37
|
} from "./IdentityDoc.ts";
|
|
38
38
|
export { DeviceRegistrationService } from "./DeviceRegistrationService.ts";
|
|
39
|
+
export {
|
|
40
|
+
generateMnemonic,
|
|
41
|
+
validateMnemonic,
|
|
42
|
+
mnemonicToEd25519Seed,
|
|
43
|
+
mnemonicToKeyPair,
|
|
44
|
+
deriveSeedWrappingKey,
|
|
45
|
+
wrapSeed,
|
|
46
|
+
unwrapSeed,
|
|
47
|
+
bip39Wordlist,
|
|
48
|
+
} from "./MnemonicKeyDerivation.ts";
|