UNPKG

micro-key-producer

Version:

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

1,041 lines 42.1 kB
/*! micro-key-producer - MIT License (c) 2024 Paul Miller (paulmillr.com) */ /** * PGP (GPG) key producer. Allows to deterministically generate ed25519 PGP keys. * * 1. Generated private and public keys would have different representation, however, **their * fingerprints would be the same**. This is because AES encryption is used to hide the keys, and * AES requires different IV / salt. * 2. The function is slow (400ms on Apple M4), because it uses S2K to derive keys. * 3. "warning: lower 3 bits of the secret key are not cleared" happens even for keys generated with * GnuPG 2.3.6, because check looks at item as Opaque MPI, when it is just MPI: see * [bugtracker URL](https://dev.gnupg.org/rGdbfb7f809b89cfe05bdacafdb91a2d485b9fe2e0). * * RFCS: * - main: https://datatracker.ietf.org/doc/html/rfc4880 * - ecdh: https://datatracker.ietf.org/doc/html/rfc6637 * - ed25519: https://www.ietf.org/archive/id/draft-koch-eddsa-for-openpgp-04.txt * - bis: https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.1 * * @module */ import { cfb } from '@noble/ciphers/aes.js'; import { ed25519, x25519 } from '@noble/curves/ed25519.js'; import { bytesToNumberBE, equalBytes, numberToBytesBE, numberToHexUnpadded, } from '@noble/curves/utils.js'; import { ripemd160, sha1 } from '@noble/hashes/legacy.js'; import { sha256, sha512 } from '@noble/hashes/sha2.js'; import { sha3_256 } from '@noble/hashes/sha3.js'; import { concatBytes, isBytes, randomBytes } from '@noble/hashes/utils.js'; import { hex, utf8 } from '@scure/base'; import * as P from 'micro-packed'; import { base64armor } from "./utils.js"; function runAesCfb(keyLen, data, key, iv, decrypt = false) { // NOTE: we need to validate key length here since file can be malformed if (keyLen !== key.length * 8) throw new Error('aes-cfb: wrong key length'); if (iv.length !== 16) throw new Error('aes-cfb: wrong IV'); // Packed does subarray and read is unaligned here // TODO: support unaligned reads in all AES? const keyCopy = key.slice(); const ivCopy = iv.slice(); const dataCopy = data.slice(); const cipher = cfb(keyCopy, ivCopy); const res = decrypt ? cipher.decrypt(dataCopy) : cipher.encrypt(dataCopy); keyCopy.fill(0); ivCopy.fill(0); dataCopy.fill(0); return res; } function createAesCfb(len) { return { encrypt: (plaintext, key, iv) => runAesCfb(len, plaintext, key, iv), decrypt: (ciphertext, key, iv) => runAesCfb(len, ciphertext, key, iv, true), }; } // PGP Types // Multiprecision Integers [RFC4880](https://datatracker.ietf.org/doc/html/rfc4880) /** * RFC 4880 multi-precision integer coder. * @example * Encode one RFC 4880 multi-precision integer. * ```ts * import { mpi } from 'micro-key-producer/pgp.js'; * mpi.encode(1n); * ``` */ export const mpi = /* @__PURE__ */ P.wrap({ encodeStream: (w, value) => { let bitLen = 0; for (let v = value; v > 0n; v >>= 1n, bitLen++) ; P.U16BE.encodeStream(w, bitLen); w.bytes(hex.decode(numberToHexUnpadded(value))); }, decodeStream: (r) => bytesToNumberBE(r.bytes((P.U16BE.decodeStream(r) + 7) >>> 3)), }); // GnuGP violates spec by using non-zero stripped MPI's for secret keys (opaque MPI/SOS). // We need to do the same to create equal keys. // More info: // - https://www.mhonarc.org/archive/html/ietf-openpgp/2019-10/msg00041.html // - https://marc.info/?l=gnupg-devel&m=161518990118244&w=2 /** * Opaque MPI coder used by OpenPGP secret-key packets. * @example * Encode one opaque MPI for an OpenPGP secret-key packet. * ```ts * import { opaquempi } from 'micro-key-producer/pgp.js'; * opaquempi.encode(new Uint8Array([1, 2])); * ``` */ export const opaquempi = /* @__PURE__ */ P.wrap({ encodeStream: (w, value) => { P.U16BE.encodeStream(w, value.length * 8); w.bytes(value); }, decodeStream: (r) => r.bytes((P.U16BE.decodeStream(r) + 7) >>> 3), }); // ASN.1 OID (object identifier) without tag & length // First two elements: [i0 * 40 + i1]. // Others: split in groups of 7 bit chunks, add 0x80 every byte except last(stop flag), like utf8. const OID_MSB = /* @__PURE__ */ (() => 2 ** 7)(); // mask for 8 bit const OID_NO_MSB = /* @__PURE__ */ (() => 2 ** 7 - 1)(); // mask for all bits except 8 /** * ASN.1 OID coder without DER tag/length wrappers. * @example * Encode one ASN.1 object identifier without DER tag and length bytes. * ```ts * import { oid } from 'micro-key-producer/pgp.js'; * oid.encode('1.3.6.1.4.1.11591.15.1'); * ``` */ export const oid = /* @__PURE__ */ P.wrap({ encodeStream: (w, value) => { const items = value.split('.').map((i) => +i); let oid = [items[0] * 40]; if (items.length >= 2) oid[0] += items[1]; for (let i = 2; i < items.length; i++) { const item = []; for (let n = items[i], mask = 0x00; n; n >>= 7, mask = OID_MSB) item.unshift((n & OID_NO_MSB) | mask); oid = oid.concat(item); } w.bytes(new Uint8Array(oid)); }, decodeStream: (r) => { if (r.isEnd()) throw new Error('PGP: empty oid'); const first = r.byte(); let res = `${Math.floor(first / 40)}.${first % 40}`; for (let num = 0; !r.isEnd();) { const byte = r.byte(); num = (num << 7) | (byte & OID_NO_MSB); if (byte & OID_MSB) continue; res += `.${num >>> 0}`; num = 0; } return res; }, }); /** * OpenPGP packet-length coder. * @example * Encode one OpenPGP packet length. * ```ts * import { PacketLen } from 'micro-key-producer/pgp.js'; * PacketLen.encode(191); * ``` */ export const PacketLen = /* @__PURE__ */ P.wrap({ encodeStream: (w, value) => { if (typeof value !== 'number') throw new Error(`PGP.PacketLen invalid length type, ${value}`); if (value < 192) w.byte(value); else if (value < 8383) { value -= 192; w.bytes(new Uint8Array([(value >> 8) + 192, value & 0xff])); } else if (value < 2 ** 32) { w.byte(0xff); P.U32BE.encodeStream(w, value); } else throw new Error(`PGP.PacketLen: length is too big: ${value}`); }, decodeStream: (r) => { let res; const first = r.byte(); if (first < 192) res = first; else if (first < 224) res = ((first - 192) << 8) + r.byte() + 192; else if (first == 255) res = P.U32BE.decodeStream(r); else throw new Error('PGP.PacketLen: Partial body lengths unsupported'); return res; }, }); // PGP Structures const PGP_PACKET_VERSION = /* @__PURE__ */ P.magic(/* @__PURE__ */ P.hex(1), '04'); // only version 4 is supported // Other (RSA/ElGamal/etc) is unsupported const pubKeyEnum = /* @__PURE__ */ P.map(P.U8, { ECDH: 18, ECDSA: 19, EdDSA: 22, }); const ECEnum = /* @__PURE__ */ P.map(/* @__PURE__ */ P.prefix(P.U8, oid), { nistP256: '1.2.840.10045.3.1.7', nistP384: '1.3.132.0.34', nistP521: '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', secp256k1: '1.3.132.0.10', curve25519: '1.3.6.1.4.1.3029.1.5.1', ed25519: '1.3.6.1.4.1.11591.15.1', }); const HashEnum = /* @__PURE__ */ P.map(P.U8, { md5: 1, sha1: 2, ripemd160: 3, sha224: 11, sha256: 8, sha384: 9, sha512: 10, sha3_256: 12, sha3_512: 14, }); const Hash = { ripemd160, sha256, sha512, sha3_256, sha1 }; const EncryptionEnum = /* @__PURE__ */ P.map(P.U8, { plaintext: 0, idea: 1, tripledes: 2, cast5: 3, blowfish: 4, aes128: 7, aes192: 8, aes256: 9, twofish: 10, }); const EncryptionKeySize = { plaintext: 0, aes128: 16, aes192: 24, aes256: 32, }; const CompressionEnum = /* @__PURE__ */ P.map(P.U8, { uncompressed: 0, zip: 1, zlib: 2, bzip2: 3, }); // bis4880 const AEADEnum = /* @__PURE__ */ P.map(P.U8, { None: 0, EAX: 1, OCB: 2, }); // https://datatracker.ietf.org/doc/html/rfc4880#section-3.7.1 const S2KEnum = /* @__PURE__ */ P.map(P.U8, { simple: 0, salted: 1, iterated: 3 }); const S2K = /* @__PURE__ */ P.tag(S2KEnum, { simple: /* @__PURE__ */ P.struct({ hash: HashEnum }), salted: /* @__PURE__ */ P.struct({ hash: HashEnum, salt: /* @__PURE__ */ P.bytes(8) }), iterated: /* @__PURE__ */ P.struct({ hash: HashEnum, salt: /* @__PURE__ */ P.bytes(8), count: P.U8, }), }); // https://datatracker.ietf.org/doc/html/rfc6637#section-9 const ECDSAPub = /* @__PURE__ */ P.struct({ curve: ECEnum, pub: mpi }); const ECDHPub = /* @__PURE__ */ P.struct({ curve: ECEnum, pub: mpi, params: /* @__PURE__ */ P.prefix(P.U8, /* @__PURE__ */ P.struct({ magic: /* @__PURE__ */ P.magic(/* @__PURE__ */ P.hex(1), '01'), hash: HashEnum, encryption: EncryptionEnum, })), }); /** * OpenPGP public-key packet coder. * @example * Encode one Ed25519 OpenPGP public-key packet. * ```ts * import { PubKeyPacket } from 'micro-key-producer/pgp.js'; * import { ed25519 } from '@noble/curves/ed25519.js'; * import { bytesToNumberBE } from '@noble/curves/utils.js'; * import { concatBytes } from '@noble/hashes/utils.js'; * const secretKey = ed25519.utils.randomSecretKey(); * PubKeyPacket.encode({ * created: 0, * algo: { * TAG: 'EdDSA', * data: { * curve: 'ed25519', * pub: bytesToNumberBE(concatBytes(Uint8Array.of(0x40), ed25519.getPublicKey(secretKey))), * }, * }, * }); * ``` */ export const PubKeyPacket = /* @__PURE__ */ (() => P.struct({ version: PGP_PACKET_VERSION, created: P.U32BE, algo: P.tag(pubKeyEnum, { EdDSA: ECDSAPub, ECDH: ECDHPub, }), }))(); const PlainSecretKey = /* @__PURE__ */ (() => P.struct({ secret: P.bytes(null), }))(); const EncryptedSecretKey = /* @__PURE__ */ (() => P.struct({ enc: EncryptionEnum, S2K, // IV as blocksize of algo. For AES it is 16 bytes, others is not supported iv: P.bytes(16), secret: P.bytes(null), }))(); // NOTE: SecretKey is specific packet type as per spec. For user facing API we using 'privateKey' const SecretKeyPacket = /* @__PURE__ */ (() => P.struct({ pub: PubKeyPacket, type: P.mappedTag(P.U8, { plain: [0x00, PlainSecretKey], // Skipping 'Any other value is a symmetric-key encryption algorithm identifier.' encrypted: [254, EncryptedSecretKey], // Same as above, but secret is with checksum encrypted2: [255, EncryptedSecretKey], }), }))(); // https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.1 const SigTypeEnum = /* @__PURE__ */ P.map(P.U8, { binary: 0x00, text: 0x01, standalone: 0x02, certGeneric: 0x10, certPersona: 0x11, certCasual: 0x12, certPositive: 0x13, subkeyBinding: 0x18, keyBinding: 0x19, key: 0x1f, keyRevocation: 0x20, subkeyRevocation: 0x28, certRevocation: 0x30, timestamp: 0x40, thirdParty: 0x50, }); // https://datatracker.ietf.org/doc/html/rfc4880.html#section-5.2.3.1 const signatureSubpacket = /* @__PURE__ */ P.map(P.U8, { signatureCreationTime: 2, signatureExpirationTime: 3, exportableCertification: 4, trustSignature: 5, regularExpression: 6, revocable: 7, keyExpirationTime: 9, placeholderBackwardsCompatibility: 10, preferredEncryptionAlgorithms: 11, revocationKey: 12, issuer: 16, notationData: 20, preferredHashAlgorithms: 21, preferredCompressionAlgorithms: 22, keyServerPreferences: 23, preferredKeyServer: 24, primaryUserID: 25, policyURI: 26, keyFlags: 27, signersUserID: 28, reasonForRevocation: 29, features: 30, signatureTarget: 31, embeddedSignature: 32, // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.1 issuerFingerprint: 33, preferredAEADAlgorithms: 34, intendedRecipientFingerprint: 35, attestedCertifications: 37, keyBlock: 38, }); const SignatureSubpacket = /* @__PURE__ */ P.prefix(PacketLen, /* @__PURE__ */ P.tag(signatureSubpacket, { issuerFingerprint: /* @__PURE__ */ P.struct({ version: PGP_PACKET_VERSION, fingerprint: /* @__PURE__ */ P.hex(20), }), signatureCreationTime: P.U32BE, keyFlags: /* @__PURE__ */ P.bitset([ '_r', 'shared', 'auth', 'split', 'encrypt', 'encryptComm', 'sign', 'certify', ]), preferredEncryptionAlgorithms: /* @__PURE__ */ P.array(null, EncryptionEnum), preferredHashAlgorithms: /* @__PURE__ */ P.array(null, HashEnum), preferredCompressionAlgorithms: /* @__PURE__ */ P.array(null, CompressionEnum), preferredAEADAlgorithms: /* @__PURE__ */ P.array(null, AEADEnum), features: /* @__PURE__ */ P.bitset([ '_r', '_r', '_r', '_r', '_r', 'v5Keys', 'aead', 'modDetect', ]), keyServerPreferences: /* @__PURE__ */ P.bitset(['modDetect'], true), issuer: /* @__PURE__ */ P.hex(8), primaryUserID: P.bool, })); const SignatureSubpackets = /* @__PURE__ */ P.prefix(P.U16BE, /* @__PURE__ */ P.array(null, SignatureSubpacket)); const SignatureHead = /* @__PURE__ */ P.struct({ version: PGP_PACKET_VERSION, type: SigTypeEnum, algo: pubKeyEnum, hash: HashEnum, hashed: SignatureSubpackets, }); const SignaturePacket = /* @__PURE__ */ (() => P.struct({ head: SignatureHead, unhashed: SignatureSubpackets, hashPrefix: P.bytes(2), // 2: ec + dsa, 1 for rsa sig: P.array(null, mpi), }))(); const UserPacket = /* @__PURE__ */ P.string(null); // PGP Functions const EXPBIAS6 = (count) => (16 + (count & 15)) << ((count >> 4) + 6); function deriveKey(hash, len, password, salt, count) { // Important: there is difference between zero and empty count count = count === undefined ? 0 : EXPBIAS6(count); const data = salt ? concatBytes(salt, password) : password; let out = Uint8Array.of(); const hashC = Hash[hash]; if (!hashC) throw new Error('PGP.deriveKey: unknown hash'); const rounds = Math.ceil(len / hashC.outputLen); for (let r = 0; r < rounds; r++) { const h = hashC.create(); // prefix if (r > 0) h.update(new Uint8Array(r)); for (let c = Math.max(count, data.length); c > 0;) { const take = Math.min(c, data.length); h.update(data.subarray(0, take)); c -= take; } out = concatBytes(h.digest()); } return out.subarray(0, len); } const Encryption = { aes128: /* @__PURE__ */ createAesCfb(128), aes192: /* @__PURE__ */ createAesCfb(192), aes256: /* @__PURE__ */ createAesCfb(256), }; // https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.4 const hashTail = /* @__PURE__ */ Uint8Array.from([0x04, 0xff]); const hashPubKey = /* @__PURE__ */ P.struct({ magic: /* @__PURE__ */ P.magic(/* @__PURE__ */ P.hex(1), '99'), pubKey: /* @__PURE__ */ P.prefix(P.U16BE, PubKeyPacket), }); const hashUser = /* @__PURE__ */ P.struct({ magic: /* @__PURE__ */ P.magic(/* @__PURE__ */ P.hex(1), 'b4'), user: /* @__PURE__ */ P.prefix(P.U32BE, UserPacket), }); const hashSelfCert = /* @__PURE__ */ P.struct({ pubKey: hashPubKey, user: hashUser }); const hashSubKeyCert = /* @__PURE__ */ P.struct({ pubKey: hashPubKey, subKey: hashPubKey }); function hashSignature(head, data) { const hashC = Hash[head.hash]; if (!hashC) throw new Error('PGP.hashSignature: unknown hash'); const h = hashC.create(); if (['certGeneric', 'certPersona', 'certCasual', 'certPositive'].includes(head.type)) h.update(hashSelfCert.encode(data)); else if (head.type === 'subkeyBinding') h.update(hashSubKeyCert.encode(data)); else if (head.type === 'binary') { if (!isBytes(data)) throw new Error('hashSignature: wrong data for type=binary'); h.update(data); } else if (head.type === 'text') { // For text document signatures (type 0x01), the // document is canonicalized by converting line endings to <CR><LF>, // and the resulting data is hashed if (typeof data !== 'string') throw new Error('hashSignature: wrong data for type=text'); const NL = '\r\n'; let canonical = data.replace(/\r\n|\n|\r/g, NL); if (!canonical.endsWith(NL)) canonical += NL; h.update(utf8.decode(canonical)); } else throw new Error('Unknown signature type'); const sigData = SignatureHead.encode(head); h.update(sigData).update(hashTail).update(P.U32BE.encode(sigData.length)); return h.digest(); } // https://datatracker.ietf.org/doc/html/rfc4880#section-6.1 function crc24(data) { let crc = 0xb704ce; for (let i = 0; i < data.length; i++) { crc ^= data[i] << 16; for (let j = 0; j < 8; j++) { crc <<= 1; if (crc & 0x1000000) crc ^= 0x1864cfb; } } return new Uint8Array([(crc >> 16) & 0xff, (crc >> 8) & 0xff, crc & 0xff]); } const PacketTags = { userId: UserPacket, signature: SignaturePacket, publicKey: PubKeyPacket, publicSubkey: PubKeyPacket, secretKey: SecretKeyPacket, secretSubkey: SecretKeyPacket, }; // https://datatracker.ietf.org/doc/html/rfc4880#section-4.2 // Old packet: [1, version: 0, tag(4), lenType(2)] -- 8 bits // New packet: [1, version: 1, tag(6)] + len(bytes) -> not supported, GPG generates version 0 for now const PacketHead = /* @__PURE__ */ (() => P.struct({ magic: P.magic(P.bits(1), 1), version: P.magic(P.bits(1), 0), // https://datatracker.ietf.org/doc/html/rfc4880#section-4.3 tag: P.map(P.bits(4), { public_key_encrypted_session_key: 1, signature: 2, symmetric_key_encrypted_session_key: 3, onePassSignature: 4, secretKey: 5, publicKey: 6, secretSubkey: 7, compressedData: 8, encryptedData: 9, marker: 10, literalData: 11, trust: 12, userId: 13, publicSubkey: 14, userAttribute: 17, encryptedProtectedData: 18, modificationDetectionCode: 19, }), lenType: P.bits(2), }))(); const Packet = /* @__PURE__ */ P.wrap({ encodeStream: (w, value) => { const data = PacketTags[value.TAG].encode(value.data); const lenType = data.length < 2 ** 8 ? 0 : data.length < 2 ** 16 ? 1 : 2; PacketHead.encodeStream(w, { tag: value.TAG, lenType }); [P.U8, P.U16BE, P.U32BE][lenType].encodeStream(w, data.length); w.bytes(data); }, decodeStream: (r) => { const { tag, lenType } = PacketHead.decodeStream(r); const packetLen = lenType !== 3 ? [P.U8, P.U16BE, P.U32BE][lenType].decodeStream(r) : r.leftBytes; return { TAG: tag, data: PacketTags[tag].decode(r.bytes(packetLen)) }; }, }); /** * OpenPGP packet-stream coder. * @example * Encode a short sequence of OpenPGP packets into the binary stream format. * ```ts * import { Stream } from 'micro-key-producer/pgp.js'; * Stream.encode([{ TAG: 'userId', data: 'alice@example.com' }]); * ``` */ export const Stream = /* @__PURE__ */ P.array(null, Packet); // Key generation const EDSIGN = /* @__PURE__ */ P.array(null, P.U256BE); function signData(head, unhashed, data, privateKey) { const hash = hashSignature(head, data); const hashPrefix = hash.subarray(0, 2); const sig = EDSIGN.decode(ed25519.sign(hash, privateKey)); return { head, unhashed, hashPrefix, sig }; } function verifyData(head, data, sig, publicKey) { const hash = hashSignature(head, data); const hashPrefix = hash.subarray(0, 2); const verified = ed25519.verify(EDSIGN.encode(sig), hash, publicKey); return { head, hashPrefix, hash, verified }; } function secretChecksum(data) { // Wow, third checksum algorithm in single spec! let checksum = 0; for (let i = 0; i < data.length; i++) checksum += data[i]; checksum %= 65536; return checksum; } // TODO: cleanup? const secretChecksumCoder = { decode(secret) { const [data, checksum] = [secret.slice(0, -2), P.U16BE.decode(secret.slice(-2))]; let ourChecksum = secretChecksum(data); if (ourChecksum !== checksum) throw new Error('PGP.secretKey: wrong checksum for plain encoding'); return mpi.decode(data); }, encode(secret) { const encoded = mpi.encode(bytesToNumberBE(secret)); return concatBytes(encoded, P.U16BE.encode(secretChecksum(encoded))); }, }; /** * Decrypts the secret scalar from a PGP secret-key packet. * @param password - Secret-key passphrase. * @param key - Parsed secret-key packet. * @returns Secret scalar as a bigint. * @throws If the packet uses unsupported encryption or fails checksum validation. {@link Error} * @example * Decrypt the secret scalar stored inside an armored private key packet. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { decodeSecretKey, getKeys, privArmor } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * const [packet] = privArmor.decode(getKeys(seed, 'alice@example.com', 'password').privateKey); * decodeSecretKey('password', packet.data); * ``` */ export function decodeSecretKey(password, key) { if (key.type.TAG === 'plain') return secretChecksumCoder.decode(key.type.data.secret); const keyData = key.type.data; const data = keyData.S2K.data; const keyLen = EncryptionKeySize[keyData.enc]; if (keyLen === undefined) throw new Error(`PGP.secretKey: unknown encryption mode=${keyData.enc}`); const encKey = deriveKey(data.hash, keyLen, utf8.decode(password), data.salt, data.count); const decrypted = Encryption[keyData.enc].decrypt(keyData.secret, encKey, keyData.iv); const decryptedKey = decrypted.subarray(0, -20); const checksum = Hash.sha1(decryptedKey); if (!equalBytes(decrypted.slice(-20), checksum)) throw new Error('PGP.secretKey: invalid sha1 checksum'); if (!['ECDH', 'ECDSA', 'EdDSA'].includes(key.pub.algo.TAG)) throw new Error(`PGP.secretKey unsupported publicKey algorithm: ${key.pub.algo.TAG}`); // Decoded as generic MPI, not as OpaqueMPI if (key.type.TAG === 'encrypted2') return secretChecksumCoder.decode(decryptedKey); return mpi.decode(decryptedKey); } function createPrivKey(pub, key, password, salt, iv, hash = 'sha1', count = 240, enc = 'aes128') { const keyLen = EncryptionKeySize[enc]; if (keyLen === undefined) throw new Error(`PGP.secretKey: unknown encryption mode=${enc}`); // Export key without password if (password === undefined) return { pub, type: { TAG: 'plain', data: { secret: secretChecksumCoder.encode(key) } } }; if (!isBytes(iv)) throw new Error('PGP.secretKey: no iv'); const encKey = deriveKey(hash, keyLen, utf8.decode(password), salt, count); const keyBytes = opaquempi.encode(key); const secretClear = concatBytes(keyBytes, sha1(keyBytes)); const secret = Encryption[enc].encrypt(secretClear, encKey, iv); const S2K = { TAG: 'iterated', data: { hash, salt, count } }; return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } }; } /** * ASCII armor for PGP public key blocks. * @example * Decode the armored public block that `getKeys()` produces. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeys, pubArmor } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * pubArmor.decode(getKeys(seed, 'alice@example.com').publicKey); * ``` */ export const pubArmor = /* @__PURE__ */ base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24); /** * ASCII armor for PGP private key blocks. * @example * Decode the armored private block back into OpenPGP packets. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeys, privArmor } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * privArmor.decode(getKeys(seed, 'alice@example.com').privateKey); * ``` */ export const privArmor = /* @__PURE__ */ base64armor('PGP PRIVATE KEY BLOCK', 64, Stream, crc24); /** * ASCII armor for detached PGP signatures. * @example * Decode an armored detached signature back into its packet list. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeyId, sigArmor, signDetached } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * sigArmor.decode(signDetached(seed, 'hello', getKeyId(seed).fingerprint)); * ``` */ export const sigArmor = /* @__PURE__ */ base64armor('PGP SIGNATURE', 64, Stream, crc24); function validateDate(timestamp) { if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > 2 ** 46) throw new Error('invalid PGP key creation time: must be a valid UNIX timestamp'); } function getPublicPackets(edPriv, cvPriv, createdAt) { validateDate(createdAt); const edPub = bytesToNumberBE(concatBytes(Uint8Array.of(0x40), ed25519.getPublicKey(edPriv))); const edPubPacket = { created: createdAt, algo: { TAG: 'EdDSA', data: { curve: 'ed25519', pub: edPub } }, }; const cvPoint = x25519.scalarMultBase(cvPriv); const cvPub = bytesToNumberBE(concatBytes(Uint8Array.of(0x40), cvPoint)); const cvPubPacket = { created: createdAt, algo: { TAG: 'ECDH', data: { curve: 'curve25519', pub: cvPub, params: { hash: 'sha256', encryption: 'aes128' } }, }, }; const fingerprint = hex.encode(sha1(hashPubKey.encode({ pubKey: edPubPacket }))); const keyId = fingerprint.slice(-16); return { edPubPacket, fingerprint, keyId, cvPubPacket }; } function getCerts(edPriv, cvPriv, user, createdAt) { // key settings same as in PGP to avoid fingerprinting since they are part of public key const preferredEncryptionAlgorithms = ['aes256', 'aes192', 'aes128', 'tripledes']; const preferredHashAlgorithms = ['sha512', 'sha384', 'sha256', 'sha224', 'sha1']; const preferredCompressionAlgorithms = ['zlib', 'bzip2', 'zip']; const preferredAEADAlgorithms = ['OCB', 'EAX']; const { edPubPacket, fingerprint, keyId, cvPubPacket } = getPublicPackets(edPriv, cvPriv, createdAt); const edCert = signData({ type: 'certPositive', algo: 'EdDSA', hash: 'sha512', hashed: [ { TAG: 'issuerFingerprint', data: { fingerprint } }, { TAG: 'signatureCreationTime', data: createdAt }, { TAG: 'keyFlags', data: { sign: true, certify: true } }, { TAG: 'preferredEncryptionAlgorithms', data: preferredEncryptionAlgorithms }, { TAG: 'preferredAEADAlgorithms', data: preferredAEADAlgorithms }, { TAG: 'preferredHashAlgorithms', data: preferredHashAlgorithms }, { TAG: 'preferredCompressionAlgorithms', data: preferredCompressionAlgorithms }, { TAG: 'features', data: { aead: true, v5Keys: true, modDetect: true } }, { TAG: 'keyServerPreferences', data: { modDetect: true } }, ], }, [{ TAG: 'issuer', data: keyId }], { pubKey: { pubKey: edPubPacket }, user: { user } }, edPriv); const cvCert = signData({ type: 'subkeyBinding', algo: 'EdDSA', hash: 'sha512', hashed: [ { TAG: 'issuerFingerprint', data: { fingerprint } }, { TAG: 'signatureCreationTime', data: createdAt }, { TAG: 'keyFlags', data: { encrypt: true, encryptComm: true } }, ], }, [{ TAG: 'issuer', data: keyId }], { pubKey: { pubKey: edPubPacket }, subKey: { pubKey: cvPubPacket } }, edPriv); return { edPubPacket, fingerprint, keyId, cvPubPacket, cvCert, edCert }; } /** * Formats the armored public half of a deterministic OpenPGP keypair. * @param edPriv - Ed25519 signing private key. * @param cvPriv - Curve25519 encryption private key. * @param user - OpenPGP user ID string. * @param createdAt - Key creation time as UNIX timestamp in seconds. * @returns ASCII-armored public key block. * @throws If the supplied key material or timestamp cannot be encoded as OpenPGP packets. {@link Error} * @example * Build the public OpenPGP block from the signing key and its Curve25519 subkey. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { formatPublic } from 'micro-key-producer/pgp.js'; * import { ed25519 } from '@noble/curves/ed25519.js'; * const seed = randomBytes(32); * const cvPriv = ed25519.utils.getExtendedPublicKey(seed).head; * formatPublic(seed, cvPriv, 'alice@example.com', 0); * ``` */ export function formatPublic(edPriv, cvPriv, user, createdAt) { const { edPubPacket, cvPubPacket, edCert, cvCert } = getCerts(edPriv, cvPriv, user, createdAt); return pubArmor.encode([ { TAG: 'publicKey', data: edPubPacket }, { TAG: 'userId', data: user }, { TAG: 'signature', data: edCert }, { TAG: 'publicSubkey', data: cvPubPacket }, { TAG: 'signature', data: cvCert }, ]); } /** * Formats the armored private half of a deterministic OpenPGP keypair. * @param edPriv - Ed25519 signing private key. * @param cvPriv - Curve25519 encryption private key. * @param user - OpenPGP user ID string. * @param password - Optional secret-key passphrase. * @param createdAt - Key creation time as UNIX timestamp in seconds. * @param edSalt - Salt for the signing secret-key S2K envelope. * @param edIV - IV for the signing secret-key S2K envelope. * @param cvSalt - Salt for the encryption subkey S2K envelope. * @param cvIV - IV for the encryption subkey S2K envelope. * @returns ASCII-armored private key block. * @throws If the supplied key material, timestamp, or secret-key envelope parameters are invalid. {@link Error} * @example * Build the password-protected private key block and matching encryption subkey. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { formatPrivate } from 'micro-key-producer/pgp.js'; * import { ed25519 } from '@noble/curves/ed25519.js'; * const seed = randomBytes(32); * const cvPriv = ed25519.utils.getExtendedPublicKey(seed).head; * formatPrivate(seed, cvPriv, 'alice@example.com', 'password'); * ``` */ export function formatPrivate(edPriv, cvPriv, user, password, createdAt = 0, edSalt = randomBytes(8), edIV = randomBytes(16), cvSalt = randomBytes(8), cvIV = randomBytes(16)) { const { edPubPacket, cvPubPacket, edCert, cvCert } = getCerts(edPriv, cvPriv, user, createdAt); const edSecret = createPrivKey(edPubPacket, edPriv, password, edSalt, edIV); const cvPrivLE = P.U256BE.encode(P.U256LE.decode(cvPriv)); const cvSecret = createPrivKey(cvPubPacket, cvPrivLE, password, cvSalt, cvIV); return privArmor.encode([ { TAG: 'secretKey', data: edSecret }, { TAG: 'userId', data: user }, { TAG: 'signature', data: edCert }, { TAG: 'secretSubkey', data: cvSecret }, { TAG: 'signature', data: cvCert }, ]); } /** * Derives PGP key ID from the private key. * PGP key depends on its date of creation. * @param edPrivKey - Ed25519 signing private key. * @param createdAt - Key creation time as UNIX timestamp in seconds. * @returns Public packets plus fingerprint and key ID. * @throws If the key material or creation time cannot be encoded as OpenPGP packets. {@link Error} * @example * Recompute the OpenPGP fingerprint and key ID for an existing signing key. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeyId } from 'micro-key-producer/pgp.js'; * getKeyId(randomBytes(32)).keyId; * ``` */ export function getKeyId(edPrivKey, createdAt = 0) { const { head: cvPrivate } = ed25519.utils.getExtendedPublicKey(edPrivKey); return getPublicPackets(edPrivKey, cvPrivate, createdAt); } /** * Derives PGP private key, public key and fingerprint. * Uses S2K KDF, which means it's slow. Use `getKeyId` if you want to get key id in a fast way. * PGP key depends on its date of creation. * NOTE: gpg: warning: lower 3 bits of the secret key are not cleared * happens even for keys generated with GnuPG 2.3.6, because check looks at item as Opaque MPI, when it is just MPI. See {@link https://dev.gnupg.org/rGdbfb7f809b89cfe05bdacafdb91a2d485b9fe2e0 | the GnuPG bugtracker note}. * @param privKey - Ed25519 signing private key. * @param user - OpenPGP user ID string. * @param password - Optional secret-key passphrase. * @param createdAt - Key creation time as UNIX timestamp in seconds. * @returns Armored keypair plus fingerprint data. * @throws If the key material or creation time cannot be encoded as OpenPGP packets. {@link Error} * @example * Derive the armored OpenPGP keypair from one Ed25519 private key. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeys } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * getKeys(seed, 'alice@example.com').publicKey; * ``` */ export function getKeys(privKey, user, password, createdAt = 0) { const { head: cvPrivate } = ed25519.utils.getExtendedPublicKey(privKey); const { keyId, fingerprint } = getPublicPackets(privKey, cvPrivate, createdAt); const publicKey = formatPublic(privKey, cvPrivate, user, createdAt); // The slow part const privateKey = formatPrivate(privKey, cvPrivate, user, password, createdAt); return { keyId, fingerprint, privateKey, publicKey }; } /** * Default export for deterministic OpenPGP key derivation. * @param privKey - Ed25519 signing private key. * @param user - OpenPGP user ID string. * @param password - Optional secret-key passphrase. * @param createdAt - Key creation time as UNIX timestamp in seconds. * @returns Armored keypair plus fingerprint data. * @throws If the key material or creation time cannot be encoded as OpenPGP packets. {@link Error} * @example * Use the default export when you want the full armored OpenPGP bundle in one call. * ```ts * import getKeys from 'micro-key-producer/pgp.js'; * import { randomBytes } from '@noble/hashes/utils.js'; * const seed = randomBytes(32); * getKeys(seed, 'alice@example.com').publicKey; * ``` */ export default getKeys; // TODO: there should be two versions of this, one throws on duplication, one doesn't. Then we can apply this to all coders // here and make it easier to use, also per-tag type inference. Probably should be in micro-packed itself (pretty useful!) // Will be easier to use, but harder to debug. Still need to think about this. But will change exported coders API here. // const taggedDict = <T>(inner: P.CoderType<{ TAG: string; data: T }[]>) => { // return P.apply(inner, { // encode: (to) => { // if (!Array.isArray(to)) throw new Error('expected array'); // const res: Record<string, any> = {}; // for (const i of to) { // const { TAG, data } = i; // if (res.hasOwnProperty(TAG)) throw new Error('duplicate tag=' + TAG); // res[TAG] = data; // } // return res; // }, // decode: (from) => { // return Object.entries(from).map(([k, v]) => ({ TAG: k, data: v })); // }, // }); // }; function parseTags(to) { if (!Array.isArray(to)) throw new Error('expected array'); const res = {}; for (const i of to) { const { TAG, data } = i; if (res.hasOwnProperty(TAG)) throw new Error('duplicate tag=' + TAG); res[TAG] = data; } return res; } function detachedType(data) { if (!isBytes(data) && typeof data !== 'string') throw new Error('wrong data'); return typeof data === 'string' ? 'text' : 'binary'; } /** * Creates an armored detached OpenPGP signature. * @param privateKey - Ed25519 signing private key. * @param data - Binary or text payload to sign. * @param fingerprint - Full OpenPGP fingerprint of the signing key. * @param signedAt - Signature creation time as UNIX timestamp in seconds. * @returns ASCII-armored detached signature. * @throws If the detached payload cannot be encoded or signed as OpenPGP data. {@link Error} * @example * Create a detached signature you can send alongside the original text payload. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeyId, signDetached } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * const { fingerprint } = getKeyId(seed); * signDetached(seed, 'hello', fingerprint); * ``` */ export function signDetached(privateKey, data, fingerprint, signedAt = 0) { const dataType = detachedType(data); const keyId = fingerprint.slice(-16); const head = { version: undefined, type: dataType, algo: 'EdDSA', hash: 'sha512', hashed: [ { TAG: 'issuerFingerprint', data: { version: undefined, fingerprint } }, { TAG: 'signatureCreationTime', data: signedAt }, ], }; const unhashed = [{ TAG: 'issuer', data: keyId }]; const sig = signData(head, unhashed, data, privateKey); return sigArmor.encode([{ TAG: 'signature', data: sig }]); } /** * Verifies an armored detached OpenPGP signature with an Ed25519 public key. * @param publicKey - Ed25519 public key bytes. * @param signature - ASCII-armored detached signature. * @param data - Original binary or text payload. * @param fingerprint - Optional expected signer fingerprint. * @returns Whether the detached signature verifies. * @throws If the signature packet, payload type, or signer fingerprint is invalid. {@link Error} * @example * Verify the detached signature with the signer's Ed25519 public key and fingerprint. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeyId, signDetached, verifyDetached } from 'micro-key-producer/pgp.js'; * import { ed25519 } from '@noble/curves/ed25519.js'; * const privateKey = randomBytes(32); * const { fingerprint } = getKeyId(privateKey); * const signature = signDetached(privateKey, 'hello', fingerprint); * verifyDetached(ed25519.getPublicKey(privateKey), signature, 'hello', fingerprint); * ``` */ export function verifyDetached(publicKey, signature, data, fingerprint) { const sigPacket = sigArmor.decode(signature); // NOTE: in theory there can be multiple signatures inside! if (sigPacket.length !== 1 || sigPacket[0].TAG !== 'signature') throw new Error('wrong signature'); const sig = sigPacket[0].data; const dataType = detachedType(data); if (dataType !== sig.head.type) throw new Error('verifyDetached: wrong data type: ' + dataType + ', got:' + sig.head.type); if (fingerprint) { const hashed = parseTags(sig.head.hashed); const unhashed = parseTags(sig.unhashed); if (hashed.issuerFingerprint && hashed.issuerFingerprint.fingerprint !== fingerprint) throw new Error('wrong fingerprint'); if (unhashed.issuer && unhashed.issuer !== fingerprint.slice(-16)) throw new Error('wrong keyId'); } const { verified, hashPrefix } = verifyData(sig.head, data, sig.sig, publicKey); if (!equalBytes(hashPrefix, sig.hashPrefix)) return false; return verified; } /** * This is a basic parsing to extract enough information to signDetached signatures. * Supports keys generated by us or PGP (ed25519 only + default opts), doesn't extract ECDH (x25519) keys. * @param privateKey - ASCII-armored private key block. * @param getPassword - Optional callback used to fetch the secret-key passphrase. * @returns Parsed secret key bytes and identifying metadata. * @throws If the armored packet layout, password callback, or decoded key material is invalid. {@link Error} * @example * Parse an armored secret key back into raw key bytes and OpenPGP metadata. * ```ts * import { randomBytes } from '@noble/hashes/utils.js'; * import { getKeys, parsePrivateKey } from 'micro-key-producer/pgp.js'; * const seed = randomBytes(32); * const { privateKey } = getKeys(seed, 'alice@example.com'); * parsePrivateKey(privateKey).then(({ keyId }) => keyId); * ``` */ export async function parsePrivateKey(privateKey, // NOTE: we cannot just provide password as argument since private key can be unprotected getPassword) { const parsed = privArmor.decode(privateKey); const secretPacket = parsed.filter((i) => i.TAG === 'secretKey'); if (secretPacket.length !== 1) throw new Error('multiple or zero secret keys'); const secret = secretPacket[0].data; let password = ''; if (secret.type.TAG !== 'plain') { if (!getPassword) throw new Error('no getPassword callback provided'); // We don't know keyId at this point yet :( password = await getPassword(); // Ask user for password via UI? } const secretScalar = decodeSecretKey(password, secret); const secretBytes = numberToBytesBE(secretScalar, 32); const publicKey = ed25519.getPublicKey(secretBytes); const pubPGP = bytesToNumberBE(concatBytes(Uint8Array.of(0x40), publicKey)); if (secret.pub.algo.TAG !== 'EdDSA' || secret.pub.algo.data.curve !== 'ed25519') throw new Error('unknown key format'); if (pubPGP !== secret.pub.algo.data.pub) throw new Error('wrong publicKey, decoding failed'); const created = secret.pub.created; const { fingerprint, keyId } = getKeyId(secretBytes, created); // TODO: check if there is certPositive with valid fingerprint? // const signatures = parsed.filter((i) => i.TAG === 'signature').map((i) => i.data); return { privateKey: secretBytes, created, fingerprint, keyId, publicKey }; } //# sourceMappingURL=pgp.js.map