micro-key-producer
Version:
Produces secure keys and passwords. Supports SSH, PGP, BLS, OTP, and many others
802 lines (743 loc) • 25.1 kB
text/typescript
/*! 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;