micro-key-producer
Version:
Produces secure passwords & keys for WebCrypto, SSH, PGP, SLIP10, OTP and many others
1,041 lines • 42.1 kB
JavaScript
/*! 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