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 | })(); |