/*! 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, type CHash } from '@noble/hashes/utils.js';
import { hex, utf8 } from '@scure/base';
import * as P from 'micro-packed';
import { base64armor } from './utils.ts';

/** Byte-array alias used across the PGP helpers. */
export type Bytes = Uint8Array;
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)
/**
 * 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: P.CoderType<bigint> = /* @__PURE__ */ 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
/**
 * 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: P.CoderType<Uint8Array> = /* @__PURE__ */ 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 = /* @__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: P.CoderType<string> = /* @__PURE__ */ 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;
  },
});

/**
 * 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: P.CoderType<number> = /* @__PURE__ */ 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 = /* @__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: Record<string, CHash> = { 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: Record<string, number> = {
  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,
    })
  ),
});

/** Supported OpenPGP public-key packet algorithms. */
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;
    }>;
  };
}>;

/**
 * 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: P.CoderType<
  P.StructInput<{
    version: undefined;
    created: number;
    algo: PubKeyPacketAlgo;
  }>
> = /* @__PURE__ */ (() =>
  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 = /* @__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: 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;
      };
    }>;
  }>
> = /* @__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],
    }),
  }))();
type SecretKeyType = P.UnwrapCoder<typeof SecretKeyPacket>;

// 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,
});

type SignatureHeadType = P.UnwrapCoder<typeof SignatureHead>;

const SignaturePacket = /* @__PURE__ */ (() =>
  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 = /* @__PURE__ */ 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: Uint8Array = 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: Record<string, ReturnType<typeof createAesCfb>> = {
  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: 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 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: 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 = /* @__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: 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)) };
  },
});

/**
 * 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: P.CoderType<any[]> = /* @__PURE__ */ P.array(null, Packet);

// Key generation
const EDSIGN = /* @__PURE__ */ 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 verifyData(head: any, data: any, sig: P.UnwrapCoder<typeof EDSIGN>, publicKey: Bytes) {
  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: Bytes) {
  // 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: Bytes) {
    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: Bytes) {
    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: string, key: SecretKeyType): bigint {
  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 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 secretChecksumCoder.decode(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}`);
  // 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 } } as const;
  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: P.Coder<any[], string> = /* @__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: P.Coder<any[], string> = /* @__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: P.Coder<any[], string> = /* @__PURE__ */ base64armor(
  'PGP SIGNATURE',
  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: number) {
  validateDate(createdAt);
  const edPub = bytesToNumberBE(concatBytes(Uint8Array.of(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(Uint8Array.of(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: number) {
  // 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: Bytes,
  cvPriv: Bytes,
  user: string,
  createdAt: number
): 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 },
  ]);
}

/**
 * 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: 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.
 * @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: 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. 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: Bytes,
  user: string,
  password?: string,
  createdAt = 0
): {
  keyId: string;
  fingerprint: string; // full fingerprint
  privateKey: string;
  publicKey: string;
} {
  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<T>(to: { TAG: string; data: T }): Record<string, T> {
  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;
}

function detachedType(data: Bytes | string) {
  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: Bytes,
  data: Bytes | string,
  fingerprint: string,
  signedAt: number = 0
): string {
  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 as any, 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: Bytes,
  signature: string,
  data: Bytes | string,
  fingerprint?: string
): boolean {
  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: Record<string, any> = 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: string,
  // NOTE: we cannot just provide password as argument since private key can be unprotected
  getPassword?: () => Promise<string>
): Promise<any> {
  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 };
}
