UNPKG

micro-key-producer

Version:

Produces secure passwords & keys for WebCrypto, SSH, PGP, SLIP10, OTP and many others

1,239 lines 49.9 kB
/** * Converts public/secret keys into JWK or DER (PKCS#8 & SPKI). * * DER support is close to Web Crypto: * - No explicit curve definitions. * - No other curves like Brainpool or secp. * - Various possible encodings with different levels of support (e.g., optional public key), * versions, etc. Attributes inside keys are ignored for now. * - We encode keys the same as Web Crypto by default. * - All returned keys match Web Crypto exactly. * - JWK keys include key usage. For P-256 and related curves, we use an additional * `_jwk_ecdh` coder to encode keys for ECDH usage. * * ASN.1 is theoretically neat but overly complex: * - DER provides canonical encoding, but there are two valid locations for publicKey * inside secretKey. * - OID-based encodings are fragmented across multiple RFCs. * - Crypto specs evolve inconsistently (e.g., EC, Ed25519, X25519 use different algorithms). * - Optional fields (attributes) can vary, leading to fingerprinting risks. * * We aim for a tree-shaking friendly interface: * - It's possible to use only JWK or only DER support. * - This mode hasn't been thoroughly tested in isolation. * * Curves and encoding: * - `isCompressed` in `getPublicKey` is fragile. Different curves may mishandle this flag. * If always compressed, it's ignored. * - DER supports both compressed and uncompressed formats. We preserve the user-provided * format during encoding. * - Secret keys: Ed25519 secrets are raw bytes; `Fn.fromBytes` may fail. * - Public keys are always points; X25519 lacks a dedicated `Point` class. * * TODO: * - Add more tests (e.g., Wycheproof SPKI vectors). * - Integrate with @noble/curves tests (despite circular deps). * - Support additional curves (Brainpool, secp...). * - Consider DER signature parsing (ASN.1 parser looks robust). * - Add RSA support (existing package available). * - Handle encrypted DER keys (unsupported by Web Crypto). * - Support explicit curve parameters (a/b, seed) — not common but present in some test vectors. * - Implement PEM conversion (Base64 armor). * * @module */ import { bytesToNumberBE } from '@noble/ciphers/utils.js'; import { ed25519, x25519 } from '@noble/curves/ed25519.js'; import { ed448, x448 } from '@noble/curves/ed448.js'; import { concatBytes } from '@noble/hashes/utils.js'; import { p256, p384, p521 } from '@noble/curves/nist.js'; import { equalBytes, numberToVarBytesBE } from '@noble/curves/utils.js'; import { base64urlnopad, utils as baseUtils } from '@scure/base'; import * as P from 'micro-packed'; const fixCoder = /* @__PURE__ */ (c, ...args) => ({ encode: (data) => c.toBytes(data, ...args), decode: (data) => c.fromBytes(data), }); function jwkPointCoder(pc) { const FpC = baseUtils.chain(fixCoder(pc.Fp), base64urlnopad); return { encode: (bytes) => { const { x, y } = pc.fromBytes(bytes).toAffine(); return { x: FpC.encode(x), y: FpC.encode(y) }; }, decode: (key) => pc.fromAffine({ x: FpC.decode(key.x), y: FpC.decode(key.y) }).toBytes(), }; } const jwkBytesCoder = { encode: (bytes) => ({ x: base64urlnopad.encode(bytes) }), decode: (key) => base64urlnopad.decode(key.x), }; function jwkConverter(curve, pubCoder, opts = {}, derive) { const secUsage = derive ? ['deriveBits'] : ['sign']; const pubUsage = derive ? [] : ['verify']; Object.freeze(opts); Object.freeze(pubUsage); Object.freeze(secUsage); function checkKey(key) { if (key.kty !== opts.kty || key.crv !== opts.crv || key.alg !== opts.alg) throw new Error('wrong curve'); } const publicKey = { encode: (bytes) => { if ('isValidPublicKey' in curve.utils) { if (!curve.utils.isValidPublicKey(bytes)) throw new Error('wrong public key'); } return { ...opts, ext: true, key_ops: pubUsage, ...pubCoder.encode(bytes) }; }, decode: (key) => { checkKey(key); return pubCoder.decode(key); }, }; const secretKey = { encode: (bytes) => { const pub = curve.getPublicKey(bytes); return { ...publicKey.encode(pub), key_ops: secUsage, d: base64urlnopad.encode(bytes), }; }, decode: (key) => { const pub = publicKey.decode(key); const res = base64urlnopad.decode(key.d); if (!equalBytes(pub, curve.getPublicKey(res))) throw new Error('wrong public key'); return res; }, }; return { publicKey, secretKey }; } /* * In @noble/curves we include a minimal, somewhat fragile ASN.1 DER decoder for signatures. * That approach works for simple signature structures, but here we face: * - Complex nested ASN.1 structures * - Multiple fields, optional fields, and versioned formats * * Any zero-dependency implementation would either re-implement large parts of micro-packed * or introduce a substantial amount of brittle code. Fortunately, this package already * depends on micro-packed. * * TODO: * - Consider moving the signature coder into this package and removing it from @noble/curves. * Consumers who need it could import it from here. * - We’re partway toward full ASN.1 encoding/decoding for certificates—worth exploring further. * - We can still use this API in @noble/curves tests despite circular dependencies by * reconstructing coders with the actual curve import via this derConvert API. */ const ASN1 = /* @__PURE__ */ (() => { // All tags are not mandatory. Nevertheless, still included to see if something decoded wrong const tagPrimitive = /* @__PURE__ */ P.map(P.bits(5), { boolean: 1, integer: 2, bitString: 3, octetString: 4, null: 5, oid: 6, real: 9, enum: 10, utf8: 12, relativeOid: 13, sequence: 16, set: 17, numericString: 18, printableString: 19, // A–Z, 0–9, space, limited symbols teletexString: 20, videotexString: 21, IA5String: 22, UTCTime: 23, generalizedTime: 24, visibleString: 26, generalString: 27, bmpString: 30, // UCS-2 (2 bytes per char) }); const tagNumber = /* @__PURE__ */ P.validate(P.bits(5), (n) => { if (n === 0b11111) throw new Error('multi-byte tags not supported'); return n; }); const tag = /* @__PURE__ */ P.validate(P.mappedTag(P.bits(2), { universal: [0, P.struct({ constructed: P.bits(1), type: tagPrimitive })], application: [1, P.struct({ constructed: P.bits(1), number: tagNumber })], contextSpecific: [2, P.struct({ constructed: P.bits(1), number: tagNumber })], private: [3, P.struct({ constructed: P.bits(1), number: tagNumber })], }), (val) => { if (val.TAG === 'universal') { if (['sequence', 'set'].includes(val.data.type)) { if (!val.data.constructed) throw new Error('SEQUENCE/SET must be constructed in DER'); } else if (val.data.constructed) throw new Error('Constructed encoding forbidden for this universal tag in DER'); } return val; }); // TODO: add dynamic size support to P.bigint/P.int? seems needed only here. // would be P.int(P.bits(7)). Seems easy, but not sure if its worth it. const varInt = /* @__PURE__ */ P.apply(P.bytes(P.bits(7)), { encode: (from) => P.int(from.length).decode(from), decode: (to) => numberToVarBytesBE(to), }); const length = /* @__PURE__ */ P.apply(P.mappedTag(P.bits(1), { short: [0, P.bits(7)], long: [1, varInt], }), { encode: (from) => from.data, decode: (to) => ({ TAG: to < 0x80 ? 'short' : 'long', data: to }), }); const tlv = /* @__PURE__ */ P.struct({ tag, data: P.bytes(length) }); const basic = (typeTag, inner) => { const tagByte = tag.encode(typeTag)[0]; return { tagByte, tagBytes: [tagByte], constructed: typeTag.data.constructed, inner, ...P.wrap({ encodeStream(w, value) { tlv.encodeStream(w, { tag: typeTag, data: inner.encode(value), }); }, decodeStream(r) { return inner.decode(tlv.decodeStream(r).data); }, }), }; }; // Primitive types const Integer = basic({ TAG: 'universal', data: { constructed: 0, type: 'integer' } }, P.wrap({ encodeStream(w, value) { if (value < 0) throw new Error('negative values not allowed'); const bytes = numberToVarBytesBE(value); if (bytes[0] & 0x80) w.byte(0x00); // prepend 0x00 for positive w.bytes(bytes); }, decodeStream(r) { const bytes = r.bytes(r.leftBytes); // up to known length if (bytes[0] & 0x80) throw new Error('negative values not allowed'); return bytesToNumberBE(bytes); }, })); // TODO: merge with PGP? This is more robust (different results). Looks like LEB128 (wasm stuff) const OID = basic({ TAG: 'universal', data: { constructed: 0, type: 'oid' } }, P.wrap({ encodeStream(w, oidStr) { const parts = oidStr.split('.').map(Number); if (parts.length < 2) throw new Error('OID must have at least two arcs'); const [first, second, ...rest] = parts; if (first < 0 || first > 2) throw new Error('First arc out of range'); if ((first < 2 && second > 39) || second < 0) throw new Error('Second arc out of range for first arc'); // Combine first two arcs into single value let combined = 40 * first + second; // P.array({continue: P.bits(1), value: P.bits(7)}), then when !continue push current value to out. // But we would need to read number from left side const out = []; // Base-128 encode (highest bit signals continuation, not just radix2**7) const encodeBase128 = (val) => { const tmp = []; for (let v = val; v; v >>= 7) tmp.unshift(v & 0x7f); if (!val) tmp.push(0); for (let i = 0; i < tmp.length - 1; i++) out.push(tmp[i] | 0x80); out.push(tmp[tmp.length - 1]); }; encodeBase128(combined); for (const val of rest) { if (val < 0) throw new Error('Negative OID arc'); encodeBase128(val); } w.bytes(Uint8Array.from(out)); }, decodeStream(r) { const bytes = r.bytes(r.leftBytes); // Must already be scoped to OID length if (bytes.length === 0) throw new Error('Empty OID encoding'); const firstVal = bytes[0]; const firstArc = Math.floor(firstVal / 40); const secondArc = firstVal % 40; const out = [firstArc, secondArc]; let val = 0; for (let i = 1; i < bytes.length; i++) { val = (val << 7) | (bytes[i] & 0x7f); if ((bytes[i] & 0x80) === 0) { out.push(val); val = 0; } } if (val !== 0) throw new Error('Truncated OID encoding'); // Range check if ((out[0] < 2 && out[1] > 39) || out[1] < 0) throw new Error('Invalid OID second arc'); return out.join('.'); }, })); const OctetString = basic({ TAG: 'universal', data: { constructed: 0, type: 'octetString' } }, P.bytes(null)); const BitString = basic({ TAG: 'universal', data: { constructed: 0, type: 'bitString' } }, P.wrap({ encodeStream(w, value) { w.byte(0); w.bytes(value); }, decodeStream(r) { const leftBits = r.byte(); if (leftBits !== 0) throw new Error('ASN1.bitString: non-zero amount of leftover bits'); return r.bytes(r.leftBytes); }, })); const sequence = (fields) => { return basic({ TAG: 'universal', data: { constructed: 1, type: 'sequence' } }, P.struct(fields)); }; const choice = (variants) => { const keys = Object.keys(variants); if (!keys.length) throw new Error('ASN1.choice: empty variants'); const tagBytes = keys.flatMap((k) => { const v = variants[k]; if (v.tagBytes && v.tagBytes.length) return v.tagBytes; return v.tagByte === undefined ? [] : [v.tagByte]; }); return { tagByte: tagBytes[0], tagBytes, constructed: variants[keys[0]].constructed, inner: P.bytes(null), ...P.wrap({ encodeStream(w, value) { if (!value.TAG || !variants[value.TAG]) throw new Error('ASN1.choice: unknown variant=' + value.TAG); variants[value.TAG].encodeStream(w, value.data); }, decodeStream(r) { const tag = r.byte(true); for (const k in variants) { const v = variants[k]; const tags = v.tagBytes && v.tagBytes.length ? v.tagBytes : v.tagByte === undefined ? [] : [v.tagByte]; if (tags.length && !tags.includes(tag)) continue; return { TAG: k, data: v.decodeStream(r) }; } throw new Error('ASN1.choice: unknown variant=' + tag); }, }), }; }; // Small schema-less parser for debug. Useful to see whats going on inside, but not enough for schema parsing. const debug = P.apply(tlv, { encode(from) { if (from.tag.TAG === 'universal') { if (['sequence', 'set'].includes(from.tag.data.type)) return P.array(null, debug).decode(from.data); if (from.tag.data.type === 'integer') return Integer.inner.decode(from.data); if (from.tag.data.type === 'oid') return OID.inner.decode(from.data); if (from.tag.data.type === 'octetString') return OctetString.inner.decode(from.data); if (from.tag.data.type === 'null') return null; } if (from.tag.TAG === 'contextSpecific' && from.tag.data.constructed) return debug.decode(from.data); return from; }, decode(_to) { // Without schema we cannot know how to encode stuff (is Uint8Array is octetString or bitString?) throw new Error('not supported'); }, }); return { debug, Integer, OctetString, OID, BitString, UTF8: basic({ TAG: 'universal', data: { constructed: 0, type: 'utf8' } }, P.string(null)), null: basic({ TAG: 'universal', data: { constructed: 0, type: 'null' } }, P.constant(null)), choice, sequence, set: (inner) => basic({ TAG: 'universal', data: { constructed: 1, type: 'set' } }, P.array(null, inner)), explicit: (number, inner) => basic({ TAG: 'contextSpecific', data: { constructed: 1, number } }, inner), implicit: (number, inner) => basic({ TAG: 'contextSpecific', data: { constructed: inner.constructed, number } }, inner.inner), // hides actual tag optional: (inner) => { const tagBytes = inner.tagBytes && inner.tagBytes.length ? inner.tagBytes : inner.tagByte === undefined ? [] : [inner.tagByte]; return { tagByte: inner.tagByte, tagBytes, inner: inner.inner, constructed: inner.constructed, ...P.wrap({ encodeStream(w, value) { if (value === undefined) return; inner.encodeStream(w, value); }, decodeStream(r) { if (r.isEnd()) return undefined; if (!tagBytes.length) throw new Error('ASN1.optional: inner coder has no tag information'); const tag = r.byte(true); if (!tagBytes.includes(tag)) return undefined; return inner.decodeStream(r); }, }), }; }, }; })(); // https://www.rfc-editor.org/rfc/rfc5480 // https://www.rfc-editor.org/rfc/rfc5915 // https://www.rfc-editor.org/rfc/rfc5958 // https://www.rfc-editor.org/rfc/rfc8410 const SpecifiedECDomain = /* @__PURE__ */ (() => ASN1.sequence({ version: ASN1.Integer, // 1 | 2 | 3. 1 -> hash optional, 2|3 -> hash mandatory, 3 -> maybe extra params fieldId: ASN1.sequence({ info: P.mappedTag(ASN1.OID, { primeField: ['1.2.840.10045.1.1', ASN1.Integer], binaryField: ['1.2.840.10045.1.2', P.bytes(null)], // a lot of stuff, basises, polynominals, too complex }), }), curve: ASN1.sequence({ a: ASN1.OctetString, b: ASN1.OctetString, seed: ASN1.optional(ASN1.BitString), }), base: ASN1.OctetString, order: ASN1.Integer, cofactor: ASN1.optional(ASN1.Integer), hash: ASN1.optional(ASN1.sequence({ algorithm: ASN1.OID, rest: P.bytes(null) })), rest: P.bytes(null), }))(); const ECParameters = /* @__PURE__ */ (() => ASN1.choice({ namedCurve: ASN1.OID, implicitCurve: ASN1.null, specifiedCurve: SpecifiedECDomain, }))(); // We can re-use for pub/secret only without RSA. RSA algorithm is different. const KeyAlgorithm = /* @__PURE__ */ (() => ASN1.sequence({ info: P.mappedTag(ASN1.OID, { // Maps webcrypto stuff, Ed/X attached EC: ['1.2.840.10045.2.1', ECParameters], X25519: ['1.3.101.110', P.constant(null)], // X25519 X448: ['1.3.101.111', P.constant(null)], // X448 Ed25519: ['1.3.101.112', P.constant(null)], // Ed25519 Ed448: ['1.3.101.113', P.constant(null)], // Ed448 rsaEncryption: ['1.2.840.113549.1.1.1', ASN1.null], // For micro-rsa-dsa-dh support: don't want to add yet, because it's an extra dependency. // rsassaPss: ['1.2.840.113549.1.1.10', sequence(hashAlgorithm, maskGenAlgorithm, saltLength, trailerField)] // rsaesOaep: ['1.2.840.113549.1.1.7', sequence(hashAlgorithm, maskGenAlgorithm, pSourceAlgorithm)] // Easy to parse, works as additional test for parser structure. DSA: [ '1.2.840.10040.4.1', ASN1.sequence({ p: ASN1.Integer, q: ASN1.Integer, g: ASN1.Integer }), ], }), }))(); // TODO: this is nice, but we cannot put all OIDS here, so, lets do P.bytes(null)? // On other hand, if there is some issues with encoding, we will convert key, but other stuff will fail on it // const DirectoryString = ASN1.choice({ // utf8: ASN1.UTF8, // }); // const Attributes = ASN1.set( // ASN1.sequence({ // attribute: P.mappedTag(ASN1.OID, { // // 1.2.840.113549.1.9.9.20 // friendlyName2: ['1.2.840.113549.1.9.9.20', ASN1.set(DirectoryString)], // friendlyName: ['1.2.840.113549.1.9.20', ASN1.set(DirectoryString)], // localKeyId: ['1.2.840.113549.1.9.21', ASN1.set(ASN1.OctetString)], // challengePassword: ['1.2.840.113549.1.9.7', ASN1.set(DirectoryString)], // }), // }) // ); const Attributes = /* @__PURE__ */ (() => ASN1.set(P.bytes(null)))(); const lenDer = (len) => { if (!Number.isSafeInteger(len) || len < 0) throw new Error(`invalid length ${len}`); if (len < 0x80) return Uint8Array.from([len]); const out = []; for (let n = len; n > 0; n >>= 8) out.unshift(n & 0xff); return Uint8Array.from([0x80 | out.length, ...out]); }; const BER = { parse: (src, pos, allowBER) => { const aTag = src[pos++]; if (aTag === undefined) throw new Error('unexpected end of input'); const cls = aTag >>> 6; const cons = !!(aTag & 0x20); let tagNum = aTag & 0x1f; const tagBytes = [aTag]; if (tagNum === 0x1f) { tagNum = 0; while (true) { const b = src[pos++]; if (b === undefined) throw new Error('unexpected end of high-tag-number'); tagBytes.push(b); tagNum = (tagNum << 7) | (b & 0x7f); if (!(b & 0x80)) break; } } const tg = { bytes: Uint8Array.from(tagBytes), cls, cons, tagNum }; const lenAt = pos; const aLen = src[pos++]; if (aLen === undefined) throw new Error('unexpected end of length'); let ln; if (aLen < 0x80) ln = { len: aLen, indefinite: false }; else if (aLen === 0x80) ln = { indefinite: true }; else { const n = aLen & 0x7f; if (!n) throw new Error('invalid length header'); if (pos + n > src.length) throw new Error('length overrun'); let len = 0; for (let i = 0; i < n; i++) len = (len << 8) | src[pos + i]; pos += n; ln = { len, indefinite: false }; } const lenBytes = src.slice(lenAt, pos); const primitiveTypes = new Set([ 1, 2, 3, 4, 5, 6, 9, 10, 12, 13, 18, 19, 20, 21, 22, 23, 24, 26, 27, 30, ]); if (ln.indefinite) { if (!allowBER) throw new Error('BER indefinite length is not allowed'); if (!tg.cons) throw new Error('BER indefinite length requires constructed tag'); const nodes = []; while (true) { if (src[pos] === 0x00 && src[pos + 1] === 0x00) { pos += 2; break; } const n = BER.parse(src, pos, allowBER); nodes.push(n); pos = n.pos; } const constructed = concatBytes(...nodes.map((i) => i.der)); if (tg.cls === 0 && primitiveTypes.has(tg.tagNum) && tg.tagNum !== 16 && tg.tagNum !== 17) { if (!allowBER) throw new Error('BER constructed primitive is not allowed'); const outTag = tg.bytes.slice(); outTag[0] &= ~0x20; if (tg.tagNum === 3) { if (!nodes.length) return { tag: tg.bytes, len: lenBytes, indefinite: true, children: nodes, der: concatBytes(...[outTag, lenDer(1), Uint8Array.from([0])]), value: Uint8Array.from([0]), pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; const parts = []; let unused = 0; for (let i = 0; i < nodes.length; i++) { const v = nodes[i].value; if (!v.length) throw new Error('invalid constructed BIT STRING chunk'); const u = v[0]; if (i < nodes.length - 1 && u !== 0) throw new Error('invalid constructed BIT STRING intermediate chunk'); unused = u; parts.push(v.slice(1)); } const value = concatBytes(...[Uint8Array.from([unused]), ...parts]); return { tag: tg.bytes, len: lenBytes, indefinite: true, children: nodes, der: concatBytes(...[outTag, lenDer(value.length), value]), value, pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; } const value = concatBytes(...nodes.map((i) => i.value)); return { tag: tg.bytes, len: lenBytes, indefinite: true, children: nodes, der: concatBytes(...[outTag, lenDer(value.length), value]), value, pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; } return { tag: tg.bytes, len: lenBytes, indefinite: true, children: nodes, der: concatBytes(...[tg.bytes, lenDer(constructed.length), constructed]), value: constructed, pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; } if (ln.len === undefined) throw new Error('length missing'); if (pos + ln.len > src.length) throw new Error('length overrun'); const valueRaw = src.slice(pos, pos + ln.len); pos += ln.len; if (!tg.cons) return { tag: tg.bytes, len: lenBytes, indefinite: false, der: concatBytes(...[tg.bytes, lenDer(valueRaw.length), valueRaw]), value: valueRaw, pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; const nodes = []; let at = 0; while (at < valueRaw.length) { const n = BER.parse(valueRaw, at, allowBER); nodes.push(n); at = n.pos; } if (at !== valueRaw.length) throw new Error('constructed value parse mismatch'); const constructed = concatBytes(...nodes.map((i) => i.der)); if (tg.cls === 0 && primitiveTypes.has(tg.tagNum) && tg.tagNum !== 16 && tg.tagNum !== 17) { if (!allowBER) throw new Error('BER constructed primitive is not allowed'); const outTag = tg.bytes.slice(); outTag[0] &= ~0x20; const value = tg.tagNum === 3 ? (() => { if (!nodes.length) return Uint8Array.from([0]); const parts = []; let unused = 0; for (let i = 0; i < nodes.length; i++) { const v = nodes[i].value; if (!v.length) throw new Error('invalid constructed BIT STRING chunk'); const u = v[0]; if (i < nodes.length - 1 && u !== 0) throw new Error('invalid constructed BIT STRING intermediate chunk'); unused = u; parts.push(v.slice(1)); } return concatBytes(...[Uint8Array.from([unused]), ...parts]); })() : concatBytes(...nodes.map((i) => i.value)); return { tag: tg.bytes, len: lenBytes, indefinite: false, children: nodes, der: concatBytes(...[outTag, lenDer(value.length), value]), value, pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; } return { tag: tg.bytes, len: lenBytes, indefinite: false, children: nodes, der: concatBytes(...[tg.bytes, lenDer(constructed.length), constructed]), value: constructed, pos, cls: tg.cls, tagNum: tg.tagNum, cons: tg.cons, }; }, tag: (cls, cons, tagNum) => { if (!Number.isInteger(cls) || cls < 0 || cls > 3) throw new Error(`invalid BER class ${cls}`); if (!Number.isInteger(tagNum) || tagNum < 0) throw new Error(`invalid BER tag number ${tagNum}`); const c = cons ? 0x20 : 0x00; if (tagNum < 31) return Uint8Array.from([(cls << 6) | c | tagNum]); const out = [(cls << 6) | c | 0x1f]; const parts = []; for (let n = tagNum; n > 0; n >>= 7) parts.unshift(n & 0x7f); if (!parts.length) parts.push(0); for (let i = 0; i < parts.length - 1; i++) out.push(parts[i] | 0x80); out.push(parts[parts.length - 1]); return Uint8Array.from(out); }, meta: (n) => ({ len: n.value.length, lenBytes: n.indefinite ? 0 : n.len[0] < 0x80 ? 1 : 1 + (n.len[0] & 0x7f), indefinite: n.indefinite, bitUnused: n.cls === 0 && n.tagNum === 3 && !n.cons && n.value.length ? n.value[0] : undefined, children: n.children?.map(BER.meta), cls: n.cls, tagNum: n.tagNum, cons: n.cons, }), buildRaw: (cls, tagNum, value) => ({ tag: BER.tag(cls, false, tagNum), len: lenDer(value.length), indefinite: false, der: concatBytes(...[BER.tag(cls, false, tagNum), lenDer(value.length), value]), value, pos: 0, cls, tagNum, cons: false, }), len: (len, lenBytes) => { if (!Number.isSafeInteger(len) || len < 0) throw new Error(`invalid BER length ${len}`); if (!Number.isSafeInteger(lenBytes) || lenBytes < 1) throw new Error(`invalid BER length-size ${lenBytes}`); if (lenBytes === 1) { if (len >= 0x80) throw new Error(`short BER length cannot encode ${len}`); return Uint8Array.from([len]); } const width = lenBytes - 1; let n = len; const out = new Uint8Array(lenBytes); out[0] = 0x80 | width; for (let i = lenBytes - 1; i >= 1; i--) { out[i] = n & 0xff; n >>>= 8; } if (n) throw new Error(`BER length ${len} does not fit ${width} bytes`); return out; }, node: (n, meta) => { if (meta.cls !== n.cls || meta.tagNum !== n.tagNum) throw new Error(`BER tag mismatch expected cls=${meta.cls} tag=${meta.tagNum}, got cls=${n.cls} tag=${n.tagNum}`); const tag = BER.tag(meta.cls, meta.cons, meta.tagNum); if (!meta.cons) { const v = n.value; if (meta.indefinite) throw new Error('BER primitive cannot use indefinite length'); return concatBytes(...[tag, BER.len(v.length, meta.lenBytes), v]); } const mm = meta.children || []; let srcChildren; if (n.cons) srcChildren = n.children || []; else if (n.cls === 0 && n.tagNum === 4) { let at = 0; const out = []; for (const m of mm) { const len = m.len; if (!Number.isInteger(len) || len < 0 || at + len > n.value.length) throw new Error('BER child shape mismatch'); const v = n.value.slice(at, at + len); out.push(BER.buildRaw(n.cls, n.tagNum, v)); at += len; } if (at !== n.value.length) throw new Error('BER child shape mismatch'); srcChildren = out; } else if (n.cls === 0 && n.tagNum === 3) { if (!n.value.length) throw new Error('BER child shape mismatch'); const u = n.value[0]; const bits = n.value.slice(1); let at = 0; const out = []; for (let i = 0; i < mm.length; i++) { const m = mm[i]; if (!Number.isInteger(m.len) || m.len < 1) throw new Error('BER child shape mismatch'); const dlen = m.len - 1; if (at + dlen > bits.length) throw new Error('BER child shape mismatch'); const cu = i + 1 === mm.length ? u : m.bitUnused || 0; const v = concatBytes(...[Uint8Array.from([cu]), bits.slice(at, at + dlen)]); out.push(BER.buildRaw(n.cls, n.tagNum, v)); at += dlen; } if (at !== bits.length) throw new Error('BER child shape mismatch'); srcChildren = out; } if (!srcChildren || srcChildren.length !== mm.length) throw new Error('BER child shape mismatch'); const body = concatBytes(...srcChildren.map((c, i) => BER.node(c, mm[i]))); if (meta.indefinite) return concatBytes(...[tag, Uint8Array.from([0x80]), body, Uint8Array.from([0x00, 0x00])]); return concatBytes(...[tag, BER.len(body.length, meta.lenBytes), body]); }, decode: (src, opts = {}) => { const allowBER = !!opts.allowBER; const nodes = []; const der = []; let pos = 0; while (pos < src.length) { const n = BER.parse(src, pos, allowBER); nodes.push(BER.meta(n)); der.push(n.der); pos = n.pos; } return { nodes, der: concatBytes(...der) }; }, encode: (nodes, der) => { const rawNodes = []; let pos = 0; while (pos < der.length) { const n = BER.parse(der, pos, false); rawNodes.push(n); pos = n.pos; } if (rawNodes.length !== nodes.length) throw new Error('BER root node count mismatch'); return concatBytes(...rawNodes.map((n, i) => BER.node(n, nodes[i]))); }, normalize: (src, opts = {}) => { const allowBER = !!opts.allowBER; const out = []; let pos = 0; while (pos < src.length) { const n = BER.parse(src, pos, allowBER); out.push(n.der); pos = n.pos; } return concatBytes(...out); }, }; const PKCS8SecretKey = /* @__PURE__ */ (() => ASN1.choice({ raw: ASN1.OctetString, struct: /* @__PURE__ */ ASN1.sequence({ version: ASN1.Integer, privateKey: ASN1.OctetString, parameters: /* @__PURE__ */ ASN1.optional(/* @__PURE__ */ ASN1.explicit(0, ECParameters)), publicKey: /* @__PURE__ */ ASN1.optional(/* @__PURE__ */ ASN1.explicit(1, ASN1.BitString)), }), }))(); // treeshake: these schema objects still stay alive through member reads unless the whole declaration is pure. const PKCS8 = /* @__PURE__ */ (() => ASN1.sequence({ version: ASN1.Integer, algorithm: KeyAlgorithm, privateKey: /* @__PURE__ */ P.apply(ASN1.OctetString, /* @__PURE__ */ P.coders.reverse(PKCS8SecretKey)), attributes: /* @__PURE__ */ ASN1.optional(/* @__PURE__ */ ASN1.implicit(0, Attributes)), publicKey: /* @__PURE__ */ ASN1.optional(/* @__PURE__ */ ASN1.implicit(1, ASN1.BitString)), }))(); const SPKI = /* @__PURE__ */ (() => ASN1.sequence({ algorithm: KeyAlgorithm, publicKey: ASN1.BitString, }))(); // Could be beautifully typed, but because of isolatedDeclarations, we return garbage. /** * Low-level DER, BER, ASN.1, PKCS#8, and SPKI helpers. * @example * Reach for the raw ASN.1 coders when you need to inspect key structures by hand. * ```ts * import { DERUtils } from 'micro-key-producer/convert.js'; * DERUtils.ASN1.OID.encode('1.2.840.10045.3.1.7'); * ``` */ export const DERUtils = /* @__PURE__ */ (() => { // treeshake: RSA PKCS#1 structure is only needed through DERUtils, not every converter bundle. const RSAPrivateKey = /* @__PURE__ */ ASN1.sequence({ version: ASN1.Integer, modulus: ASN1.Integer, publicExponent: ASN1.Integer, privateExponent: ASN1.Integer, prime1: ASN1.Integer, prime2: ASN1.Integer, exponent1: ASN1.Integer, exponent2: ASN1.Integer, coefficient: ASN1.Integer, }); return { BER, ASN1: ASN1, RSAPrivateKey: RSAPrivateKey, PKCS8SecretKey: PKCS8SecretKey, PKCS8: PKCS8, SPKI: SPKI, }; })(); /** Named-curve OID table used by the DER helpers. */ export const CurveOID = { 'P-256': '1.2.840.10045.3.1.7', 'P-384': '1.3.132.0.34', 'P-521': '1.3.132.0.35', brainpoolP256r1: '1.3.36.3.3.2.8.1.1.7', brainpoolP384r1: '1.3.36.3.3.2.8.1.1.11', brainpoolP512r1: '1.3.36.3.3.2.8.1.1.13', }; /** * Maps a named-curve OID to its public curve name. * @param oid - Object identifier string. * @returns Known curve name or `OID:...` fallback. * @example * Convert a DER named-curve OID into the public curve name used by this package. * ```ts * import { CurveOID, curveOID } from 'micro-key-producer/convert.js'; * curveOID(CurveOID['P-256']); * ``` */ export const curveOID = (oid) => { for (const c in CurveOID) if (CurveOID[c] === oid) return c; return `OID:${oid}`; }; function derConverter(curve, info) { function checkParams(params) { if (params.TAG !== 'namedCurve') throw new Error('non-named curves not supported'); } function checkAlgo(keyInfo) { if (keyInfo.TAG !== info.TAG) throw new Error('different curve algorithm'); if (keyInfo.TAG === 'EC' && info.TAG === 'EC') { checkParams(keyInfo.data); if (keyInfo.data.data !== info.data.data) throw new Error('different curve'); } } if (info.TAG === 'EC') checkParams(info.data); const publicKey = { encode: (key) => { if ('isValidPublicKey' in curve.utils) { if (!curve.utils.isValidPublicKey(key)) throw new Error('wrong public key'); } // we encode what was given always by user (no method to uncompress public key without deps on point) return SPKI.encode({ algorithm: { info }, publicKey: key }); }, decode: (key) => { const decoded = SPKI.decode(key); checkAlgo(decoded.algorithm.info); const publicKey = decoded.publicKey; if ('isValidPublicKey' in curve.utils) { if (!curve.utils.isValidPublicKey(publicKey)) throw new Error('wrong public key'); } return publicKey; }, }; // There also encrypted version of PKCS8, not supported in webcrypto, not supported by us (yet?) const secretKey = { encode: (key, opts = {}) => { if ('isValidSecretKey' in curve.utils) { if (!curve.utils.isValidSecretKey(key)) throw new Error('wrong secret key'); } // uncompressed by default (compat with webcrypto) const publicKey = opts.noPublicKey ? undefined : info.TAG === 'EC' ? curve.getPublicKey(key, !!opts.compressed) // only in weierstrass : curve.getPublicKey(key); const privateKey = info.TAG === 'EC' ? { TAG: 'struct', data: { version: 1n, privateKey: key, publicKey } } : { TAG: 'raw', data: key }; return PKCS8.encode({ version: 0n, algorithm: { info }, privateKey }); }, decode: (key) => { const decoded = PKCS8.decode(key); checkAlgo(decoded.algorithm.info); // EC + struct, other + raw // It would be nice to check if publicKey is valid, but that would leak compressed/uncompressed stuff here. let secretKey; if (decoded.algorithm.info.TAG === 'EC') { if (decoded.privateKey.TAG !== 'struct') throw new Error('derConverter.secretKey.decode: algorithm secret key type mismatch'); if (decoded.privateKey.data.parameters) checkParams(decoded.privateKey.data.parameters); secretKey = decoded.privateKey.data.privateKey; } else { if (decoded.privateKey.TAG !== 'raw') throw new Error('derConverter.secretKey.decode: algorithm secret key type mismatch'); secretKey = decoded.privateKey.data; } // Check publicKey if exists const pub = curve.getPublicKey(secretKey, false); const pubCompressed = curve.getPublicKey(secretKey, true); const checkPub = (p) => { if (!equalBytes(p, pub) && !equalBytes(p, pubCompressed)) throw new Error('wrong public key'); }; if (decoded.publicKey) checkPub(decoded.publicKey); if (decoded.privateKey.TAG === 'struct' && decoded.privateKey.data.publicKey) checkPub(decoded.privateKey.data.publicKey); if ('isValidSecretKey' in curve.utils) { if (!curve.utils.isValidSecretKey(secretKey)) throw new Error('wrong secret key'); } return secretKey; }, }; // const Signature = {} return { publicKey, secretKey }; } // Per-curve definitions /** * JWK converter for P-256 signing keys. * @example * Encode a freshly generated P-256 signing key as JWK. * ```ts * import { p256 } from '@noble/curves/nist.js'; * import { p256_jwk } from 'micro-key-producer/convert.js'; * p256_jwk.secretKey.encode(p256.utils.randomSecretKey()); * ``` */ export const p256_jwk = /* @__PURE__ */ (() => jwkConverter(p256, jwkPointCoder(p256.Point), { kty: 'EC', crv: 'P-256' }, false))(); /** * JWK converter for P-256 ECDH keys. * @example * Encode a P-256 private key for ECDH-oriented JWK consumers. * ```ts * import { p256 } from '@noble/curves/nist.js'; * import { p256_jwk_ecdh } from 'micro-key-producer/convert.js'; * p256_jwk_ecdh.secretKey.encode(p256.utils.randomSecretKey()); * ``` */ export const p256_jwk_ecdh = /* @__PURE__ */ (() => jwkConverter(p256, jwkPointCoder(p256.Point), { kty: 'EC', crv: 'P-256' }, true))(); /** * DER converter for P-256 keys. * @example * Encode the same P-256 secret key into DER/PKCS#8 form. * ```ts * import { p256 } from '@noble/curves/nist.js'; * import { p256_der } from 'micro-key-producer/convert.js'; * p256_der.secretKey.encode(p256.utils.randomSecretKey()); * ``` */ export const p256_der = /* @__PURE__ */ derConverter(p256, { TAG: 'EC', data: { TAG: 'namedCurve', data: '1.2.840.10045.3.1.7' }, }); /** * JWK converter for P-384 signing keys. * @example * Encode a freshly generated P-384 signing key as JWK. * ```ts * import { p384 } from '@noble/curves/nist.js'; * import { p384_jwk } from 'micro-key-producer/convert.js'; * p384_jwk.secretKey.encode(p384.utils.randomSecretKey()); * ``` */ export const p384_jwk = /* @__PURE__ */ (() => jwkConverter(p384, jwkPointCoder(p384.Point), { kty: 'EC', crv: 'P-384' }, false))(); /** * JWK converter for P-384 ECDH keys. * @example * Encode a P-384 private key for ECDH-oriented JWK consumers. * ```ts * import { p384 } from '@noble/curves/nist.js'; * import { p384_jwk_ecdh } from 'micro-key-producer/convert.js'; * p384_jwk_ecdh.secretKey.encode(p384.utils.randomSecretKey()); * ``` */ export const p384_jwk_ecdh = /* @__PURE__ */ (() => jwkConverter(p384, jwkPointCoder(p384.Point), { kty: 'EC', crv: 'P-384' }, true))(); /** * DER converter for P-384 keys. * @example * Encode the same P-384 secret key into DER/PKCS#8 form. * ```ts * import { p384 } from '@noble/curves/nist.js'; * import { p384_der } from 'micro-key-producer/convert.js'; * p384_der.secretKey.encode(p384.utils.randomSecretKey()); * ``` */ export const p384_der = /* @__PURE__ */ derConverter(p384, { TAG: 'EC', data: { TAG: 'namedCurve', data: '1.3.132.0.34' }, }); /** * JWK converter for P-521 signing keys. * @example * Encode a freshly generated P-521 signing key as JWK. * ```ts * import { p521 } from '@noble/curves/nist.js'; * import { p521_jwk } from 'micro-key-producer/convert.js'; * p521_jwk.secretKey.encode(p521.utils.randomSecretKey()); * ``` */ export const p521_jwk = /* @__PURE__ */ (() => jwkConverter(p521, jwkPointCoder(p521.Point), { kty: 'EC', crv: 'P-521' }, false))(); /** * JWK converter for P-521 ECDH keys. * @example * Encode a P-521 private key for ECDH-oriented JWK consumers. * ```ts * import { p521 } from '@noble/curves/nist.js'; * import { p521_jwk_ecdh } from 'micro-key-producer/convert.js'; * p521_jwk_ecdh.secretKey.encode(p521.utils.randomSecretKey()); * ``` */ export const p521_jwk_ecdh = /* @__PURE__ */ (() => jwkConverter(p521, jwkPointCoder(p521.Point), { kty: 'EC', crv: 'P-521' }, true))(); /** * DER converter for P-521 keys. * @example * Encode the same P-521 secret key into DER/PKCS#8 form. * ```ts * import { p521 } from '@noble/curves/nist.js'; * import { p521_der } from 'micro-key-producer/convert.js'; * p521_der.secretKey.encode(p521.utils.randomSecretKey()); * ``` */ export const p521_der = /* @__PURE__ */ derConverter(p521, { TAG: 'EC', data: { TAG: 'namedCurve', data: '1.3.132.0.35' }, }); /** * JWK converter for Ed25519 keys. * @example * Encode an Ed25519 secret key into JWK form. * ```ts * import { ed25519 } from '@noble/curves/ed25519.js'; * import { ed25519_jwk } from 'micro-key-producer/convert.js'; * ed25519_jwk.secretKey.encode(ed25519.utils.randomSecretKey()); * ``` */ export const ed25519_jwk = /* @__PURE__ */ jwkConverter(ed25519, jwkBytesCoder, { kty: 'OKP', crv: 'Ed25519', alg: 'Ed25519' }, false); /** * DER converter for Ed25519 keys. * @example * Encode the same Ed25519 secret key into DER/PKCS#8 form. * ```ts * import { ed25519 } from '@noble/curves/ed25519.js'; * import { ed25519_der } from 'micro-key-producer/convert.js'; * ed25519_der.secretKey.encode(ed25519.utils.randomSecretKey()); * ``` */ export const ed25519_der = /* @__PURE__ */ derConverter(ed25519, { TAG: 'Ed25519', data: null, }); /** * JWK converter for Ed448 keys. * @example * Encode an Ed448 secret key into JWK form. * ```ts * import { ed448 } from '@noble/curves/ed448.js'; * import { ed448_jwk } from 'micro-key-producer/convert.js'; * ed448_jwk.secretKey.encode(ed448.utils.randomSecretKey()); * ``` */ export const ed448_jwk = /* @__PURE__ */ jwkConverter(ed448, jwkBytesCoder, { kty: 'OKP', crv: 'Ed448', alg: 'Ed448' }, false); /** * DER converter for Ed448 keys. * @example * Encode the same Ed448 secret key into DER/PKCS#8 form. * ```ts * import { ed448 } from '@noble/curves/ed448.js'; * import { ed448_der } from 'micro-key-producer/convert.js'; * ed448_der.secretKey.encode(ed448.utils.randomSecretKey()); * ``` */ export const ed448_der = /* @__PURE__ */ derConverter(ed448, { TAG: 'Ed448', data: null, }); /** * JWK converter for X25519 keys. * @example * Encode an X25519 private key into JWK form. * ```ts * import { x25519 } from '@noble/curves/ed25519.js'; * import { x25519_jwk } from 'micro-key-producer/convert.js'; * x25519_jwk.secretKey.encode(x25519.utils.randomSecretKey()); * ``` */ export const x25519_jwk = /* @__PURE__ */ jwkConverter(x25519, jwkBytesCoder, { kty: 'OKP', crv: 'X25519' }, true); /** * DER converter for X25519 keys. * @example * Encode the same X25519 secret key into DER/PKCS#8 form. * ```ts * import { x25519 } from '@noble/curves/ed25519.js'; * import { x25519_der } from 'micro-key-producer/convert.js'; * x25519_der.secretKey.encode(x25519.utils.randomSecretKey()); * ``` */ export const x25519_der = /* @__PURE__ */ derConverter(x25519, { TAG: 'X25519', data: null, }); /** * JWK converter for X448 keys. * @example * Encode an X448 private key into JWK form. * ```ts * import { x448 } from '@noble/curves/ed448.js'; * import { x448_jwk } from 'micro-key-producer/convert.js'; * x448_jwk.secretKey.encode(x448.utils.randomSecretKey()); * ``` */ export const x448_jwk = /* @__PURE__ */ jwkConverter(x448, jwkBytesCoder, { kty: 'OKP', crv: 'X448' }, true); /** * DER converter for X448 keys. * @example * Encode the same X448 secret key into DER/PKCS#8 form. * ```ts * import { x448 } from '@noble/curves/ed448.js'; * import { x448_der } from 'micro-key-producer/convert.js'; * x448_der.secretKey.encode(x448.utils.randomSecretKey()); * ``` */ export const x448_der = /* @__PURE__ */ derConverter(x448, { TAG: 'X448', data: null, }); //# sourceMappingURL=convert.js.map