# micro-key-producer

Produces secure passwords & keys for WebCrypto, SSH, PGP, SLIP10, OTP and many others.

- 🔓 Secure: audited [noble](https://paulmillr.com/noble/) cryptography
- 🔻 Tree-shakeable: unused code is excluded from your builds
- 🎲 Create deterministic (known) or random keys
- 🔑 SSH, PGP, TOR, IPNS, SLIP10, BLS12-381 ETH keys
- X509 certificates
- 💾 WebCrypto-compatible JWK, DER, PKCS#8, SPKI converter
- 🔗 gpgkp(1): Sign git commits without gnupg
- 📟 Generate secure passwords & OTP 2FA codes

Used in: [terminal7 WebRTC terminal multiplexer](https://github.com/tuzig/terminal7).

## Usage

> `npm install micro-key-producer`

> `jsr add jsr:@paulmillr/micro-key-producer`

```ts
import ssh from 'micro-key-producer/ssh.js';
import pgp from 'micro-key-producer/pgp.js';
import slip10 from 'micro-key-producer/slip10.js';
import * as webconv from 'micro-key-producer/convert.js';
import ipns from 'micro-key-producer/ipns.js';
import tor from 'micro-key-producer/tor.js';
import { createDerivedEIP2334Keystores } from 'micro-key-producer/bls.js';
import { secureMask } from 'micro-key-producer/password.js';
import { hotp, totp } from 'micro-key-producer/otp.js';

import { randomBytes } from 'micro-key-producer/utils.js';
```

- [Usage](#usage)
  - [Key generation: deterministic vs random seeds](#key-generation-deterministic-vs-random-seeds)
  - [ssh: ed25519 keys](#ssh-ed25519-keys)
  - [pgp: ed25519 keys](#pgp-ed25519-keys)
    - [gpgkp(1): Sign git commits without gnupg](#gpgkp1-sign-git-commits-without-gnupg)
  - [slip10: bip32-like ed25519 keys](#slip10-bip32-like-ed25519-keys)
  - [convert: key converter for JWK, DER, PKCS, SPKI](#convert-key-converter-for-jwk-der-pkcs-spki)
  - [tor: keys and addresses](#tor-keys-and-addresses)
  - [ipns: addresses](#ipns-addresses)
  - [bls: keys for ETH validators](#bls-keys-for-eth-validators)
  - [password: secure passwords with masks](#password-secure-passwords-with-masks)
  - [otp: 2FA HOTP and TOTP codes](#otp-2fa-hotp-and-totp-codes)
- [Internals](#internals)
  - [PGP key generation](#pgp-key-generation)
  - [Password generation](#password-generation)
    - [Bruteforce estimation and ZXCVBN score](#bruteforce-estimation-and-zxcvbn-score)
    - [Mask control characters](#mask-control-characters)
    - [Design rationale](#design-rationale)
    - [What do we want from passwords?](#what-do-we-want-from-passwords)
  - [SLIP10 API](#slip10-api)
- [License](#license)

### Key generation: deterministic vs random seeds

Every method takes a seed (key), from which the formatted result is produced.

A seed can be **deterministic** (a.k.a. known - it will always produce the same result), or **random**.

> `npm install @scure/bip39` for mnemonic-based examples

```js
// known: (deterministic) Uses known mnemonic (handled in separate package)
import { mnemonicToSeedSync } from '@scure/bip39';
const mnemonic = 'letter advice cage absurd amount doctor acoustic avoid letter advice cage above';
const knownSeed = mnemonicToSeedSync(mnemonic, '');

// random: Uses system's CSPRNG to produce new random seed
import { randomBytes } from 'micro-key-producer/utils.js';
const randSeed = randomBytes(32);
```

### ssh: ed25519 keys

```js
import ssh from 'micro-key-producer/ssh.js';
import { randomBytes } from 'micro-key-producer/utils.js';

const seed = randomBytes(32);
const key = ssh(seed, 'user@example.com');
console.log(key.fingerprint, key.privateKey, key.publicKey);
// SHA256:3M832z6j5R6mQh4TTzVG5KVs2Ibvy...
// -----BEGIN OPENSSH PRIVATE KEY----- ...
// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
```

### pgp: ed25519 keys

```js
import pgp, { getKeyId } from 'micro-key-producer/pgp.js';
import { randomBytes } from 'micro-key-producer/utils.js';

const seed = randomBytes(32);
const email = 'user@example.com';
const pass = 'password';
const createdAt = Math.floor(Date.now() / 1000); // optional; unix timestamp

const keyId = getKeyId(seed);
const key = pgp(seed, email, pass, createdAt);
console.log(key.fingerprint, key.privateKey, key.publicKey);
// ca88e2a8afd9cdb8
// -----BEGIN PGP PRIVATE KEY BLOCK-----...
// -----BEGIN PGP PUBLIC KEY BLOCK-----...
```

The PGP (GPG) keys conform to
[RFC 4880](https://datatracker.ietf.org/doc/html/rfc4880) &
[RFC 6637](https://datatracker.ietf.org/doc/html/rfc6637). Only ed25519 algorithm is currently supported.

#### gpgkp(1): Sign git commits without gnupg

`gpgkp` binary is installed by the package. You can use it to sign and verify git commits.

Enable PGP commit signing:

```sh
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global user.signingkey 125679DA6845B812
```

Set gpgkp from key-producer as preferred signing program:

```sh
git config --global gpg.program $(which gpgkp)
```

```sh
git config --global user.name "Alice"
git config --global user.email "alice@example.com"
```

### slip10: bip32-like ed25519 keys

```js
import slip10 from 'micro-key-producer/slip10.js';
import { sha256 } from '@noble/hashes/sha2.js';
import { randomBytes } from 'micro-key-producer/utils.js';

const seed = randomBytes(32);
const root = slip10.fromMasterSeed(seed);
const account0 = root.derive("m/0'");
const signing = root.derive("m/0/2147483647'/1'", true);
const msgHash = sha256(new TextEncoder().encode('hello slip10'));

// props
[root.depth, root.index, root.chainCode];
[account0.privateKey, account0.publicKey];
const sig = signing.sign(msgHash);
signing.verify(msgHash, sig);
```

SLIP10 (ed25519 BIP32) HDKey implementation has been funded by the Kin Foundation for
[Kinetic](https://github.com/kin-labs/kinetic).


### convert: key converter for JWK, DER, PKCS, SPKI

```ts
import { p256 } from '@noble/curves/nist.js';
import { p256_der, p256_jwk, p256_jwk_ecdh } from 'micro-key-producer/convert.js';
const { publicKey, secretKey } = p256.keygen();
console.log(
  secretKey,
  p256_der.secretKey.encode(secretKey),
  p256_der.secretKey.decode(p256_der.secretKey.encode(secretKey)),
  p256_jwk.secretKey.encode(secretKey),
  p256_jwk_ecdh.secretKey.encode(secretKey)
)
```

The module allows to convert between "raw" noble-curves format and WebCrypto formats (JWK, DER, PKCS, SPKI).

### tor: keys and addresses

```js
import tor from 'micro-key-producer/tor.js';
import { randomBytes } from 'micro-key-producer/utils.js';
const seed = randomBytes(32);
const key = tor(seed);
console.log(key.privateKey, key.publicKey);
// ED25519-V3:EOl78M2gA...
// rx724x3oambzxr46pkbd... .onion
```

### ipns: addresses

```js
import ipns from 'micro-key-producer/ipns.js';
import { randomBytes } from 'micro-key-producer/utils.js';
const seed = randomBytes(32);
const k = ipns(seed);
console.log(k.privateKey, k.publicKey, k.base16, k.base32, k.base36, k.contenthash);
// 0x080112400681d6420abb1b...
// 0x017200240801122012c829...
// ipns://f0172002408011220...
// ipns://bafzaajaiaejcaewi...
// ipns://k51qzi5uqu5dgnfwb...
// 0xe501017200240801122012...
```

### bls: EIP-2333 keys for ETH validators

> `npm install @scure/bip39`

```js
import { mnemonicToSeedSync } from '@scure/bip39';
import { createDerivedEIP2334Keystores } from 'micro-key-producer/bls.js';

const password = 'my_password';
const mnemonic = 'letter advice cage absurd amount doctor acoustic avoid letter advice cage above';
const keyType = 'signing'; // or 'withdrawal'
const indexes = [0, 1, 2, 3]; // create 4 keys

const keystores = createDerivedEIP2334Keystores(
  password,
  'scrypt',
  mnemonicToSeedSync(mnemonic, ''),
  keyType,
  indexes
);
```

Conforms to EIP-2333 / EIP-2334 / EIP-2335. Online demo: [eip2333-tool](https://iancoleman.io/eip2333/)

### password: secure passwords with masks

```js
import { mask, secureMask } from 'micro-key-producer/password.js';
import { randomBytes } from '@noble/hashes/utils.js';

const seed = randomBytes(32);
const pass = secureMask.apply(seed).password;
// wivfi1-Zykrap-fohcij, will change on each run
// secureMask is format from iOS keychain, see "Detailed API" section
```

Supports iOS / macOS Safari Secure Password from Keychain. Optional zxcvbn score for password bruteforce estimation

### otp: 2FA HOTP and TOTP codes

```js
import { hotp, totp, parse } from 'micro-key-producer/otp.js';
hotp(parse('ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS'), 0n); // 549419
totp(parse('ZYTYYE5FOAGW5ML7LRWUL4WTZLNJAMZS'), 0); // 549419
```

Conforms to [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238).

### x509: certificates

```js
import * as x509 from 'micro-key-producer/x509.js';
```

## Internals

### PGP key generation

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).

```js
import * as pgp from 'micro-key-producer/pgp.js';
import { randomBytes } from 'micro-key-producer/utils.js';
const pseed = randomBytes(32);
pgp.getKeyId(pseed); // fast
const pkeys = pgp.getKeys(pseed, 'user@example.com', 'password');
console.log(pkeys.keyId);
console.log(pkeys.privateKey);
console.log(pkeys.publicKey);

// Also, you can explore existing keys internal structure
console.log(pgp.pubArmor.decode(pkeys.publicKey));
const privDecoded = pgp.privArmor.decode(pkeys.privateKey);
console.log(privDecoded);
// And receive raw private keys as bigint
console.log({
  ed25519: pgp.decodeSecretKey('password', privDecoded[0].data),
  cv25519: pgp.decodeSecretKey('password', privDecoded[3].data),
});
```

### Password generation

#### Bruteforce estimation and ZXCVBN score

```js
import { secureMask, mask } from 'micro-key-producer/password.js';
console.log(secureMask.estimate());

// Output
// {
//   score: 'somewhat guessable', // ZXCVBN Score
//   // Guess times
//   guesses: {
//     online_throttling: '1y 115mo', // Throttled online attack
//     online: '1mo 10d', // Online attack
//     // Offline attack (salte, slow hash function like bcrypt, scrypt, PBKDF2, argon, etc)
//     slow: '57min 36sec',
//     fast: '0 sec' // Offline attack
//   },
//   // Estimated attack costs (in $)
//   costs: {
//     luks: 1.536122841572242, // LUKS (Linux FDE)
//     filevault2: 0.2308740987992559, // FileVault 2 (macOS FDE)
//     macos: 0.03341598798410283, // MaccOS v10.8+ passwords
//     pbkdf2: 0.011138662661367609 // PBKDF2 (PBKDF2-HMAC-SHA256)
//   }
// }
```

#### Mask control characters

| Mask | Description                        | Example       |
| ---- | ---------------------------------- | ------------- |
| 1    | digits                             | 4, 7, 5, 0    |
| @    | symbols                            | !, @, %, ^    |
| v    | vowels                             | a, e, i       |
| c    | consonant                          | b, c, d       |
| a    | letter (vowel or consonant)        | a, b, e, c    |
| V    | uppercase vowel                    | A, E, I       |
| C    | uppercase consonant                | B, C, D       |
| A    | uppercase letter                   | A, B, E, C    |
| l    | lower and upper case letters       | A, b, C       |
| n    | same as 'l', but also digits       | A, 1, b, 2, C |
| \*   | same as 'n', but also symbols      | A, 1, !, b, @ |
| s    | syllable (same as 'cv')            | ca, re, do    |
| S    | Capitalized syllable (same as 'Cv) | Ca, Ti, Je    |
|      | All other characters used as is    |               |

Examples:

- Mask: `Cvccvc-cvccvc-cvccv1` will generate `Mavmuq-xadgys-poqsa5`
- Mask `@Ss-ss-ss` will generate: `*Tavy-qyjy-vemo`

#### Design rationale

Most strict password rules (so password will be accepted everywhere):

- at least one upper-case character
- at least one lower-case character
- at least one symbol
- at least one digit
- length greater or equal to 8
  These rules don't significantly increase password entropy (most humans will use mask like 'Aaaaaa1@' or any other popular mask),
  but they means that we cannot simple use mask like `********`, since it can generate passwords which won't satisfy these rules.

#### What do we want from passwords?

- **_length_**: entering 32 character password for FDE via IPMI java applet on remote server is pretty painful.
  -> 12-16 probably ok, anything with more characters has chance to be truncated by service.
- **_readability_**: entering '!#%!$#Y^&\*#%@#!!1' from air-gapped pc is hard.
- **_entropy_**:
  - 32 bit is likely to be brutforced via network
  - 64 bit: 22 days && 1.6k$ at 4x V100: https://blog.trailofbits.com/2019/11/27/64-bits-ought-to-be-enough-for-anybody/
    but it is simple loop, if there is something like pbkdf before password, it will significantly slowdown everything
  - 80 bits is probably outside of budget for most attackers (btc hash rate) even if there is major speedup for specific algorithm
  - For websites and services we don't care much about entropy, since passwords are unique and there is no re-usage,
    however for FDE / server password entropy is pretty important
- no fancy and unique mask by default: we don't want to fingeprint users
- any mask will leak eventually (even if user choices personal mask, there will be password leaks from websites),
  so we cannot calculate entropy by `******` mask, we need to calculate entropy for specific mask (which is smaller).
- Password generator should be reversible, that way we can easily proof entropy/strength of password.

### SLIP10 API

SLIP-0010 hierarchical deterministic (HD) wallets for implementation. Based on code from
[scure-bip32](https://github.com/paulmillr/scure-bip32). Check out
[scure-bip39](https://github.com/paulmillr/scure-bip39) if you also need mnemonic phrases.

- SLIP-0010 publicKey is 33 bytes (see
  [this issue](https://github.com/satoshilabs/slips/issues/1251)), if you want 32-byte publicKey,
  use `.publicKeyRaw` getter
- SLIP-0010 vectors fingerprint is actually `parentFingerprint`
- SLIP-0010 doesn't allow deriving non-hardened keys for Ed25519, however some other libraries treat
  non-hardened keys (`m/0/1`) as hardened (`m/0'/1'`). If you want this behaviour, there is a flag
  `forceHardened` in `derive` method

Note: `chainCode` property is essentially a private part of a secret "master" key, it should be
guarded from unauthorized access.

The full API is:

```ts
declare class HDKey {
  public static HARDENED_OFFSET: number;
  public static fromMasterSeed(seed: Uint8Array | string): HDKey;

  readonly depth: number;
  readonly index: number;
  readonly chainCode: Uint8Array | null;
  readonly parentFingerprint: number;
  public readonly privateKey: Uint8Array;

  get fingerprint(): number;
  get fingerprintHex(): string;
  get parentFingerprintHex(): string;
  get pubKeyHash(): Uint8Array;
  get publicKey(): Uint8Array;
  get publicKeyRaw(): Uint8Array;

  derive(path: string, forceHardened?: boolean): HDKey;
  deriveChild(index: number): HDKey;
  sign(hash: Uint8Array): Uint8Array;
  verify(hash: Uint8Array, signature: Uint8Array): boolean;
}
```

## License

MIT (c) Paul Miller [(https://paulmillr.com)](https://paulmillr.com), see LICENSE file.
