Add comprehensive installation and setup documentation

- Add GETTING_STARTED.md with quick start guide and development modes
- Add INSTALL.sh automated installation script
- Add INSTALLATION_CHECKLIST.md, INSTALLATION_SUCCESS.md, and INSTALLATION_SUMMARY.md
- Add QUICK_REFERENCE.md for common commands
- Add SETUP_GUIDE.md with detailed setup instructions
- Update README.md with improved project overview
- Add did-wallet app dependencies and node_modules
This commit is contained in:
Dorian
2026-01-27 17:18:21 +00:00
parent a81f655133
commit 0d073fa89e
22658 changed files with 4494151 additions and 6 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+258
View File
@@ -0,0 +1,258 @@
# ed25519-keygen
Generate ed25519 keys for SSH, PGP (GPG), TOR, IPNS and SLIP-0010 hdkey.
- Pure JS, no CLI tools are involved
- Can generate both deterministic and random keys
- Uses [noble-curves](https://github.com/paulmillr/noble-curves) under the hood
Includes SLIP-0010 (ed BIP32) HDKey implementation, funded by the Kin Foundation for
[Kinetic](https://github.com/kin-labs/kinetic). For the apps made with the library, check out:
[terminal7 WebRTC terminal multiplexer](https://github.com/tuzig/terminal7)
## Usage
> npm install ed25519-keygen
The package exports six modules:
- [`ed25519-keygen/ssh`](#sshseed-username) for SSH key generation
- [`ed25519-keygen/pgp`](#pgpseed-user-password) for
[RFC 4880](https://datatracker.ietf.org/doc/html/rfc4880) +
[RFC 6637](https://datatracker.ietf.org/doc/html/rfc6637)
- [`ed25519-keygen/tor`](#torseed) for TOR onion addresses
- [`ed25519-keygen/ipns`](#ipnsseed) for IPNS 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)
Use it in the following way:
```ts
import ssh from 'ed25519-keygen/ssh';
import pgp from 'ed25519-keygen/pgp';
import tor from 'ed25519-keygen/tor';
import { randomBytes } from 'ed25519-keygen/utils';
```
## `ssh(seed, username)`
- `seed: Uint8Array`
- `username: string`
- Returns
`{ fingerprint: string, privateKey: string, publicKey: string, publicKeyBytes: Uint8Array }`
```js
import ssh from 'ed25519-keygen/ssh';
import { randomBytes } from 'ed25519-keygen/utils';
const sseed = randomBytes(32);
const skeys = await ssh(sseed, 'user@example.com');
console.log(skeys.fingerprint);
console.log(skeys.privateKey);
console.log(skeys.publicKey);
/*
SHA256:3M832z6j5R6mQh4TTzVG5KVs2IbvythcS6VPiEixMJg
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACB7IzMcWzDbGACJFPmt8hDZGedH6W1w0SGuY1Ut+oIlxQAAAJh8wUpUfMFK
VAAAAAtzc2gtZWQyNTUxOQAAACB7IzMcWzDbGACJFPmt8hDZGedH6W1w0SGuY1Ut+oIlxQ
AAAEBPTJHsreF9Losr930Yt/8DseFi66G7vK8QF/Kd8fcRlXsjMxxbMNsYAIkU+a3yENkZ
50fpbXDRIa5jVS36giXFAAAAEHVzZXJAZXhhbXBsZS5jb20BAgMEBQ==
-----END OPENSSH PRIVATE KEY-----
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHsjMxxbMNsYAIkU+a3yENkZ50fpbXDRIa5jVS36giXF user@example.com
*/
```
## `pgp(seed, user, password)`
- `seed: Uint8Array`
- `user: string`
- `password: string`
- `createdAt: number` - (default: 0) timestamp corresponding to key creation time
- Returns `{ keyId: string, privateKey: string, publicKey: string, publicKeyBytes: Uint8Array }`
Creates keys compatible with GPG. GPG is a commonly known utility that supports PGP protocol.
Quirks:
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 (~725ms on Apple M1), 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 'ed25519-keygen/pgp';
import { randomBytes } from 'ed25519-keygen/utils';
const pseed = randomBytes(32);
const pkeys = await pgp.getKeys(pseed, 'user@example.com', 'password');
console.log(pkeys.keyId);
console.log(pkeys.privateKey);
console.log(pkeys.publicKey);
/*
ca88e2a8afd9cdb8
-----BEGIN PGP PRIVATE KEY BLOCK-----
lIYEAAAAABYJKwYBBAHaRw8BAQdA0TSxOgyxDIuJh0afj457vpf7IZJsnyVu+HG2
k/v1F0P+BwMCpkzMdFodxiHwgVurmhm72ikz5FqdF8WJEBy0VC8ovbXkNz9oCi31
grwUafRgb874q0n99Q6Kh1cDMwMNF6vjQvgusaJQtvy75Y0pkNEnKrQQdXNlckBl
eGFtcGxlLmNvbYiUBBMWCgA8FiEELCHHdWxwPnwIbwzfyojiqK/ZzbgFAgAAAAAC
GwMFCwkIBwIDIgIBBhUKCQgLAgQWAgMBAh4HAheAAAoJEMqI4qiv2c24nyABALvn
+XR0T4AeohBNL+h88o2tgPazB1GtKo1FhMb8cpaDAQCRr8Ml3Ow5ijijFBQ0aqG4
1D43SIinNvQFD59o85YfBZyLBAAAAAASCisGAQQBl1UBBQEBB0C8acmhByJtlAZo
7T2lVQa0iCo0RBm/CgMJKO+3/NaHGgMBCAf+BwMCJsLfz4M3/KrwyBBBRu8MTvjq
pY5FjFcJGoPRhYHX+/ZATZf4cRrA0LX3zDi1nudO1f755Q4ALWjPXNXMMBkmKjHJ
p5WaAFm7xMdxEvXYaIh4BBgWCgAgFiEELCHHdWxwPnwIbwzfyojiqK/ZzbgFAgAA
AAACGwwACgkQyojiqK/ZzbikkgEAod4RMOLsVngAH/WFBWQi+Ee5hZ4nXsfasDsT
hNEnaqcBAI5h/ss8SU/gOmx/uiGJTCpp8VRAac+VbeU5XU//aLwA
=oOli
-----END PGP PRIVATE KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEAAAAABYJKwYBBAHaRw8BAQdA0TSxOgyxDIuJh0afj457vpf7IZJsnyVu+HG2
k/v1F0O0EHVzZXJAZXhhbXBsZS5jb22IlAQTFgoAPBYhBCwhx3VscD58CG8M38qI
4qiv2c24BQIAAAAAAhsDBQsJCAcCAyICAQYVCgkICwIEFgIDAQIeBwIXgAAKCRDK
iOKor9nNuJ8gAQC75/l0dE+AHqIQTS/ofPKNrYD2swdRrSqNRYTG/HKWgwEAka/D
JdzsOYo4oxQUNGqhuNQ+N0iIpzb0BQ+faPOWHwW4OAQAAAAAEgorBgEEAZdVAQUB
AQdAvGnJoQcibZQGaO09pVUGtIgqNEQZvwoDCSjvt/zWhxoDAQgHiHgEGBYKACAW
IQQsIcd1bHA+fAhvDN/KiOKor9nNuAUCAAAAAAIbDAAKCRDKiOKor9nNuKSSAQCh
3hEw4uxWeAAf9YUFZCL4R7mFnidex9qwOxOE0SdqpwEAjmH+yzxJT+A6bH+6IYlM
KmnxVEBpz5Vt5TldT/9ovAA=
=4hZe
-----END PGP PUBLIC KEY BLOCK-----
*/
// Also, you can explore existing keys internal structure
console.log(await pgp.pubArmor.decode(keys.publicKey));
const privDecoded = await pgp.privArmor.decode(keys.privateKey);
console.log(privDecoded);
// And receive raw private keys as bigint
console.log({
ed25519: await pgp.decodeSecretKey('password', privDecoded[0].data),
cv25519: await pgp.decodeSecretKey('password', privDecoded[3].data),
});
```
## `tor(seed)`
Generates TOR addresses.
- `seed: Uint8Array`
- Returns `{ privateKey: string, publicKey: string, publicKeyBytes: Uint8Array }`
```js
import tor from 'ed25519-keygen/tor';
import { randomBytes } from 'ed25519-keygen/utils';
const tseed = randomBytes(32);
const tkeys = await tor(tseed);
console.log(tkeys.privateKey);
console.log(tkeys.publicKey);
/*
ED25519-V3:EOl78M2gARYOyp4BDltfzxSR3dA/LLTXZLb2imgOwFuYC5ISIUxsQ42ywzHaxvc03mahmaLziuyN0+f8EhM+4w==
rx724x3oambzxr46pkbdckdqyut5x5lhsneru3uditf4nuyuf4uou6qd.onion
*/
```
## `ipns(seed)`
Generates IPNS addresses.
- `seed: Uint8Array`
- Returns
`{ privateKey: string, publicKey: string, base36: string, base32: string, base16: string, contenthash: string}`
```js
import ipns from 'ed25519-keygen/ipns';
import { randomBytes } from 'ed25519-keygen/utils';
const iseed = randomBytes(32);
const ikeys = await ipns(iseed);
console.log(ikeys.privateKey);
console.log(ikeys.publicKey);
console.log(ikeys.base16);
console.log(ikeys.base32);
console.log(ikeys.base36);
console.log(ikeys.contenthash);
/*
0x080112400681d6420abb1ba47acd5c03c8e5ee84185a2673576b262e234e50c46d86f59712c8299ec2c51dffbbcb4f9fccadcee1424cb237e9b30d3cd72d47c18103689d
0x017200240801122012c8299ec2c51dffbbcb4f9fccadcee1424cb237e9b30d3cd72d47c18103689d
ipns://f017200240801122012c8299ec2c51dffbbcb4f9fccadcee1424cb237e9b30d3cd72d47c18103689d
ipns://bafzaajaiaejcaewifgpmfri57654wt47zsw45ykcjszdp2ntbu6nolkhygaqg2e5
ipns://k51qzi5uqu5dgnfwbc46une4upw1vc9hxznymyeykmg6rev1513yrnbyrwmmql
0xe501017200240801122012c8299ec2c51dffbbcb4f9fccadcee1424cb237e9b30d3cd72d47c18103689d
*/
```
## hdkey
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
```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;
}
```
## utils
```ts
import { randomBytes } from 'ed25519-keygen/utils';
const key = randomBytes(32);
```
CSPRNG for secure generation of random Uint8Array. Utilizes webcrypto under the hood.
## License
MIT (c) Paul Miller [(https://paulmillr.com)](https://paulmillr.com), see LICENSE file.
+31
View File
@@ -0,0 +1,31 @@
export declare const MASTER_SECRET: Uint8Array;
export declare const HARDENED_OFFSET: number;
type Hex = Uint8Array | string;
interface HDKeyOpt {
depth?: number;
index?: number;
parentFingerprint?: number;
chainCode: Uint8Array;
privateKey: Uint8Array;
}
export declare class HDKey {
get publicKeyRaw(): Uint8Array;
get publicKey(): Uint8Array;
get pubHash(): Uint8Array;
get fingerprint(): number;
get fingerprintHex(): string;
get parentFingerprintHex(): string;
static fromMasterSeed(seed: Hex): HDKey;
readonly depth: number;
readonly index: number;
readonly chainCode: Uint8Array;
readonly parentFingerprint: number;
readonly privateKey: Uint8Array;
constructor(opt: HDKeyOpt);
derive(path: string, forceHardened?: boolean): HDKey;
deriveChild(index: number): HDKey;
sign(message: Hex): Uint8Array;
verify(message: Hex, signature: Hex): boolean;
}
export {};
//# sourceMappingURL=hdkey.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"hdkey.d.ts","sourceRoot":"","sources":["src/hdkey.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,aAAa,YAA8B,CAAC;AACzD,eAAO,MAAM,eAAe,EAAE,MAAmB,CAAC;AAGlD,KAAK,GAAG,GAAG,UAAU,GAAG,MAAM,CAAC;AAkB/B,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,qBAAa,KAAK;IAChB,IAAI,YAAY,IAAI,UAAU,CAE7B;IACD,IAAI,SAAS,IAAI,UAAU,CAE1B;IACD,IAAI,OAAO,IAAI,UAAU,CAExB;IACD,IAAI,WAAW,IAAI,MAAM,CAExB;IACD,IAAI,cAAc,IAAI,MAAM,CAE3B;IACD,IAAI,oBAAoB,IAAI,MAAM,CAEjC;IAED,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,GAAG,KAAK;IAcvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAK;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAK;IAC3B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAK;IACvC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;gBAEpB,GAAG,EAAE,QAAQ;IAgBzB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,UAAQ,GAAG,KAAK;IAkBlD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK;IAejC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU;IAI9B,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,OAAO;CAI9C"}
+119
View File
@@ -0,0 +1,119 @@
/*! micro-ed25519-hdkey - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { ed25519 } from '@noble/curves/ed25519';
import { hmac } from '@noble/hashes/hmac';
import { ripemd160 } from '@noble/hashes/ripemd160';
import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, createView, hexToBytes, utf8ToBytes, bytesToHex } from '@noble/hashes/utils';
import { bytes as assertBytes } from '@noble/hashes/_assert';
export const MASTER_SECRET = utf8ToBytes('ed25519 seed');
export const HARDENED_OFFSET = 0x80000000;
const ZERO = new Uint8Array([0]);
function ensureBytes(b, ...lengths) {
if (typeof b === 'string')
b = hexToBytes(b);
assertBytes(b, ...lengths);
return b;
}
const hash160 = (data) => ripemd160(sha256(data));
const fromU32 = (data) => createView(data).getUint32(0, false);
const toU32 = (n) => {
if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) {
throw new Error(`Invalid number=${n}. Should be from 0 to 2 ** 32 - 1`);
}
const buf = new Uint8Array(4);
createView(buf).setUint32(0, n, false);
return buf;
};
export class HDKey {
get publicKeyRaw() {
return ed25519.getPublicKey(this.privateKey);
}
get publicKey() {
return concatBytes(ZERO, this.publicKeyRaw);
}
get pubHash() {
return hash160(this.publicKey);
}
get fingerprint() {
return fromU32(this.pubHash);
}
get fingerprintHex() {
return bytesToHex(toU32(this.fingerprint));
}
get parentFingerprintHex() {
return bytesToHex(toU32(this.parentFingerprint));
}
static fromMasterSeed(seed) {
seed = ensureBytes(seed);
if (8 * seed.length < 128 || 8 * seed.length > 512) {
throw new Error(`HDKey: wrong seed length=${seed.length}. Should be between 128 and 512 bits; 256 bits is advised)`);
}
const I = hmac(sha512, MASTER_SECRET, seed);
return new HDKey({
privateKey: I.slice(0, 32),
chainCode: I.slice(32),
});
}
constructor(opt) {
this.depth = 0;
this.index = 0;
this.parentFingerprint = 0;
if (!opt || typeof opt !== 'object')
throw new Error('HDKey.constructor must not be called directly');
assertBytes(opt.privateKey, 32);
assertBytes(opt.chainCode, 32);
this.depth = opt.depth || 0;
this.index = opt.index || 0;
this.parentFingerprint = opt.parentFingerprint || 0;
if (!this.depth) {
if (this.parentFingerprint || this.index)
throw new Error('HDKey: zero depth with non-zero index/parent fingerprint');
}
this.chainCode = opt.chainCode;
this.privateKey = opt.privateKey;
}
derive(path, forceHardened = false) {
if (!/^[mM]'?/.test(path))
throw new Error('Path must start with "m" or "M"');
if (/^[mM]'?$/.test(path))
return this;
const parts = path.replace(/^[mM]'?\//, '').split('/');
// tslint:disable-next-line
let child = this;
for (const c of parts) {
const m = /^(\d+)('?)$/.exec(c);
if (!m || m.length !== 3)
throw new Error(`Invalid child index: ${c}`);
let idx = +m[1];
if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET)
throw new Error('Invalid index');
// hardened key
if (forceHardened || m[2] === "'")
idx += HARDENED_OFFSET;
child = child.deriveChild(idx);
}
return child;
}
deriveChild(index) {
if (index < HARDENED_OFFSET)
throw new Error(`Non-hardened child derivation not possible for Ed25519 (index=${index})`);
// Hardened child: 0x00 || ser256(kpar) || ser32(index)
const data = concatBytes(ZERO, this.privateKey, toU32(index));
const I = hmac(sha512, this.chainCode, data);
return new HDKey({
chainCode: I.slice(32),
depth: this.depth + 1,
parentFingerprint: this.fingerprint,
index,
privateKey: I.slice(0, 32),
});
}
sign(message) {
return ed25519.sign(message, this.privateKey);
}
verify(message, signature) {
signature = ensureBytes(signature, 64);
return ed25519.verify(signature, message, this.publicKeyRaw);
}
}
+1
View File
@@ -0,0 +1 @@
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
throw new Error('The module has no entry-point: consult README for usage');
+12
View File
@@ -0,0 +1,12 @@
export declare function formatPublicKey(pubBytes: Uint8Array): string;
export declare function parseAddress(address: string): Uint8Array;
export declare function getKeys(seed: Uint8Array): Promise<{
publicKey: string;
privateKey: string;
base36: string;
base32: string;
base16: string;
contenthash: string;
}>;
export default getKeys;
//# sourceMappingURL=ipns.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ipns.d.ts","sourceRoot":"","sources":["src/ipns.ts"],"names":[],"mappings":"AAYA,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,UAEnD;AAKD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAoBxD;AAGD,wBAAsB,OAAO,CAAC,IAAI,EAAE,UAAU;;;;;;;GAsB7C;AAED,eAAe,OAAO,CAAC"}
+57
View File
@@ -0,0 +1,57 @@
import { ed25519 } from '@noble/curves/ed25519';
import { concatBytes } from '@noble/hashes/utils';
import { hex, base32, utils } from '@scure/base';
const base36 = utils.chain(utils.radix(36), utils.alphabet('0123456789abcdefghijklmnopqrstuvwxyz'), utils.padding(0), utils.join(''));
// Formats IPNS public key in bytes array format to 'ipns://k...' string format
export function formatPublicKey(pubBytes) {
return `ipns://k${base36.encode(pubBytes)}`;
}
// Takes an IPNS pubkey (address) string as input and returns bytes array of the key
// Supports various formats ('ipns://k', 'ipns://b', 'ipns://f')
// Handles decoding and validation of the key before returning pubkey bytes
export function parseAddress(address) {
address = address.toLowerCase();
if (address.startsWith('ipns://'))
address = address.slice(7);
let hexKey;
if (address.startsWith('k')) {
// Decode base-36 pubkey (after removing 'k' prefix) and encode it as a hex string
hexKey = hex.encode(base36.decode(address.slice(1)));
}
else if (address.startsWith('b')) {
// Decode base-32 pubkey (after removing 'b' prefix) and encode it as a hex string
hexKey = hex.encode(base32.decode(address.slice(1).toUpperCase()));
}
else if (address.startsWith('f')) {
hexKey = address.slice(1);
}
else
throw new Error('Unsupported Base-X Format'); // Throw error if pubkey format is not supported
// Check if hexKey has expected prefix '0172002408011220' and length of 80
if (hexKey.startsWith('0172002408011220') && hexKey.length === 80) {
return hex.decode(hexKey);
}
// Throw error if IPNS key prefix is invalid
throw new Error('Invalid IPNS Key Prefix: ' + hexKey);
}
// Generates an ed25519 pubkey from a seed and converts it to several IPNS pubkey formats
export async function getKeys(seed) {
//? privKey "seed" should be checked for <ed25519.curve.n?
if (seed.length != 32)
throw new TypeError('Seed must be 32 bytes in length');
// Generate ed25519 public key from seed
const pubKey = await ed25519.getPublicKey(seed);
// Create public key bytes by concatenating prefix bytes and pubKey
const pubKeyBytes = concatBytes(new Uint8Array([0x01, 0x72, 0x00, 0x24, 0x08, 0x01, 0x12, 0x20]), pubKey);
const hexKey = hex.encode(pubKeyBytes).toLowerCase();
// Return different representations of the keys
return {
publicKey: `0x${hexKey}`,
privateKey: `0x${hex.encode(concatBytes(new Uint8Array([0x08, 0x01, 0x12, 0x40]), seed, pubKey))}`,
base36: `ipns://k${base36.encode(pubKeyBytes)}`,
base32: `ipns://b${base32.encode(pubKeyBytes).toLowerCase()}`,
base16: `ipns://f${hexKey}`,
contenthash: `0xe501${hexKey}`,
};
}
export default getKeys;
+106
View File
@@ -0,0 +1,106 @@
{
"name": "ed25519-keygen",
"version": "0.4.11",
"description": "Generate ed25519 keys for SSH, PGP (GPG), TOR, IPNS and SLIP-0010 hdkey",
"type": "module",
"main": "index.js",
"module": "index.js",
"files": [
"index.js",
"index.d.ts",
"index.d.ts.map",
"ipns.js",
"ipns.d.ts",
"ipns.d.ts.map",
"hdkey.js",
"hdkey.d.ts",
"hdkey.d.ts.map",
"pgp.js",
"pgp.d.ts",
"pgp.d.ts.map",
"ssh.js",
"ssh.d.ts",
"ssh.d.ts.map",
"tor.js",
"tor.d.ts",
"tor.d.ts.map",
"utils.js",
"utils.d.ts",
"utils.d.ts.map",
"src/index.ts",
"src/ipns.ts",
"src/hdkey.ts",
"src/pgp.ts",
"src/ssh.ts",
"src/tor.ts",
"src/utils.ts"
],
"dependencies": {
"@noble/curves": "~1.3.0",
"@noble/hashes": "~1.3.3",
"@scure/base": "~1.1.5",
"micro-packed": "~0.5.2"
},
"devDependencies": {
"micro-should": "0.4.0",
"prettier": "3.1.1",
"typescript": "5.3.2"
},
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
},
"./ipns": {
"types": "./ipns.d.ts",
"default": "./ipns.js"
},
"./hdkey": {
"types": "./hdkey.d.ts",
"default": "./hdkey.js"
},
"./ssh": {
"types": "./ssh.d.ts",
"default": "./ssh.js"
},
"./pgp": {
"types": "./pgp.d.ts",
"default": "./pgp.js"
},
"./tor": {
"types": "./tor.d.ts",
"default": "./tor.js"
},
"./utils": {
"types": "./utils.d.ts",
"default": "./utils.js"
}
},
"scripts": {
"build": "tsc",
"lint": "prettier --check src",
"format": "prettier --write 'src/**/*.{js,ts}' 'test/*.js'",
"test": "node test/index.js"
},
"author": "Paul Miller (https://paulmillr.com)",
"license": "MIT",
"homepage": "https://github.com/paulmillr/ed25519-keygen",
"repository": {
"type": "git",
"url": "git+https://github.com/paulmillr/ed25519-keygen.git"
},
"keywords": [
"ed25519",
"PGP",
"GPG",
"RFC 4880",
"RFC 6637",
"SSH",
"TOR",
"onion",
"key generation",
"ec",
"elliptic"
],
"funding": "https://paulmillr.com/funding/"
}
+74
View File
@@ -0,0 +1,74 @@
import * as P from 'micro-packed';
export type Bytes = Uint8Array;
export declare const mpi: P.BytesCoderStream<bigint> & P.BytesCoder<bigint>;
export declare const opaquempi: P.BytesCoderStream<Uint8Array> & P.BytesCoder<Uint8Array>;
export declare const oid: P.BytesCoderStream<string> & P.BytesCoder<string>;
export declare const PacketLen: P.BytesCoderStream<number> & P.BytesCoder<number>;
export declare const PubKeyPacket: P.CoderType<P.StructInput<{
version: undefined;
created: number;
algo: P.Values<{
EdDSA: {
TAG: "EdDSA";
data: P.StructInput<{
curve: any;
pub: any;
}>;
};
ECDH: {
TAG: "ECDH";
data: P.StructInput<{
curve: any;
pub: any;
params: any;
}>;
};
}>;
}>>;
declare const SecretKeyPacket: P.CoderType<P.StructInput<{
pub: P.StructInput<{
version: any;
created: any;
algo: any;
}>;
type: P.Values<{
plain: {
TAG: "plain";
data: P.StructInput<{
secret: any;
}>;
};
encrypted: {
TAG: "encrypted";
data: P.StructInput<{
enc: any;
S2K: any;
iv: any;
secret: any;
}>;
};
encrypted2: {
TAG: "encrypted2";
data: P.StructInput<{
enc: any;
S2K: any;
iv: any;
secret: any;
}>;
};
}>;
}>>;
type SecretKeyType = P.UnwrapCoder<typeof SecretKeyPacket>;
export declare const Stream: P.CoderType<any[]>;
export declare function decodeSecretKey(password: string, key: SecretKeyType): Promise<bigint>;
export declare const pubArmor: P.Coder<any[], string>;
export declare const privArmor: P.Coder<any[], string>;
export declare function formatPublic(edPriv: Bytes, cvPriv: Bytes, user: string, created?: number): Promise<string>;
export declare function formatPrivate(edPriv: Bytes, cvPriv: Bytes, user: string, password: string, created?: number, edSalt?: Uint8Array, edIV?: Uint8Array, cvSalt?: Uint8Array, cvIV?: Uint8Array): Promise<string>;
export declare function getKeys(privKey: Bytes, user: string, password: string, created?: number): Promise<{
keyId: string;
privateKey: string;
publicKey: string;
}>;
export default getKeys;
//# sourceMappingURL=pgp.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"pgp.d.ts","sourceRoot":"","sources":["src/pgp.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAElC,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC;AAoD/B,eAAO,MAAM,GAAG,mDASd,CAAC;AAOH,eAAO,MAAM,SAAS,2DAMpB,CAAC;AAOH,eAAO,MAAM,GAAG,mDA0Bd,CAAC;AAEH,eAAO,MAAM,SAAS,mDAqBpB,CAAC;AA8FH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;GAOvB,CAAC;AAeH,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GASnB,CAAC;AACH,KAAK,aAAa,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,eAAe,CAAC,CAAC;AAyO3D,eAAO,MAAM,MAAM,oBAAwB,CAAC;AA0B5C,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,mBAwBzE;AAsBD,eAAO,MAAM,QAAQ,wBAA2D,CAAC;AACjF,eAAO,MAAM,SAAS,wBAA4D,CAAC;AA4EnF,wBAAsB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,SAAI,mBAczF;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,KAAK,EACb,MAAM,EAAE,KAAK,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,SAAI,EACX,MAAM,aAAiB,EACvB,IAAI,aAAkB,EACtB,MAAM,aAAiB,EACvB,IAAI,aAAkB,mBAkBvB;AAOD,wBAAsB,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,SAAI;;;;GAOxF;AAED,eAAe,OAAO,CAAC"}
+607
View File
@@ -0,0 +1,607 @@
import { ed25519, x25519 } from '@noble/curves/ed25519';
import { bytesToNumberBE, equalBytes, numberToHexUnpadded } from '@noble/curves/abstract/utils';
import { crypto } from '@noble/hashes/crypto';
import { ripemd160 } from '@noble/hashes/ripemd160';
import { sha1 } from '@noble/hashes/sha1';
import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import { sha3_256 } from '@noble/hashes/sha3';
import { concatBytes, randomBytes } from '@noble/hashes/utils';
import { utf8, hex } from '@scure/base';
import * as P from 'micro-packed';
// 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
// Safari supports AES_CFB via webCrypto, but chromium/firefox do not.
// Test page: https://diafygi.github.io/webcrypto-examples/
const BLOCK_LEN = 16;
const IV = new Uint8Array(BLOCK_LEN);
async function runAesBlock(msg, key) {
if (key.length !== 16 && key.length !== 32)
throw new Error('Invalid key length');
if (!crypto)
throw new Error('crypto.subtle must be defined');
const mode = { name: `AES-CBC`, length: key.length * 8 };
const wKey = await crypto.subtle.importKey('raw', key, mode, true, ['encrypt']);
const cipher = await crypto.subtle.encrypt({ name: `aes-cbc`, iv: IV, counter: IV, length: 64 }, wKey, msg);
return new Uint8Array(cipher).subarray(0, 16);
}
async function runAesCfb(keyLen, data, key, iv, decrypt = false) {
if (keyLen !== key.length * 8)
throw new Error('AesCfbProcess: wrong key length');
if (iv.length !== 16)
throw new Error('AesCfbProcess: wrong IV');
const blocks = [];
let prevBlock = iv;
for (let i = 0; i < data.length; i += 16) {
const curBlock = data.subarray(i, i + 16);
const enc = await runAesBlock(prevBlock, key);
const outBlock = curBlock.slice();
for (let j = 0; j < outBlock.length; j++)
outBlock[j] ^= enc[j];
blocks.push(outBlock);
prevBlock = decrypt ? curBlock : outBlock;
}
return concatBytes(...blocks);
}
function createAesCfb(len) {
return {
encrypt: (plaintext, key, iv) => runAesCfb(len, plaintext, key, iv),
decrypt: (ciphertext, key, iv) => runAesCfb(len, ciphertext, key, iv, true),
};
}
// PGP Types
// Multiprecision Integers [RFC4880](https://datatracker.ietf.org/doc/html/rfc4880)
export const mpi = P.wrap({
encodeStream: (w, value) => {
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) => 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.wrap({
encodeStream: (w, value) => {
P.U16BE.encodeStream(w, value.length * 8);
w.bytes(value);
},
decodeStream: (r) => 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.wrap({
encodeStream: (w, value) => {
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) => {
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.wrap({
encodeStream: (w, value) => {
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) => {
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 = { 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 = {
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 const PubKeyPacket = P.struct({
version: PGP_PACKET_VERSION,
created: P.U32BE,
algo: P.tag(pubKeyEnum, {
EdDSA: ECDSAPub,
ECDH: ECDHPub,
}),
});
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.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],
}),
});
// 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,
});
const SignaturePacket = P.struct({
head: SignatureHead,
unhashed: SignatureSubpackets,
hashPrefix: P.bytes(2),
// 2: ec + dsa, 1 for rsa
sig: P.array(null, mpi),
});
const UserPacket = P.string(null);
// PGP Functions
const EXPBIAS6 = (count) => (16 + (count & 15)) << ((count >> 4) + 6);
function deriveKey(hash, len, password, salt, count) {
// 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 = {
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, data) {
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();
}
const getFingerprint = (pubKey) => hex.encode(sha1(hashPubKey.encode({ pubKey })));
const getKeyId = (fp) => fp.slice(-16);
// https://datatracker.ietf.org/doc/html/rfc4880#section-6.1
function crc24(data) {
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 = {
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, value) => {
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) => {
const { tag, lenType } = PacketHead.decodeStream(r);
const packetLen = lenType !== 3 ? [P.U8, P.U16BE, P.U32BE][lenType].decodeStream(r) : r.data.length - r.pos;
return { TAG: tag, data: PacketTags[tag].decode(r.bytes(packetLen)) };
},
});
export const Stream = P.array(null, Packet);
// Key generation
const EDSIGN = P.array(null, P.U256BE);
async function signData(head, unhashed, data, privateKey) {
const hash = hashSignature(head, data);
const hashPrefix = hash.subarray(0, 2);
const sig = EDSIGN.decode(await ed25519.sign(hash, privateKey));
return { head, unhashed, hashPrefix, sig };
}
function decodeSecretChecksum(secret) {
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, key) {
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.salt, data.count);
const decrypted = await 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);
}
async function createPrivKey(pub, key, password, salt, iv, hash = 'sha1', count = 240, enc = 'aes128') {
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 = await Encryption[enc].encrypt(secretClear, encKey, iv);
const S2K = { TAG: 'iterated', data: { hash, salt, count } };
return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } };
}
export const pubArmor = P.base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24);
export const privArmor = P.base64armor('PGP PRIVATE KEY BLOCK', 64, Stream, crc24);
async function getPublicPackets(edPriv, cvPriv, created = 0) {
const edPub = bytesToNumberBE(concatBytes(new Uint8Array([0x40]), await ed25519.getPublicKey(edPriv)));
const edPubPacket = {
created,
algo: { TAG: 'EdDSA', data: { curve: 'ed25519', pub: edPub } },
};
const cvPoint = x25519.scalarMultBase(cvPriv);
const cvPub = bytesToNumberBE(concatBytes(new Uint8Array([0x40]), cvPoint));
const cvPubPacket = {
created,
algo: {
TAG: 'ECDH',
data: { curve: 'curve25519', pub: cvPub, params: { hash: 'sha256', encryption: 'aes128' } },
},
};
const fingerprint = getFingerprint(edPubPacket);
const keyId = getKeyId(fingerprint);
return { edPubPacket, fingerprint, keyId, cvPubPacket };
}
async function getCerts(edPriv, cvPriv, user, created = 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 } = await getPublicPackets(edPriv, cvPriv, created);
const edCert = await signData({
type: 'certPositive',
algo: 'EdDSA',
hash: 'sha512',
hashed: [
{ TAG: 'issuerFingerprint', data: { fingerprint } },
{ TAG: 'signatureCreationTime', data: created },
{ 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 = await signData({
type: 'subkeyBinding',
algo: 'EdDSA',
hash: 'sha512',
hashed: [
{ TAG: 'issuerFingerprint', data: { fingerprint } },
{ TAG: 'signatureCreationTime', data: created },
{ 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 async function formatPublic(edPriv, cvPriv, user, created = 0) {
const { edPubPacket, cvPubPacket, edCert, cvCert } = await getCerts(edPriv, cvPriv, user, created);
return pubArmor.encode([
{ TAG: 'publicKey', data: edPubPacket },
{ TAG: 'userId', data: user },
{ TAG: 'signature', data: edCert },
{ TAG: 'publicSubkey', data: cvPubPacket },
{ TAG: 'signature', data: cvCert },
]);
}
export async function formatPrivate(edPriv, cvPriv, user, password, created = 0, edSalt = randomBytes(8), edIV = randomBytes(16), cvSalt = randomBytes(8), cvIV = randomBytes(16)) {
const { edPubPacket, cvPubPacket, edCert, cvCert } = await getCerts(edPriv, cvPriv, user, created);
const edSecret = await createPrivKey(edPubPacket, edPriv, password, edSalt, edIV);
const cvPrivLE = P.U256BE.encode(P.U256LE.decode(cvPriv));
const cvSecret = await 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 },
]);
}
/*
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 async function getKeys(privKey, user, password, created = 0) {
const { keyId } = await getPublicPackets(privKey, privKey, created);
const { head: cvPrivate } = await ed25519.utils.getExtendedPublicKey(privKey);
const publicKey = await formatPublic(privKey, cvPrivate, user, created);
// The slow part
const privateKey = await formatPrivate(privKey, cvPrivate, user, password, created);
return { keyId, privateKey, publicKey };
}
export default getKeys;
+137
View File
@@ -0,0 +1,137 @@
/*! micro-ed25519-hdkey - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { ed25519 } from '@noble/curves/ed25519';
import { hmac } from '@noble/hashes/hmac';
import { ripemd160 } from '@noble/hashes/ripemd160';
import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, createView, hexToBytes, utf8ToBytes, bytesToHex } from '@noble/hashes/utils';
import { bytes as assertBytes } from '@noble/hashes/_assert';
export const MASTER_SECRET = utf8ToBytes('ed25519 seed');
export const HARDENED_OFFSET: number = 0x80000000;
const ZERO = new Uint8Array([0]);
type Hex = Uint8Array | string;
function ensureBytes(b: Hex, ...lengths: number[]): Uint8Array {
if (typeof b === 'string') b = hexToBytes(b);
assertBytes(b, ...lengths);
return b;
}
const hash160 = (data: Uint8Array) => ripemd160(sha256(data));
const fromU32 = (data: Uint8Array) => createView(data).getUint32(0, false);
const toU32 = (n: number) => {
if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) {
throw new Error(`Invalid number=${n}. Should be from 0 to 2 ** 32 - 1`);
}
const buf = new Uint8Array(4);
createView(buf).setUint32(0, n, false);
return buf;
};
interface HDKeyOpt {
depth?: number;
index?: number;
parentFingerprint?: number;
chainCode: Uint8Array;
privateKey: Uint8Array;
}
export class HDKey {
get publicKeyRaw(): Uint8Array {
return ed25519.getPublicKey(this.privateKey);
}
get publicKey(): Uint8Array {
return concatBytes(ZERO, this.publicKeyRaw);
}
get pubHash(): Uint8Array {
return hash160(this.publicKey);
}
get fingerprint(): number {
return fromU32(this.pubHash);
}
get fingerprintHex(): string {
return bytesToHex(toU32(this.fingerprint));
}
get parentFingerprintHex(): string {
return bytesToHex(toU32(this.parentFingerprint));
}
static fromMasterSeed(seed: Hex): HDKey {
seed = ensureBytes(seed);
if (8 * seed.length < 128 || 8 * seed.length > 512) {
throw new Error(
`HDKey: wrong seed length=${seed.length}. Should be between 128 and 512 bits; 256 bits is advised)`
);
}
const I = hmac(sha512, MASTER_SECRET, seed);
return new HDKey({
privateKey: I.slice(0, 32),
chainCode: I.slice(32),
});
}
readonly depth: number = 0;
readonly index: number = 0;
readonly chainCode: Uint8Array;
readonly parentFingerprint: number = 0;
readonly privateKey: Uint8Array;
constructor(opt: HDKeyOpt) {
if (!opt || typeof opt !== 'object')
throw new Error('HDKey.constructor must not be called directly');
assertBytes(opt.privateKey, 32);
assertBytes(opt.chainCode, 32);
this.depth = opt.depth || 0;
this.index = opt.index || 0;
this.parentFingerprint = opt.parentFingerprint || 0;
if (!this.depth) {
if (this.parentFingerprint || this.index)
throw new Error('HDKey: zero depth with non-zero index/parent fingerprint');
}
this.chainCode = opt.chainCode;
this.privateKey = opt.privateKey;
}
derive(path: string, forceHardened = false): HDKey {
if (!/^[mM]'?/.test(path)) throw new Error('Path must start with "m" or "M"');
if (/^[mM]'?$/.test(path)) return this;
const parts = path.replace(/^[mM]'?\//, '').split('/');
// tslint:disable-next-line
let child: HDKey = this;
for (const c of parts) {
const m = /^(\d+)('?)$/.exec(c);
if (!m || m.length !== 3) throw new Error(`Invalid child index: ${c}`);
let idx = +m[1];
if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) throw new Error('Invalid index');
// hardened key
if (forceHardened || m[2] === "'") idx += HARDENED_OFFSET;
child = child.deriveChild(idx);
}
return child;
}
deriveChild(index: number): HDKey {
if (index < HARDENED_OFFSET)
throw new Error(`Non-hardened child derivation not possible for Ed25519 (index=${index})`);
// Hardened child: 0x00 || ser256(kpar) || ser32(index)
const data = concatBytes(ZERO, this.privateKey, toU32(index));
const I = hmac(sha512, this.chainCode, data);
return new HDKey({
chainCode: I.slice(32),
depth: this.depth + 1,
parentFingerprint: this.fingerprint,
index,
privateKey: I.slice(0, 32),
});
}
sign(message: Hex): Uint8Array {
return ed25519.sign(message, this.privateKey);
}
verify(message: Hex, signature: Hex): boolean {
signature = ensureBytes(signature, 64);
return ed25519.verify(signature, message, this.publicKeyRaw);
}
}
+1
View File
@@ -0,0 +1 @@
throw new Error('The module has no entry-point: consult README for usage');
+67
View File
@@ -0,0 +1,67 @@
import { ed25519 } from '@noble/curves/ed25519';
import { concatBytes } from '@noble/hashes/utils';
import { hex, base32, utils } from '@scure/base';
const base36 = utils.chain(
utils.radix(36),
utils.alphabet('0123456789abcdefghijklmnopqrstuvwxyz'),
utils.padding(0),
utils.join('')
);
// Formats IPNS public key in bytes array format to 'ipns://k...' string format
export function formatPublicKey(pubBytes: Uint8Array) {
return `ipns://k${base36.encode(pubBytes)}`;
}
// Takes an IPNS pubkey (address) string as input and returns bytes array of the key
// Supports various formats ('ipns://k', 'ipns://b', 'ipns://f')
// Handles decoding and validation of the key before returning pubkey bytes
export function parseAddress(address: string): Uint8Array {
address = address.toLowerCase();
if (address.startsWith('ipns://')) address = address.slice(7);
let hexKey;
if (address.startsWith('k')) {
// Decode base-36 pubkey (after removing 'k' prefix) and encode it as a hex string
hexKey = hex.encode(base36.decode(address.slice(1)));
} else if (address.startsWith('b')) {
// Decode base-32 pubkey (after removing 'b' prefix) and encode it as a hex string
hexKey = hex.encode(base32.decode(address.slice(1).toUpperCase()));
} else if (address.startsWith('f')) {
hexKey = address.slice(1);
} else throw new Error('Unsupported Base-X Format'); // Throw error if pubkey format is not supported
// Check if hexKey has expected prefix '0172002408011220' and length of 80
if (hexKey.startsWith('0172002408011220') && hexKey.length === 80) {
return hex.decode(hexKey);
}
// Throw error if IPNS key prefix is invalid
throw new Error('Invalid IPNS Key Prefix: ' + hexKey);
}
// Generates an ed25519 pubkey from a seed and converts it to several IPNS pubkey formats
export async function getKeys(seed: Uint8Array) {
//? privKey "seed" should be checked for <ed25519.curve.n?
if (seed.length != 32) throw new TypeError('Seed must be 32 bytes in length');
// Generate ed25519 public key from seed
const pubKey = await ed25519.getPublicKey(seed);
// Create public key bytes by concatenating prefix bytes and pubKey
const pubKeyBytes = concatBytes(
new Uint8Array([0x01, 0x72, 0x00, 0x24, 0x08, 0x01, 0x12, 0x20]),
pubKey
);
const hexKey = hex.encode(pubKeyBytes).toLowerCase();
// Return different representations of the keys
return {
publicKey: `0x${hexKey}`,
privateKey: `0x${hex.encode(
concatBytes(new Uint8Array([0x08, 0x01, 0x12, 0x40]), seed, pubKey)
)}`,
base36: `ipns://k${base36.encode(pubKeyBytes)}`,
base32: `ipns://b${base32.encode(pubKeyBytes).toLowerCase()}`,
base16: `ipns://f${hexKey}`,
contenthash: `0xe501${hexKey}`,
};
}
export default getKeys;
+710
View File
@@ -0,0 +1,710 @@
import { ed25519, x25519 } from '@noble/curves/ed25519';
import { bytesToNumberBE, equalBytes, numberToHexUnpadded } from '@noble/curves/abstract/utils';
import { crypto } from '@noble/hashes/crypto';
import { ripemd160 } from '@noble/hashes/ripemd160';
import { sha1 } from '@noble/hashes/sha1';
import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import { sha3_256 } from '@noble/hashes/sha3';
import { CHash, concatBytes, randomBytes } from '@noble/hashes/utils';
import { utf8, hex } from '@scure/base';
import * as P from 'micro-packed';
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
// Safari supports AES_CFB via webCrypto, but chromium/firefox do not.
// Test page: https://diafygi.github.io/webcrypto-examples/
const BLOCK_LEN = 16;
const IV = new Uint8Array(BLOCK_LEN);
async function runAesBlock(msg: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
if (key.length !== 16 && key.length !== 32) throw new Error('Invalid key length');
if (!crypto) throw new Error('crypto.subtle must be defined');
const mode = { name: `AES-CBC`, length: key.length * 8 };
const wKey = await crypto.subtle.importKey('raw', key, mode, true, ['encrypt']);
const cipher = await crypto.subtle.encrypt(
{ name: `aes-cbc`, iv: IV, counter: IV, length: 64 },
wKey,
msg
);
return new Uint8Array(cipher).subarray(0, 16);
}
async function runAesCfb(keyLen: number, data: Bytes, key: Bytes, iv: Bytes, decrypt = false) {
if (keyLen !== key.length * 8) throw new Error('AesCfbProcess: wrong key length');
if (iv.length !== 16) throw new Error('AesCfbProcess: wrong IV');
const blocks: Bytes[] = [];
let prevBlock = iv;
for (let i = 0; i < data.length; i += 16) {
const curBlock = data.subarray(i, i + 16);
const enc = await runAesBlock(prevBlock, key);
const outBlock = curBlock.slice();
for (let j = 0; j < outBlock.length; j++) outBlock[j] ^= enc[j];
blocks.push(outBlock);
prevBlock = decrypt ? curBlock : outBlock;
}
return concatBytes(...blocks);
}
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.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.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.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.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 const PubKeyPacket = P.struct({
version: PGP_PACKET_VERSION,
created: P.U32BE,
algo: P.tag(pubKeyEnum, {
EdDSA: ECDSAPub,
ECDH: ECDHPub,
}),
});
type PubKeyType = P.UnwrapCoder<typeof PubKeyPacket>;
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.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();
}
const getFingerprint = (pubKey: PubKeyType) => hex.encode(sha1(hashPubKey.encode({ pubKey })));
const getKeyId = (fp: string) => fp.slice(-16);
// 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.data.length - r.pos;
return { TAG: tag, data: PacketTags[tag].decode(r.bytes(packetLen)) };
},
});
export const Stream = P.array(null, Packet);
// Key generation
const EDSIGN = P.array(null, P.U256BE);
async function signData(
head: SignatureHeadType,
unhashed: any,
data: any,
privateKey: Bytes
): Promise<SignatureType> {
const hash = hashSignature(head, data);
const hashPrefix = hash.subarray(0, 2);
const sig = EDSIGN.decode(await 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 async function decodeSecretKey(password: string, key: SecretKeyType) {
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 = await 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);
}
async function createPrivKey(
pub: PubKeyType,
key: Bytes,
password: string,
salt: Bytes,
iv: Bytes,
hash = 'sha1',
count = 240,
enc = 'aes128'
): Promise<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 = await 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.base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24);
export const privArmor = P.base64armor('PGP PRIVATE KEY BLOCK', 64, Stream, crc24);
async function getPublicPackets(edPriv: Bytes, cvPriv: Bytes, created = 0) {
const edPub = bytesToNumberBE(
concatBytes(new Uint8Array([0x40]), await ed25519.getPublicKey(edPriv))
);
const edPubPacket = {
created,
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,
algo: {
TAG: 'ECDH',
data: { curve: 'curve25519', pub: cvPub, params: { hash: 'sha256', encryption: 'aes128' } },
},
} as const;
const fingerprint = getFingerprint(edPubPacket);
const keyId = getKeyId(fingerprint);
return { edPubPacket, fingerprint, keyId, cvPubPacket };
}
async function getCerts(edPriv: Bytes, cvPriv: Bytes, user: string, created = 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 } = await getPublicPackets(
edPriv,
cvPriv,
created
);
const edCert = await signData(
{
type: 'certPositive',
algo: 'EdDSA',
hash: 'sha512',
hashed: [
{ TAG: 'issuerFingerprint', data: { fingerprint } },
{ TAG: 'signatureCreationTime', data: created },
{ 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 = await signData(
{
type: 'subkeyBinding',
algo: 'EdDSA',
hash: 'sha512',
hashed: [
{ TAG: 'issuerFingerprint', data: { fingerprint } },
{ TAG: 'signatureCreationTime', data: created },
{ 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 async function formatPublic(edPriv: Bytes, cvPriv: Bytes, user: string, created = 0) {
const { edPubPacket, cvPubPacket, edCert, cvCert } = await getCerts(
edPriv,
cvPriv,
user,
created
);
return pubArmor.encode([
{ TAG: 'publicKey', data: edPubPacket },
{ TAG: 'userId', data: user },
{ TAG: 'signature', data: edCert },
{ TAG: 'publicSubkey', data: cvPubPacket },
{ TAG: 'signature', data: cvCert },
]);
}
export async function formatPrivate(
edPriv: Bytes,
cvPriv: Bytes,
user: string,
password: string,
created = 0,
edSalt = randomBytes(8),
edIV = randomBytes(16),
cvSalt = randomBytes(8),
cvIV = randomBytes(16)
) {
const { edPubPacket, cvPubPacket, edCert, cvCert } = await getCerts(
edPriv,
cvPriv,
user,
created
);
const edSecret = await createPrivKey(edPubPacket, edPriv, password, edSalt, edIV);
const cvPrivLE = P.U256BE.encode(P.U256LE.decode(cvPriv));
const cvSecret = await 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 },
]);
}
/*
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 async function getKeys(privKey: Bytes, user: string, password: string, created = 0) {
const { keyId } = await getPublicPackets(privKey, privKey, created);
const { head: cvPrivate } = await ed25519.utils.getExtendedPublicKey(privKey);
const publicKey = await formatPublic(privKey, cvPrivate, user, created);
// The slow part
const privateKey = await formatPrivate(privKey, cvPrivate, user, password, created);
return { keyId, privateKey, publicKey };
}
export default getKeys;
+103
View File
@@ -0,0 +1,103 @@
import { ed25519 } from '@noble/curves/ed25519';
import { sha256 } from '@noble/hashes/sha256';
import { concatBytes, randomBytes } from '@noble/hashes/utils';
import { base64 } from '@scure/base';
import * as P from 'micro-packed';
export const SSHString = P.string(P.U32BE);
export const SSHBuf = P.bytes(P.U32BE);
export const SSHKeyType = P.magic(SSHString, 'ssh-ed25519');
export const PublicKey = P.struct({ keyType: SSHKeyType, pubKey: P.bytes(P.U32BE) });
const PrivateKey = P.padRight(
8,
P.struct({
check1: P.bytes(4),
check2: P.bytes(4),
keyType: SSHKeyType,
pubKey: SSHBuf,
privKey: SSHBuf,
comment: SSHString,
}),
(i: number) => i + 1
);
// https://tools.ietf.org/html/draft-miller-ssh-agent-02#section-4.5
export const AuthData = P.struct({
nonce: SSHBuf,
userAuthRequest: P.U8, // == 50
user: SSHString,
conn: SSHString,
auth: SSHString,
haveSig: P.U8, // == 1
keyType: SSHKeyType,
pubKey: P.prefix(P.U32BE, PublicKey),
});
export type AuthDataType = P.UnwrapCoder<typeof AuthData>;
export const PrivateExport = P.base64armor(
'openssh private key',
70,
P.struct({
magic: P.magicBytes('openssh-key-v1\0'),
// Only decrypted ed25519 keys supported for now
ciphername: P.magic(SSHString, 'none'),
kdfname: P.magic(SSHString, 'none'),
kdfopts: P.magic(SSHString, ''),
keys: P.array(
P.U32BE,
P.struct({
pubKey: P.prefix(P.U32BE, PublicKey),
privKey: P.prefix(P.U32BE, PrivateKey),
})
),
})
);
export function formatPublicKey(bytes: Uint8Array, comment?: string): string {
const blob = PublicKey.encode({ pubKey: bytes });
return `ssh-ed25519 ${base64.encode(blob)}${comment ? ` ${comment}` : ''}`;
}
export function getFingerprint(bytes: Uint8Array): string {
const blob = PublicKey.encode({ pubKey: bytes });
// ssh-keygen -l -f ~/.ssh/id_ed25519
// 256 SHA256:+WK/Sl4XJjoxDlAWYuhq4Fl2hka9j3GOUjYczQkqnCI user@comp.local (ED25519)
return `SHA256:${base64.encode(sha256(blob)).replace(/=$/, '')}`;
}
// For determenistic generation in tests
export async function getKeys(
privateKey: Uint8Array,
comment?: string,
checkBytes = randomBytes(4)
) {
const pubKey = await ed25519.getPublicKey(privateKey);
return {
publicKeyBytes: pubKey,
publicKey: formatPublicKey(pubKey, comment),
fingerprint: getFingerprint(pubKey),
privateKey: PrivateExport.encode({
keys: [
{
pubKey: { pubKey },
privKey: {
// Check bytes, should be same
check1: checkBytes,
check2: checkBytes,
pubKey,
privKey: concatBytes(privateKey, pubKey),
comment: comment || '',
},
},
],
}),
};
}
// For SSH Agents
export function authSign(privateKey: Uint8Array, data: AuthDataType): Uint8Array {
return ed25519.sign(AuthData.encode(data), privateKey);
}
export default getKeys;
+36
View File
@@ -0,0 +1,36 @@
import { ed25519 } from '@noble/curves/ed25519';
import { sha3_256 } from '@noble/hashes/sha3';
import { concatBytes } from '@noble/hashes/utils';
import { utf8, base32, base64 } from '@scure/base';
const ADDRESS_VERSION = new Uint8Array([0x03]);
export function formatPublicKey(pubBytes: Uint8Array) {
// checksum = H(".onion checksum" || pubkey || version)
const checksum = sha3_256(concatBytes(utf8.decode('.onion checksum'), pubBytes, ADDRESS_VERSION));
// onion_address = base32(pubkey || checksum || version);
const addr = concatBytes(pubBytes, checksum.slice(0, 2), ADDRESS_VERSION);
return `${base32.encode(addr).toLowerCase()}.onion`;
}
export function parseAddress(address: string): Uint8Array {
if (!address.endsWith('.onion')) throw new Error('Address must end with .onion');
const addr = base32.decode(address.replace(/\.onion$/, '').toUpperCase());
// skip last 3 bytes
const skip = addr.slice(0, addr.length - 3);
const key = formatPublicKey(skip);
if (key !== address) throw new Error('Invalid checksum');
return skip;
}
export async function getKeys(seed: Uint8Array) {
const { head, prefix, pointBytes } = await ed25519.utils.getExtendedPublicKey(seed);
const added = concatBytes(head, prefix);
return {
publicKeyBytes: pointBytes,
publicKey: formatPublicKey(pointBytes),
privateKey: `ED25519-V3:${base64.encode(added)}`,
};
}
export default getKeys;
+2
View File
@@ -0,0 +1,2 @@
import { randomBytes } from '@noble/hashes/utils';
export { randomBytes };
+43
View File
@@ -0,0 +1,43 @@
import * as P from 'micro-packed';
export declare const SSHString: P.CoderType<string>;
export declare const SSHBuf: P.CoderType<Uint8Array>;
export declare const SSHKeyType: P.CoderType<undefined>;
export declare const PublicKey: P.CoderType<P.StructInput<{
keyType: undefined;
pubKey: Uint8Array;
}>>;
export declare const AuthData: P.CoderType<P.StructInput<{
nonce: Uint8Array;
userAuthRequest: number;
user: string;
conn: string;
auth: string;
haveSig: number;
keyType: undefined;
pubKey: P.StructInput<{
keyType: any;
pubKey: any;
}>;
}>>;
export type AuthDataType = P.UnwrapCoder<typeof AuthData>;
export declare const PrivateExport: P.Coder<P.StructInput<{
magic: undefined;
ciphername: undefined;
kdfname: undefined;
kdfopts: undefined;
keys: P.StructInput<{
pubKey: any;
privKey: any;
}>[];
}>, string>;
export declare function formatPublicKey(bytes: Uint8Array, comment?: string): string;
export declare function getFingerprint(bytes: Uint8Array): string;
export declare function getKeys(privateKey: Uint8Array, comment?: string, checkBytes?: Uint8Array): Promise<{
publicKeyBytes: Uint8Array;
publicKey: string;
fingerprint: string;
privateKey: string;
}>;
export declare function authSign(privateKey: Uint8Array, data: AuthDataType): Uint8Array;
export default getKeys;
//# sourceMappingURL=ssh.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["src/ssh.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAElC,eAAO,MAAM,SAAS,qBAAoB,CAAC;AAC3C,eAAO,MAAM,MAAM,yBAAmB,CAAC;AACvC,eAAO,MAAM,UAAU,wBAAoC,CAAC;AAC5D,eAAO,MAAM,SAAS;;;GAA8D,CAAC;AAerF,eAAO,MAAM,QAAQ;;;;;;;;;;;;GASnB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC;AAE1D,eAAO,MAAM,aAAa;;;;;;;;;WAiBzB,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKxD;AAGD,wBAAsB,OAAO,CAC3B,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,aAAiB;;;;;GAuB5B;AAGD,wBAAgB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,GAAG,UAAU,CAE/E;AAED,eAAe,OAAO,CAAC"}
+78
View File
@@ -0,0 +1,78 @@
import { ed25519 } from '@noble/curves/ed25519';
import { sha256 } from '@noble/hashes/sha256';
import { concatBytes, randomBytes } from '@noble/hashes/utils';
import { base64 } from '@scure/base';
import * as P from 'micro-packed';
export const SSHString = P.string(P.U32BE);
export const SSHBuf = P.bytes(P.U32BE);
export const SSHKeyType = P.magic(SSHString, 'ssh-ed25519');
export const PublicKey = P.struct({ keyType: SSHKeyType, pubKey: P.bytes(P.U32BE) });
const PrivateKey = P.padRight(8, P.struct({
check1: P.bytes(4),
check2: P.bytes(4),
keyType: SSHKeyType,
pubKey: SSHBuf,
privKey: SSHBuf,
comment: SSHString,
}), (i) => i + 1);
// https://tools.ietf.org/html/draft-miller-ssh-agent-02#section-4.5
export const AuthData = P.struct({
nonce: SSHBuf,
userAuthRequest: P.U8, // == 50
user: SSHString,
conn: SSHString,
auth: SSHString,
haveSig: P.U8, // == 1
keyType: SSHKeyType,
pubKey: P.prefix(P.U32BE, PublicKey),
});
export const PrivateExport = P.base64armor('openssh private key', 70, P.struct({
magic: P.magicBytes('openssh-key-v1\0'),
// Only decrypted ed25519 keys supported for now
ciphername: P.magic(SSHString, 'none'),
kdfname: P.magic(SSHString, 'none'),
kdfopts: P.magic(SSHString, ''),
keys: P.array(P.U32BE, P.struct({
pubKey: P.prefix(P.U32BE, PublicKey),
privKey: P.prefix(P.U32BE, PrivateKey),
})),
}));
export function formatPublicKey(bytes, comment) {
const blob = PublicKey.encode({ pubKey: bytes });
return `ssh-ed25519 ${base64.encode(blob)}${comment ? ` ${comment}` : ''}`;
}
export function getFingerprint(bytes) {
const blob = PublicKey.encode({ pubKey: bytes });
// ssh-keygen -l -f ~/.ssh/id_ed25519
// 256 SHA256:+WK/Sl4XJjoxDlAWYuhq4Fl2hka9j3GOUjYczQkqnCI user@comp.local (ED25519)
return `SHA256:${base64.encode(sha256(blob)).replace(/=$/, '')}`;
}
// For determenistic generation in tests
export async function getKeys(privateKey, comment, checkBytes = randomBytes(4)) {
const pubKey = await ed25519.getPublicKey(privateKey);
return {
publicKeyBytes: pubKey,
publicKey: formatPublicKey(pubKey, comment),
fingerprint: getFingerprint(pubKey),
privateKey: PrivateExport.encode({
keys: [
{
pubKey: { pubKey },
privKey: {
// Check bytes, should be same
check1: checkBytes,
check2: checkBytes,
pubKey,
privKey: concatBytes(privateKey, pubKey),
comment: comment || '',
},
},
],
}),
};
}
// For SSH Agents
export function authSign(privateKey, data) {
return ed25519.sign(AuthData.encode(data), privateKey);
}
export default getKeys;
+9
View File
@@ -0,0 +1,9 @@
export declare function formatPublicKey(pubBytes: Uint8Array): string;
export declare function parseAddress(address: string): Uint8Array;
export declare function getKeys(seed: Uint8Array): Promise<{
publicKeyBytes: Uint8Array;
publicKey: string;
privateKey: string;
}>;
export default getKeys;
//# sourceMappingURL=tor.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tor.d.ts","sourceRoot":"","sources":["src/tor.ts"],"names":[],"mappings":"AAOA,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU,UAMnD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQxD;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,UAAU;;;;GAQ7C;AAED,eAAe,OAAO,CAAC"}
+33
View File
@@ -0,0 +1,33 @@
import { ed25519 } from '@noble/curves/ed25519';
import { sha3_256 } from '@noble/hashes/sha3';
import { concatBytes } from '@noble/hashes/utils';
import { utf8, base32, base64 } from '@scure/base';
const ADDRESS_VERSION = new Uint8Array([0x03]);
export function formatPublicKey(pubBytes) {
// checksum = H(".onion checksum" || pubkey || version)
const checksum = sha3_256(concatBytes(utf8.decode('.onion checksum'), pubBytes, ADDRESS_VERSION));
// onion_address = base32(pubkey || checksum || version);
const addr = concatBytes(pubBytes, checksum.slice(0, 2), ADDRESS_VERSION);
return `${base32.encode(addr).toLowerCase()}.onion`;
}
export function parseAddress(address) {
if (!address.endsWith('.onion'))
throw new Error('Address must end with .onion');
const addr = base32.decode(address.replace(/\.onion$/, '').toUpperCase());
// skip last 3 bytes
const skip = addr.slice(0, addr.length - 3);
const key = formatPublicKey(skip);
if (key !== address)
throw new Error('Invalid checksum');
return skip;
}
export async function getKeys(seed) {
const { head, prefix, pointBytes } = await ed25519.utils.getExtendedPublicKey(seed);
const added = concatBytes(head, prefix);
return {
publicKeyBytes: pointBytes,
publicKey: formatPublicKey(pointBytes),
privateKey: `ED25519-V3:${base64.encode(added)}`,
};
}
export default getKeys;
+3
View File
@@ -0,0 +1,3 @@
import { randomBytes } from '@noble/hashes/utils';
export { randomBytes };
//# sourceMappingURL=utils.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,CAAC"}
+2
View File
@@ -0,0 +1,2 @@
import { randomBytes } from '@noble/hashes/utils';
export { randomBytes };