Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/curvefallback.js

6.2 KiB, 1 run

created by r2519314175:1355, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* ============================================================
2 Daimond — pure-JS Ed25519 / X25519 fallback (curvefallback.js)
3 ------------------------------------------------------------
4 A last resort for unlock on a browser whose WebCrypto does not
5 implement Ed25519 or X25519 — old Android Chrome, and Firefox
6 before ~129/132. On those engines the passphrase-derived AES-GCM
7 unwrap of the private key still works, but the importKey/deriveBits
8 call that loads the signing or sealing key THROWS, and the account
9 cannot be opened at all. This module lets identity.js fall back to
10 the vendored @noble/curves implementation so the same account
11 still signs and still opens sealed messages.
12
13 INTEROPERABILITY. The fallback is bit-identical to WebCrypto:
14 Ed25519 is deterministic (RFC 8032), so a signature made here
15 verifies under WebCrypto and vice versa; an X25519 shared secret
16 computed here equals the one WebCrypto derives from the same keys.
17 Verified against Node's WebCrypto for both curves. Nothing about
18 the on-disk format or the account's algorithm changes.
19
20 SECURITY TRADEOFF — READ THIS. WebCrypto keeps a private key
21 NON-EXTRACTABLE: the bytes never enter JS. This fallback cannot;
22 it necessarily holds the raw 32-byte Ed25519 seed and the raw
23 32-byte X25519 scalar in JS memory for as long as the identity is
24 unlocked. That is a real reduction in protection against a script
25 that can read this page's heap. It is accepted ONLY because the
26 alternative on these engines is that the user cannot log in on
27 this device AT ALL. The material is never logged and never
28 transmitted; identity.js zeroes it on lock() where practical.
29 Callers must keep it that way.
30
31 Classic script, attached as `window.DaimondCurveFallback` to match
32 identity.js. Depends on `window.DaimondNoble` from
33 vendor/noble-curves.min.js, which must load first.
34 ============================================================ */
35(function () {
36 'use strict';
37
38 // pkcs8 (RFC 8410) for these curves is a fixed 48-byte structure: a
39 // 16-byte header, then the 32-byte key. The header's twelfth byte is the
40 // algorithm OID's final octet — 0x70 for Ed25519, 0x6e for X25519 — and it
41 // is the one byte worth checking, so a key of the wrong curve is refused
42 // rather than silently misread.
43 var PKCS8_LEN = 48;
44 var HEADER_LEN = 16;
45 var OID_INDEX = 11;
46 var OID_ED25519 = 0x70;
47 var OID_X25519 = 0x6e;
48
49 // The exact 16-byte pkcs8 header WebCrypto emits for an X25519 private key,
50 // used to rebuild a pkcs8 around a freshly generated scalar so a later
51 // modern browser can import it unchanged.
52 var X25519_PKCS8_HEADER = [
53 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
54 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20,
55 ];
56
57 function noble() {
58 return (typeof window !== 'undefined' && window.DaimondNoble) || null;
59 }
60
61 /// Is the vendored pure-JS implementation loaded and usable?
62 function available() {
63 var n = noble();
64 return !!(n && n.ed25519 && n.x25519
65 && typeof n.ed25519.sign === 'function'
66 && typeof n.x25519.getSharedSecret === 'function');
67 }
68
69 /// Extract the 32-byte key from a pkcs8 of the expected curve, or throw.
70 function keyFromPkcs8(pkcs8, oidByte) {
71 var b = (pkcs8 instanceof Uint8Array) ? pkcs8 : new Uint8Array(pkcs8);
72 if (b.length !== PKCS8_LEN || b[OID_INDEX] !== oidByte) {
73 throw new Error('curvefallback: unexpected pkcs8 shape');
74 }
75 return b.slice(HEADER_LEN); // a copy, not a view onto the caller's buffer.
76 }
77
78 /// The 32-byte Ed25519 seed from an Ed25519 pkcs8.
79 function edSeedFromPkcs8(pkcs8) {
80 return keyFromPkcs8(pkcs8, OID_ED25519);
81 }
82
83 /// The 32-byte X25519 scalar from an X25519 pkcs8.
84 function xScalarFromPkcs8(pkcs8) {
85 return keyFromPkcs8(pkcs8, OID_X25519);
86 }
87
88 /// Wrap a 32-byte X25519 scalar back into the pkcs8 WebCrypto would emit,
89 /// so a stored sealing key made here stays importable by a modern engine.
90 function xPkcs8FromScalar(scalar) {
91 var s = (scalar instanceof Uint8Array) ? scalar : new Uint8Array(scalar);
92 if (s.length !== 32) throw new Error('curvefallback: X25519 scalar must be 32 bytes');
93 var out = new Uint8Array(PKCS8_LEN);
94 out.set(X25519_PKCS8_HEADER, 0);
95 out.set(s, HEADER_LEN);
96 return out;
97 }
98
99 /// The raw 32-byte Ed25519 public key for a seed.
100 function edPublicKey(seed) {
101 return noble().ed25519.getPublicKey(seed);
102 }
103
104 /// An Ed25519 signature (64 bytes) over the given bytes. Deterministic,
105 /// so identical to the one WebCrypto would produce for the same key.
106 function edSign(seed, data) {
107 return noble().ed25519.sign(data, seed);
108 }
109
110 /// Verify an Ed25519 signature. Used by the interop test.
111 function edVerify(pub, sig, data) {
112 try { return noble().ed25519.verify(sig, data, pub); }
113 catch (e) { return false; }
114 }
115
116 /// The raw 32-byte X25519 public key for a scalar.
117 function xPublicKey(scalar) {
118 return noble().x25519.getPublicKey(scalar);
119 }
120
121 /// The raw 32-byte X25519 shared secret — the same bytes WebCrypto's
122 /// deriveBits returns. The INPUT to a key derivation, never a key itself.
123 function xSharedSecret(scalar, theirPub) {
124 return noble().x25519.getSharedSecret(scalar, theirPub);
125 }
126
127 /// A fresh 32-byte X25519 scalar for generating a sealing key on an engine
128 /// without WebCrypto X25519. noble clamps at use; WebCrypto clamps on
129 /// import, so the two agree on the derived secret.
130 function randomXScalar() {
131 return crypto.getRandomValues(new Uint8Array(32));
132 }
133
134 /// Best-effort overwrite of a byte buffer. Not a guarantee — the JS engine
135 /// may have copied it — but it shortens the window where the material sits
136 /// readable, which is the whole reason this file documents its tradeoff.
137 function zero(bytes) {
138 if (bytes && typeof bytes.fill === 'function') {
139 try { bytes.fill(0); } catch (e) { /* frozen or detached */ }
140 }
141 }
142
143 window.DaimondCurveFallback = {
144 available: available,
145 edSeedFromPkcs8: edSeedFromPkcs8,
146 xScalarFromPkcs8: xScalarFromPkcs8,
147 xPkcs8FromScalar: xPkcs8FromScalar,
148 edPublicKey: edPublicKey,
149 edSign: edSign,
150 edVerify: edVerify,
151 xPublicKey: xPublicKey,
152 xSharedSecret: xSharedSecret,
153 randomXScalar: randomXScalar,
154 zero: zero,
155 };
156})();