ed25519-keygen
Advanced tools
Comparing version 0.2.4 to 0.3.0
{ | ||
"name": "ed25519-keygen", | ||
"version": "0.2.4", | ||
"description": "Generate ed25519 keys deterministically for SSH, PGP (GPG) and TOR", | ||
"version": "0.3.0", | ||
"description": "Generate ed25519 keys for SSH, PGP (GPG), TOR and SLIP-0010 hdkey", | ||
"type": "module", | ||
@@ -13,2 +13,6 @@ "main": "index.js", | ||
"index.d.ts.map", | ||
"src/hdkey.ts", | ||
"hdkey.js", | ||
"hdkey.d.ts", | ||
"hdkey.d.ts.map", | ||
"src/pgp.ts", | ||
@@ -38,3 +42,3 @@ "pgp.js", | ||
"devDependencies": { | ||
"micro-should": "0.2.0", | ||
"micro-should": "0.4.0", | ||
"prettier": "2.6.2", | ||
@@ -48,2 +52,6 @@ "typescript": "4.7.3" | ||
}, | ||
"./hdkey": { | ||
"types": "./hdkey.d.ts", | ||
"default": "./hdkey.js" | ||
}, | ||
"./ssh": { | ||
@@ -50,0 +58,0 @@ "types": "./ssh.d.ts", |
101
pgp.d.ts
@@ -7,4 +7,81 @@ import * as P from 'micro-packed'; | ||
export declare const PacketLen: P.BytesCoderStream<number> & P.BytesCoder<number>; | ||
export declare const PubKeyPacket: P.CoderType<{ | ||
created: number; | ||
algo: { | ||
TAG: "ECDH"; | ||
data: { | ||
curve: string; | ||
pub: bigint; | ||
params: { | ||
hash: string; | ||
encryption: string; | ||
} & {}; | ||
} & {}; | ||
} | { | ||
TAG: "EdDSA"; | ||
data: { | ||
curve: string; | ||
pub: bigint; | ||
} & {}; | ||
}; | ||
} & {}>; | ||
declare const SecretKeyPacket: P.CoderType<{ | ||
secret: Uint8Array; | ||
type: { | ||
TAG: "encrypted"; | ||
data: { | ||
secret: Uint8Array; | ||
enc: string; | ||
S2K: { | ||
TAG: "simple"; | ||
data: { | ||
hash: string; | ||
} & {}; | ||
} | { | ||
TAG: "salted"; | ||
data: { | ||
hash: string; | ||
salt: Uint8Array; | ||
} & {}; | ||
} | { | ||
TAG: "iterated"; | ||
data: { | ||
hash: string; | ||
salt: Uint8Array; | ||
count: number; | ||
} & {}; | ||
}; | ||
iv: Uint8Array; | ||
} & {}; | ||
} | { | ||
TAG: "plain"; | ||
data: { | ||
secret: Uint8Array; | ||
} & {}; | ||
} | { | ||
TAG: "encrypted2"; | ||
data: { | ||
secret: Uint8Array; | ||
enc: string; | ||
S2K: { | ||
TAG: "simple"; | ||
data: { | ||
hash: string; | ||
} & {}; | ||
} | { | ||
TAG: "salted"; | ||
data: { | ||
hash: string; | ||
salt: Uint8Array; | ||
} & {}; | ||
} | { | ||
TAG: "iterated"; | ||
data: { | ||
hash: string; | ||
salt: Uint8Array; | ||
count: number; | ||
} & {}; | ||
}; | ||
iv: Uint8Array; | ||
} & {}; | ||
}; | ||
pub: { | ||
@@ -30,24 +107,2 @@ created: number; | ||
} & {}; | ||
s2k_type: number; | ||
enc: string; | ||
S2K: { | ||
TAG: "simple"; | ||
data: { | ||
hash: string; | ||
} & {}; | ||
} | { | ||
TAG: "salted"; | ||
data: { | ||
hash: string; | ||
salt: Uint8Array; | ||
} & {}; | ||
} | { | ||
TAG: "iterated"; | ||
data: { | ||
hash: string; | ||
salt: Uint8Array; | ||
count: number; | ||
} & {}; | ||
}; | ||
iv: Uint8Array; | ||
} & {}>; | ||
@@ -54,0 +109,0 @@ declare type SecretKeyType = P.UnwrapCoder<typeof SecretKeyPacket>; |
46
pgp.js
@@ -225,3 +225,3 @@ import * as ed25519 from '@noble/ed25519'; | ||
}); | ||
const PubKeyPacket = P.struct({ | ||
export const PubKeyPacket = P.struct({ | ||
version: PGP_PACKET_VERSION, | ||
@@ -234,10 +234,19 @@ created: P.U32BE, | ||
}); | ||
const SecretKeyPacket = P.struct({ | ||
pub: PubKeyPacket, | ||
s2k_type: P.U8, | ||
const PlainSecretKey = P.struct({ | ||
secret: P.bytes(null), | ||
}); | ||
const EncryptedSecretKey = P.struct({ | ||
enc: EncryptionEnum, | ||
S2K: S2K, | ||
S2K, | ||
iv: P.bytes(16), | ||
secret: P.bytes(null), | ||
}); | ||
const SecretKeyPacket = P.struct({ | ||
pub: PubKeyPacket, | ||
type: P.mappedTag(P.U8, { | ||
plain: [0x00, PlainSecretKey], | ||
encrypted: [254, EncryptedSecretKey], | ||
encrypted2: [255, EncryptedSecretKey], | ||
}), | ||
}); | ||
const SigTypeEnum = P.map(P.U8, { | ||
@@ -449,11 +458,22 @@ binary: 0x00, | ||
} | ||
function decodeSecretChecksum(secret) { | ||
const [data, checksum] = [secret.slice(0, -2), P.U16BE.decode(secret.slice(-2))]; | ||
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 async function decodeSecretKey(password, key) { | ||
if (key.s2k_type !== 254) | ||
throw new Error(`PGP.secretKey Unsupported s2k_type=${key.s2k_type}`); | ||
const data = key.S2K.data; | ||
const keyLen = EncryptionKeySize[key.enc]; | ||
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=${key.enc}`); | ||
throw new Error(`PGP.secretKey: unknown encryption mode=${keyData.enc}`); | ||
const encKey = deriveKey(data.hash, keyLen, utf8.decode(password), data.salt, data.count); | ||
const decrypted = await Encryption[key.enc].decrypt(key.secret, encKey, key.iv); | ||
const decrypted = await Encryption[keyData.enc].decrypt(keyData.secret, encKey, keyData.iv); | ||
const decryptedKey = decrypted.subarray(0, -20); | ||
@@ -465,2 +485,4 @@ const checksum = Hash.sha1(decryptedKey); | ||
throw new Error(`PGP.secretKey unsupported publicKey algorithm: ${key.pub.algo.TAG}`); | ||
if (key.type.TAG === 'encrypted2') | ||
return decodeSecretChecksum(decryptedKey); | ||
return mpi.decode(decryptedKey); | ||
@@ -477,3 +499,3 @@ } | ||
const S2K = { TAG: 'iterated', data: { hash, salt, count } }; | ||
return { pub, s2k_type: 254, enc, S2K, iv, secret }; | ||
return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } }; | ||
} | ||
@@ -480,0 +502,0 @@ export const pubArmor = P.base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24); |
# ed25519-keygen | ||
Generate ed25519 keys deterministically for SSH, PGP (GPG) and TOR. | ||
Generate ed25519 keys for SSH, PGP (GPG), TOR and SLIP-0010 hdkey. | ||
Does not use CLI utils, everything is done programmatically in pure JS. | ||
Generation is deterministic and done in pure javascript, without CLI tools. Uses audited [@noble/ed25519](https://github.com/paulmillr/noble-ed25519) under the hood. | ||
Includes SLIP-0010 / BIP32 HDKey implementation, sponsored by the Kin Foundation for [Kinetic](https://github.com/kin-labs/kinetic). | ||
## Usage | ||
@@ -11,3 +13,3 @@ | ||
The package exports four modules: | ||
The package exports five modules: | ||
@@ -17,2 +19,3 @@ - [`ed25519-keygen/ssh`](#sshseed-username) for SSH key generation | ||
- [`ed25519-keygen/tor`](#torseed) for TOR onion addresses | ||
- [`ed25519-keygen/hdkey`](#hdkey) for [SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md)/[BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) HDKey | ||
- [`ed25519-keygen/utils`](#randombyteslength) for cryptographically secure random number generator (CSPRNG) | ||
@@ -148,2 +151,52 @@ | ||
## hdkey | ||
SLIP-0010 hierarchical deterministic (HD) wallets for implementation. Based on audited 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 | ||
```ts | ||
import { HDKey } from 'ed25519-keygen/hdkey'; | ||
const hdkey1 = HDKey.fromMasterSeed(seed); | ||
// props | ||
[hdkey1.depth, hdkey1.index, hdkey1.chainCode]; | ||
console.log(hdkey2.privateKey, hdkey2.publicKey); | ||
console.log(hdkey3.derive("m/0/2147483647'/1'")); | ||
const sig = hdkey3.sign(hash); | ||
hdkey3.verify(hash, sig); | ||
``` | ||
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 | ||
class HDKey { | ||
public static HARDENED_OFFSET: number; | ||
public static fromMasterSeed(seed: Uint8Array | string): HDKey; | ||
readonly depth: number = 0; | ||
readonly index: number = 0; | ||
readonly chainCode: Uint8Array | null = null; | ||
readonly parentFingerprint: number = 0; | ||
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 = false): HDKey; | ||
deriveChild(index: number): HDKey; | ||
sign(hash: Uint8Array): Uint8Array; | ||
verify(hash: Uint8Array, signature: Uint8Array): boolean; | ||
} | ||
``` | ||
### `randomBytes(length)` | ||
@@ -150,0 +203,0 @@ |
@@ -263,3 +263,3 @@ import * as ed25519 from '@noble/ed25519'; | ||
const PubKeyPacket = P.struct({ | ||
export const PubKeyPacket = P.struct({ | ||
version: PGP_PACKET_VERSION, | ||
@@ -274,9 +274,9 @@ created: P.U32BE, | ||
// NOTE: SecretKey is specific packet type as per spec. For user facing API we using 'privateKey' | ||
const SecretKeyPacket = P.struct({ | ||
pub: PubKeyPacket, | ||
s2k_type: P.U8, | ||
// only for 254 & 255 | ||
const PlainSecretKey = P.struct({ | ||
secret: P.bytes(null), | ||
}); | ||
const EncryptedSecretKey = P.struct({ | ||
enc: EncryptionEnum, | ||
S2K: S2K, | ||
S2K, | ||
// IV as blocksize of algo. For AES it is 16 bytes, others is not supported | ||
@@ -286,2 +286,13 @@ iv: P.bytes(16), | ||
}); | ||
// NOTE: SecretKey is specific packet type as per spec. For user facing API we using 'privateKey' | ||
const SecretKeyPacket = 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>; | ||
@@ -536,7 +547,19 @@ | ||
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 async function decodeSecretKey(password: string, key: SecretKeyType) { | ||
if (key.s2k_type !== 254) throw new Error(`PGP.secretKey Unsupported s2k_type=${key.s2k_type}`); | ||
const data = key.S2K.data; | ||
const keyLen = EncryptionKeySize[key.enc]; | ||
if (keyLen === undefined) throw new Error(`PGP.secretKey: unknown encryption mode=${key.enc}`); | ||
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( | ||
@@ -549,3 +572,3 @@ data.hash, | ||
); | ||
const decrypted = await Encryption[key.enc].decrypt(key.secret, encKey, key.iv); | ||
const decrypted = await Encryption[keyData.enc].decrypt(keyData.secret, encKey, keyData.iv); | ||
const decryptedKey = decrypted.subarray(0, -20); | ||
@@ -558,2 +581,3 @@ const checksum = Hash.sha1(decryptedKey); | ||
// Decoded as generic MPI, not as OpaqueMPI | ||
if (key.type.TAG === 'encrypted2') return decodeSecretChecksum(decryptedKey); | ||
return mpi.decode(decryptedKey); | ||
@@ -579,3 +603,3 @@ } | ||
const S2K = { TAG: 'iterated', data: { hash, salt, count } } as const; | ||
return { pub, s2k_type: 254, enc, S2K, iv, secret }; | ||
return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } }; | ||
} | ||
@@ -582,0 +606,0 @@ |
Sorry, the diff of this file is not supported yet
84345
27
1942
207