UNPKG

micro-key-producer

Version:

Produces secure keys and passwords. Supports SSH, PGP, BLS, OTP, and many others

802 lines (743 loc) 25.1 kB
/*! micro-key-producer - MIT License (c) 2024 Paul Miller (paulmillr.com) */ import { cfb } from '@noble/ciphers/aes'; import { bytesToNumberBE, equalBytes, numberToHexUnpadded } from '@noble/curves/abstract/utils'; import { ed25519, x25519 } from '@noble/curves/ed25519'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { sha1 } from '@noble/hashes/sha1'; import { sha256 } from '@noble/hashes/sha256'; import { sha3_256 } from '@noble/hashes/sha3'; import { sha512 } from '@noble/hashes/sha512'; import { type CHash, concatBytes, randomBytes } from '@noble/hashes/utils'; import { hex, utf8 } from '@scure/base'; import * as P from 'micro-packed'; import { base64armor } from './utils.js'; export type Bytes = Uint8Array; // 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 function runAesCfb(keyLen: number, data: Bytes, key: Bytes, iv: Bytes, 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: number) { return { encrypt: (plaintext: Bytes, key: Bytes, iv: Bytes) => runAesCfb(len, plaintext, key, iv), decrypt: (ciphertext: Bytes, key: Bytes, iv: Bytes) => runAesCfb(len, ciphertext, key, iv, true), }; } // PGP Types // Multiprecision Integers [RFC4880](https://datatracker.ietf.org/doc/html/rfc4880) export const mpi: P.CoderType<bigint> = P.wrap({ encodeStream: (w: P.Writer, value: bigint) => { 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: P.Reader): bigint => 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 export const opaquempi: P.CoderType<Uint8Array> = P.wrap({ encodeStream: (w: P.Writer, value: Bytes) => { P.U16BE.encodeStream(w, value.length * 8); w.bytes(value); }, decodeStream: (r: P.Reader): Bytes => 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 = 2 ** 7; // mask for 8 bit const OID_NO_MSB = 2 ** 7 - 1; // mask for all bits except 8 export const oid: P.CoderType<string> = P.wrap({ encodeStream: (w: P.Writer, value: string) => { 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: P.Reader): string => { 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; }, }); export const PacketLen: P.CoderType<number> = P.wrap({ encodeStream: (w: P.Writer, value: number) => { 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: P.Reader): number => { 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 = P.magic(P.hex(1), '04'); // only version 4 is supported // Other (RSA/ElGamal/etc) is unsupported const pubKeyEnum = P.map(P.U8, { ECDH: 18, ECDSA: 19, EdDSA: 22, }); const ECEnum = P.map(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 = 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: Record<string, CHash> = { ripemd160, sha256, sha512, sha3_256, sha1 }; const EncryptionEnum = P.map(P.U8, { plaintext: 0, idea: 1, tripledes: 2, cast5: 3, blowfish: 4, aes128: 7, aes192: 8, aes256: 9, twofish: 10, }); const EncryptionKeySize: Record<string, number> = { plaintext: 0, aes128: 16, aes192: 24, aes256: 32, }; const CompressionEnum = P.map(P.U8, { uncompressed: 0, zip: 1, zlib: 2, bzip2: 3, }); // bis4880 const AEADEnum = P.map(P.U8, { None: 0, EAX: 1, OCB: 2, }); // https://datatracker.ietf.org/doc/html/rfc4880#section-3.7.1 const S2KEnum = P.map(P.U8, { simple: 0, salted: 1, iterated: 3 }); const S2K = P.tag(S2KEnum, { simple: P.struct({ hash: HashEnum }), salted: P.struct({ hash: HashEnum, salt: P.bytes(8) }), iterated: P.struct({ hash: HashEnum, salt: P.bytes(8), count: P.U8 }), }); // https://datatracker.ietf.org/doc/html/rfc6637#section-9 const ECDSAPub = P.struct({ curve: ECEnum, pub: mpi }); const ECDHPub = P.struct({ curve: ECEnum, pub: mpi, params: P.prefix( P.U8, P.struct({ magic: P.magic(P.hex(1), '01'), hash: HashEnum, encryption: EncryptionEnum, }) ), }); export type PubKeyPacketAlgo = P.Values<{ EdDSA: { TAG: 'EdDSA'; data: P.StructInput<{ curve: any; pub: any; }>; }; ECDH: { TAG: 'ECDH'; data: P.StructInput<{ curve: any; pub: any; params: any; }>; }; }>; export const PubKeyPacket: P.CoderType< P.StructInput<{ version: undefined; created: number; algo: PubKeyPacketAlgo; }> > = P.struct({ version: PGP_PACKET_VERSION, created: P.U32BE, algo: P.tag(pubKeyEnum, { EdDSA: ECDSAPub, ECDH: ECDHPub, }), }); type PubKeyType = P.UnwrapCoder<typeof PubKeyPacket>; type PacketData = P.StructInput<{ enc: any; S2K: any; iv: any; secret: any; }>; const PlainSecretKey = P.struct({ secret: P.bytes(null), }); const EncryptedSecretKey = 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: P.CoderType< P.StructInput<{ pub: P.StructInput<{ version: undefined; created: number; algo: PubKeyPacketAlgo; }>; type: P.Values<{ plain: { TAG: 'plain'; data: P.StructInput<{ secret: any; }>; }; encrypted: { TAG: 'encrypted'; data: PacketData; }; encrypted2: { TAG: 'encrypted2'; data: PacketData; }; }>; }> > = 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], }), }); type SecretKeyType = P.UnwrapCoder<typeof SecretKeyPacket>; // https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.1 const SigTypeEnum = 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 = 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 = P.prefix( PacketLen, P.tag(signatureSubpacket, { issuerFingerprint: P.struct({ version: PGP_PACKET_VERSION, fingerprint: P.hex(20) }), signatureCreationTime: P.U32BE, keyFlags: P.bitset([ '_r', 'shared', 'auth', 'split', 'encrypt', 'encryptComm', 'sign', 'certify', ]), preferredEncryptionAlgorithms: P.array(null, EncryptionEnum), preferredHashAlgorithms: P.array(null, HashEnum), preferredCompressionAlgorithms: P.array(null, CompressionEnum), preferredAEADAlgorithms: P.array(null, AEADEnum), features: P.bitset(['_r', '_r', '_r', '_r', '_r', 'v5Keys', 'aead', 'modDetect']), keyServerPreferences: P.bitset(['modDetect'], true), issuer: P.hex(8), primaryUserID: P.bool, }) ); const SignatureSubpackets = P.prefix(P.U16BE, P.array(null, SignatureSubpacket)); const SignatureHead = P.struct({ version: PGP_PACKET_VERSION, type: SigTypeEnum, algo: pubKeyEnum, hash: HashEnum, hashed: SignatureSubpackets, }); type SignatureHeadType = P.UnwrapCoder<typeof SignatureHead>; const SignaturePacket = P.struct({ head: SignatureHead, unhashed: SignatureSubpackets, hashPrefix: P.bytes(2), // 2: ec + dsa, 1 for rsa sig: P.array(null, mpi), }); type SignatureType = P.UnwrapCoder<typeof SignaturePacket>; const UserPacket = P.string(null); // PGP Functions const EXPBIAS6 = (count: number) => (16 + (count & 15)) << ((count >> 4) + 6); function deriveKey(hash: string, len: number, password: Bytes, salt?: Bytes, count?: number) { // Important: there is difference between zero and empty count count = count === undefined ? 0 : EXPBIAS6(count); const data = salt ? concatBytes(salt, password) : password; let out = new Uint8Array([]); 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: Record<string, ReturnType<typeof createAesCfb>> = { aes128: createAesCfb(128), aes192: createAesCfb(192), aes256: createAesCfb(256), }; // https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.4 const hashTail = new Uint8Array([0x04, 0xff]); const hashPubKey = P.struct({ magic: P.magic(P.hex(1), '99'), pubKey: P.prefix(P.U16BE, PubKeyPacket), }); const hashUser = P.struct({ magic: P.magic(P.hex(1), 'b4'), user: P.prefix(P.U32BE, UserPacket), }); const hashSelfCert = P.struct({ pubKey: hashPubKey, user: hashUser }); const hashSubKeyCert = P.struct({ pubKey: hashPubKey, subKey: hashPubKey }); function hashSignature(head: SignatureHeadType, data: any) { 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 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: Bytes) { 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: Record<string, any> = { 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 = 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 = P.wrap({ encodeStream: (w: P.Writer, value: any) => { 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: P.Reader): any => { 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)) }; }, }); export const Stream: P.CoderType<any[]> = P.array(null, Packet); // Key generation const EDSIGN = P.array(null, P.U256BE); function signData( head: SignatureHeadType, unhashed: any, data: any, privateKey: Bytes ): SignatureType { const hash = hashSignature(head, data); const hashPrefix = hash.subarray(0, 2); const sig = EDSIGN.decode(ed25519.sign(hash, privateKey)) as any; return { head, unhashed, hashPrefix, sig }; } function decodeSecretChecksum(secret: Bytes) { const [data, checksum] = [secret.slice(0, -2), P.U16BE.decode(secret.slice(-2))]; // Wow, third checksum algorithm in single spec! let ourChecksum = 0; for (let i = 0; i < data.length; i++) ourChecksum += data[i]; ourChecksum %= 65536; if (ourChecksum !== checksum) throw new Error('PGP.secretKey: wrong checksum for plain encoding'); return mpi.decode(data); } export function decodeSecretKey(password: string, key: SecretKeyType): bigint { if (key.type.TAG === 'plain') return decodeSecretChecksum(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 as any).salt, (data as any).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 decodeSecretChecksum(decryptedKey); return mpi.decode(decryptedKey); } function createPrivKey( pub: PubKeyType, key: Bytes, password: string, salt: Bytes, iv: Bytes, hash = 'sha1', count = 240, enc = 'aes128' ): SecretKeyType { const keyLen = EncryptionKeySize[enc]; if (keyLen === undefined) throw new Error(`PGP.secretKey: unknown encryption mode=${enc}`); 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 } } as const; return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } }; } export const pubArmor: P.Coder<any[], string> = base64armor( 'PGP PUBLIC KEY BLOCK', 64, Stream, crc24 ); export const privArmor: P.Coder<any[], string> = base64armor( 'PGP PRIVATE KEY BLOCK', 64, Stream, crc24 ); function validateDate(timestamp: number) { 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: Bytes, cvPriv: Bytes, createdAt = 0) { validateDate(createdAt); const edPub = bytesToNumberBE(concatBytes(new Uint8Array([0x40]), ed25519.getPublicKey(edPriv))); const edPubPacket = { created: createdAt, algo: { TAG: 'EdDSA', data: { curve: 'ed25519', pub: edPub } }, } as const; const cvPoint = x25519.scalarMultBase(cvPriv); const cvPub = bytesToNumberBE(concatBytes(new Uint8Array([0x40]), cvPoint)); const cvPubPacket = { created: createdAt, algo: { TAG: 'ECDH', data: { curve: 'curve25519', pub: cvPub, params: { hash: 'sha256', encryption: 'aes128' } }, }, } as const; const fingerprint = hex.encode(sha1(hashPubKey.encode({ pubKey: edPubPacket }))); const keyId = fingerprint.slice(-16); return { edPubPacket, fingerprint, keyId, cvPubPacket }; } function getCerts(edPriv: Bytes, cvPriv: Bytes, user: string, createdAt = 0) { // 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 }; } export function formatPublic(edPriv: Bytes, cvPriv: Bytes, user: string, createdAt = 0): string { 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 }, ]); } export function formatPrivate( edPriv: Bytes, cvPriv: Bytes, user: string, password: string, createdAt = 0, edSalt: Uint8Array = randomBytes(8), edIV: Uint8Array = randomBytes(16), cvSalt: Uint8Array = randomBytes(8), cvIV: Uint8Array = randomBytes(16) ): string { 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. */ export function getKeyId( edPrivKey: Bytes, createdAt = 0 ): { edPubPacket: { readonly created: number; readonly algo: { readonly TAG: 'EdDSA'; readonly data: { readonly curve: 'ed25519'; readonly pub: bigint; }; }; }; fingerprint: string; keyId: string; cvPubPacket: { readonly created: number; readonly algo: { readonly TAG: 'ECDH'; readonly data: { readonly curve: 'curve25519'; readonly pub: bigint; readonly params: { readonly hash: 'sha256'; readonly encryption: 'aes128'; }; }; }; }; } { 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: * https://dev.gnupg.org/rGdbfb7f809b89cfe05bdacafdb91a2d485b9fe2e0 */ export function getKeys( privKey: Bytes, user: string, password: string, createdAt = 0 ): { keyId: string; privateKey: string; publicKey: string; } { const { head: cvPrivate } = ed25519.utils.getExtendedPublicKey(privKey); const { keyId } = getPublicPackets(privKey, cvPrivate, createdAt); const publicKey = formatPublic(privKey, cvPrivate, user, createdAt); // The slow part const privateKey = formatPrivate(privKey, cvPrivate, user, password, createdAt); return { keyId, privateKey, publicKey }; } export default getKeys;