Skip to content

Commit

Permalink
Use typescript isolatedDeclarations.
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Jan 4, 2025
1 parent 4daa16f commit 03fbf2a
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 40 deletions.
25 changes: 19 additions & 6 deletions src/bls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function parentSKToLamportPK(parentSK: Uint8Array, index: number) {
* @param ikm - secret octet string
* @param keyInfo - additional key information
*/
export function hkdfModR(ikm: Uint8Array, keyInfo = new Uint8Array()) {
export function hkdfModR(ikm: Uint8Array, keyInfo: Uint8Array = new Uint8Array()): Uint8Array {
ikm = ensureBytes('IKM', ikm);
keyInfo = ensureBytes('key information', keyInfo);
let salt = utf8ToBytes('BLS-SIG-KEYGEN-SALT-');
Expand Down Expand Up @@ -109,7 +109,14 @@ export function deriveSeedTree(seed: Uint8Array, path: string): Uint8Array {

export const EIP2334_KEY_TYPES = ['withdrawal', 'signing'] as const;
export type EIP2334KeyType = (typeof EIP2334_KEY_TYPES)[number];
export function deriveEIP2334Key(seed: Uint8Array, type: EIP2334KeyType, index: number) {
export function deriveEIP2334Key(
seed: Uint8Array,
type: EIP2334KeyType,
index: number
): {
key: Uint8Array;
path: string;
} {
if (!isBytes(seed)) throw new Error('Valid seed expected');
if (!EIP2334_KEY_TYPES.includes(type)) throw new Error('Valid keystore type expected');
assertUint32(index);
Expand All @@ -133,7 +140,7 @@ export function deriveEIP2334Key(seed: Uint8Array, type: EIP2334KeyType, index:
* const derivedSigning = bls.deriveEIP2334SigningKey(withdrawal.key);
* deepStrictEqual(derivedSigning, signing.key);
*/
export function deriveEIP2334SigningKey(withdrawalKey: Uint8Array, index = 0) {
export function deriveEIP2334SigningKey(withdrawalKey: Uint8Array, index = 0): Uint8Array {
withdrawalKey = ensureBytes('withdrawal key', withdrawalKey, 32);
assertUint32(index);
return deriveChild(withdrawalKey, index);
Expand Down Expand Up @@ -262,7 +269,10 @@ function deriveEIP2335Key(password: string, salt: Uint8Array, kdf: KDFType): Uin
* @returns decrypted secret and optionally path
* @example decryptEIP2335Keystore(JSON.parse(keystoreString), 'my_password');
*/
export function decryptEIP2335Keystore<T extends KDFType>(store: Keystore<T>, password: string) {
export function decryptEIP2335Keystore<T extends KDFType>(
store: Keystore<T>,
password: string
): Uint8Array {
validateKeystore(store);
const c = store.crypto;
const checksumProvided = c.checksum.message;
Expand Down Expand Up @@ -381,7 +391,7 @@ export class EIP2335Keystore<T extends KDFType> {
/**
* Clean internal key material
*/
clean() {
clean(): void {
this.destroyed = true;
this.key.fill(0);
this.salt.fill(0);
Expand Down Expand Up @@ -417,4 +427,7 @@ export function createDerivedEIP2334Keystores<T extends KDFType>(
}

// Internal methods for test purposes only
export const _TEST = /* @__PURE__ */ { normalizePassword, deriveEIP2335Key };
export const _TEST: {
normalizePassword: typeof normalizePassword;
deriveEIP2335Key: typeof deriveEIP2335Key;
} = /* @__PURE__ */ { normalizePassword, deriveEIP2335Key };
4 changes: 2 additions & 2 deletions src/otp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function buildURL(opts: OTPOpts): string {
return `otpauth://totp/?secret=${sec}&interval=${int_}&digits=${opts.digits}&algorithm=${algo}`;
}

export function hotp(opts: OTPOpts, counter: number | bigint) {
export function hotp(opts: OTPOpts, counter: number | bigint): string {
const hash = { sha1, sha256, sha512 }[opts.algorithm];
if (!hash) throw new Error(`TOTP: unknown hash: ${opts.algorithm}`);
const mac = hmac(hash, opts.secret, U64BE.encode(BigInt(counter)));
Expand All @@ -59,6 +59,6 @@ export function hotp(opts: OTPOpts, counter: number | bigint) {
return num.toString().slice(-opts.digits).padStart(opts.digits, '0');
}

export function totp(opts: OTPOpts, ts = Date.now()) {
export function totp(opts: OTPOpts, ts: number = Date.now()): string {
return hotp(opts, Math.floor(ts / (opts.interval * 1000)));
}
29 changes: 25 additions & 4 deletions src/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ DATE.d = 24 * DATE.h;
DATE.mo = 30 * DATE.d;
DATE.y = 365 * DATE.mo;

export function formatDuration(dur: number) {
export function formatDuration(dur: number): string {
if (Number.isNaN(dur)) return 'never';
if (dur > DATE.y * 100) return 'centuries';
let parts = [];
Expand Down Expand Up @@ -135,6 +135,27 @@ function passwordScore(cardinality: bigint) {
return res;
}

export type PassEstimate = {
// Score/guesses based on zxcvbn, it is pretty bad model, but will be ok for now
score: string;
guesses: {
online_throttling: string;
online: string;
slow: string;
fast: string;
};
// Password is assumed salted.
// Non-salted passwords allow multi-target attacks which significantly reduces costs.
// Values taken from hashcat 6.1.1 on RTX 3080
// https://gist.github.com/Chick3nman/bb22b28ec4ddec0cb5f59df97c994db4
costs: {
luks: number;
filevault2: number;
macos: number;
pbkdf2: number;
};
};

/**
* Estimate attack price for a password.
* @returns `{ luks, filevault2, macos, pbkdf2 }`
Expand Down Expand Up @@ -218,20 +239,20 @@ class Mask {
.join('');
return { password, entropyLeft };
}
inverse({ password, entropyLeft }: ApplyResult) {
inverse({ password, entropyLeft }: ApplyResult): Uint8Array {
const values = zip(this.sets, password.split('')).map(([s, c]) => Array.from(s).indexOf(c));
const num = zip(this.sets, values).reduceRight(
(acc, [s, v]) => acc * BigInt(s.size) + BigInt(v),
0n
);
return numberToVarBytesBE(entropyLeft * this.cardinality + num);
}
estimate() {
estimate(): PassEstimate {
return estimateAttack(this.cardinality);
}
}

export const mask = (mask: string) => new Mask(mask);
export const mask = (mask: string): Mask => new Mask(mask);

/*
'Safari Keychain Secure Password'-like password:
Expand Down
138 changes: 121 additions & 17 deletions src/pgp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function createAesCfb(len: number) {

// PGP Types
// Multiprecision Integers [RFC4880](https://datatracker.ietf.org/doc/html/rfc4880)
export const mpi = P.wrap({
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++);
Expand All @@ -63,7 +63,7 @@ export const mpi = P.wrap({
// 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.wrap({
export const opaquempi: P.CoderType<Uint8Array> = P.wrap({
encodeStream: (w: P.Writer, value: Bytes) => {
P.U16BE.encodeStream(w, value.length * 8);
w.bytes(value);
Expand All @@ -76,7 +76,7 @@ export const opaquempi = P.wrap({
// 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.wrap({
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];
Expand Down Expand Up @@ -104,7 +104,7 @@ export const oid = P.wrap({
},
});

export const PacketLen = P.wrap({
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);
Expand Down Expand Up @@ -219,7 +219,31 @@ const ECDHPub = P.struct({
),
});

export const PubKeyPacket = P.struct({
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, {
Expand All @@ -228,6 +252,12 @@ export const PubKeyPacket = P.struct({
}),
});
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),
Expand All @@ -240,8 +270,33 @@ const EncryptedSecretKey = P.struct({
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.struct({
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],
Expand Down Expand Up @@ -481,7 +536,7 @@ const Packet = P.wrap({
},
});

export const Stream = P.array(null, Packet);
export const Stream: P.CoderType<any[]> = P.array(null, Packet);

// Key generation
const EDSIGN = P.array(null, P.U256BE);
Expand Down Expand Up @@ -553,8 +608,18 @@ function createPrivKey(
return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } };
}

export const pubArmor = base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24);
export const privArmor = base64armor('PGP PRIVATE KEY BLOCK', 64, Stream, crc24);
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');
Expand Down Expand Up @@ -633,7 +698,7 @@ function getCerts(edPriv: Bytes, cvPriv: Bytes, user: string, createdAt = 0) {
return { edPubPacket, fingerprint, keyId, cvPubPacket, cvCert, edCert };
}

export function formatPublic(edPriv: Bytes, cvPriv: Bytes, user: string, createdAt = 0) {
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 },
Expand All @@ -650,11 +715,11 @@ export function formatPrivate(
user: string,
password: string,
createdAt = 0,
edSalt = randomBytes(8),
edIV = randomBytes(16),
cvSalt = randomBytes(8),
cvIV = randomBytes(16)
) {
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));
Expand All @@ -672,7 +737,37 @@ export function formatPrivate(
* Derives PGP key ID from the private key.
* PGP key depends on its date of creation.
*/
export function getKeyId(edPrivKey: Bytes, createdAt = 0) {
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);
}
Expand All @@ -685,7 +780,16 @@ export function getKeyId(edPrivKey: Bytes, createdAt = 0) {
* 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) {
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);
Expand Down
2 changes: 1 addition & 1 deletion src/slip10.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, concatBytes, createView, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';

export const MASTER_SECRET = utf8ToBytes('ed25519 seed');
export const MASTER_SECRET: Uint8Array = utf8ToBytes('ed25519 seed');
export const HARDENED_OFFSET: number = 0x80000000;
const ZERO = new Uint8Array([0]);

Expand Down
Loading

0 comments on commit 03fbf2a

Please sign in to comment.