Skip to content

Commit

Permalink
Use aes-cfb from noble-ciphers
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Mar 25, 2024
1 parent 070aeb9 commit be898f7
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 93 deletions.
13 changes: 11 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"src"
],
"dependencies": {
"@noble/ciphers": "~0.5.2",
"@noble/curves": "~1.4.0",
"@noble/hashes": "~1.4.0",
"@scure/base": "~1.1.6",
Expand Down
4 changes: 2 additions & 2 deletions src/ipns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ export function parseAddress(address: string): Uint8Array {
}

// Generates an ed25519 pubkey from a seed and converts it to several IPNS pubkey formats
export async function getKeys(seed: Uint8Array) {
export 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);
const pubKey = 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]),
Expand Down
104 changes: 39 additions & 65 deletions src/pgp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cfb } from '@noble/ciphers/aes';
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';
Expand All @@ -20,36 +20,22 @@ export type Bytes = Uint8Array;

// 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) {
function runAesCfb(keyLen: number, data: Bytes, key: Bytes, iv: Bytes, decrypt = false) {
// NOTE: we need to validate key length here since file can be malformed
if (keyLen !== key.length * 8) throw new Error('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);
// Packed does subarray and read is unaligned here
// TODO: support unaligned reads in all AES?
const keyCopy = key.slice();
const ivCopy = iv.slice();
const dataCopy = data.slice();
const cipher = cfb(keyCopy, ivCopy);
const res = decrypt ? cipher.decrypt(dataCopy) : cipher.encrypt(dataCopy);
keyCopy.fill(0);
ivCopy.fill(0);
dataCopy.fill(0);
return res;
}

function createAesCfb(len: number) {
Expand Down Expand Up @@ -503,15 +489,15 @@ export const Stream = P.array(null, Packet);

// Key generation
const EDSIGN = P.array(null, P.U256BE);
async function signData(
function signData(
head: SignatureHeadType,
unhashed: any,
data: any,
privateKey: Bytes
): Promise<SignatureType> {
): SignatureType {
const hash = hashSignature(head, data);
const hashPrefix = hash.subarray(0, 2);
const sig = EDSIGN.decode(await ed25519.sign(hash, privateKey)) as any;
const sig = EDSIGN.decode(ed25519.sign(hash, privateKey)) as any;
return { head, unhashed, hashPrefix, sig };
}

Expand All @@ -525,7 +511,7 @@ function decodeSecretChecksum(secret: Bytes) {
return mpi.decode(data);
}

export async function decodeSecretKey(password: string, key: SecretKeyType) {
export 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;
Expand All @@ -539,7 +525,7 @@ export async function decodeSecretKey(password: string, key: SecretKeyType) {
(data as any).salt,
(data as any).count
);
const decrypted = await Encryption[keyData.enc].decrypt(keyData.secret, encKey, keyData.iv);
const decrypted = Encryption[keyData.enc].decrypt(keyData.secret, encKey, keyData.iv);
const decryptedKey = decrypted.subarray(0, -20);
const checksum = Hash.sha1(decryptedKey);
if (!equalBytes(decrypted.slice(-20), checksum))
Expand All @@ -551,7 +537,7 @@ export async function decodeSecretKey(password: string, key: SecretKeyType) {
return mpi.decode(decryptedKey);
}

async function createPrivKey(
function createPrivKey(
pub: PubKeyType,
key: Bytes,
password: string,
Expand All @@ -560,24 +546,22 @@ async function createPrivKey(
hash = 'sha1',
count = 240,
enc = 'aes128'
): Promise<SecretKeyType> {
): 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 secret = Encryption[enc].encrypt(secretClear, encKey, iv);
const S2K = { TAG: 'iterated', data: { hash, salt, count } } as const;
return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } };
}

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))
);
function getPublicPackets(edPriv: Bytes, cvPriv: Bytes, created = 0) {
const edPub = bytesToNumberBE(concatBytes(new Uint8Array([0x40]), ed25519.getPublicKey(edPriv)));
const edPubPacket = {
created,
algo: { TAG: 'EdDSA', data: { curve: 'ed25519', pub: edPub } },
Expand All @@ -596,20 +580,20 @@ async function getPublicPackets(edPriv: Bytes, cvPriv: Bytes, created = 0) {
return { edPubPacket, fingerprint, keyId, cvPubPacket };
}

async function getCerts(edPriv: Bytes, cvPriv: Bytes, user: string, created = 0) {
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(
const { edPubPacket, fingerprint, keyId, cvPubPacket } = getPublicPackets(
edPriv,
cvPriv,
created
);

const edCert = await signData(
const edCert = signData(
{
type: 'certPositive',
algo: 'EdDSA',
Expand All @@ -630,7 +614,7 @@ async function getCerts(edPriv: Bytes, cvPriv: Bytes, user: string, created = 0)
{ pubKey: { pubKey: edPubPacket }, user: { user } },
edPriv
);
const cvCert = await signData(
const cvCert = signData(
{
type: 'subkeyBinding',
algo: 'EdDSA',
Expand All @@ -648,13 +632,8 @@ async function getCerts(edPriv: Bytes, cvPriv: Bytes, user: string, created = 0)
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
);
export function formatPublic(edPriv: Bytes, cvPriv: Bytes, user: string, created = 0) {
const { edPubPacket, cvPubPacket, edCert, cvCert } = getCerts(edPriv, cvPriv, user, created);
return pubArmor.encode([
{ TAG: 'publicKey', data: edPubPacket },
{ TAG: 'userId', data: user },
Expand All @@ -664,7 +643,7 @@ export async function formatPublic(edPriv: Bytes, cvPriv: Bytes, user: string, c
]);
}

export async function formatPrivate(
export function formatPrivate(
edPriv: Bytes,
cvPriv: Bytes,
user: string,
Expand All @@ -675,15 +654,10 @@ export async function formatPrivate(
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 { edPubPacket, cvPubPacket, edCert, cvCert } = getCerts(edPriv, cvPriv, user, created);
const edSecret = createPrivKey(edPubPacket, edPriv, password, edSalt, edIV);
const cvPrivLE = P.U256BE.encode(P.U256LE.decode(cvPriv));
const cvSecret = await createPrivKey(cvPubPacket, cvPrivLE, password, cvSalt, cvIV);
const cvSecret = createPrivKey(cvPubPacket, cvPrivLE, password, cvSalt, cvIV);
return privArmor.encode([
{ TAG: 'secretKey', data: edSecret },
{ TAG: 'userId', data: user },
Expand All @@ -698,12 +672,12 @@ export async function formatPrivate(
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);
export function getKeys(privKey: Bytes, user: string, password: string, created = 0) {
const { keyId } = getPublicPackets(privKey, privKey, created);
const { head: cvPrivate } = ed25519.utils.getExtendedPublicKey(privKey);
const publicKey = formatPublic(privKey, cvPrivate, user, created);
// The slow part
const privateKey = await formatPrivate(privKey, cvPrivate, user, password, created);
const privateKey = formatPrivate(privKey, cvPrivate, user, password, created);
return { keyId, privateKey, publicKey };
}

Expand Down
8 changes: 2 additions & 6 deletions src/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,8 @@ export function getFingerprint(bytes: Uint8Array): string {
}

// For determenistic generation in tests
export async function getKeys(
privateKey: Uint8Array,
comment?: string,
checkBytes = randomBytes(4)
) {
const pubKey = await ed25519.getPublicKey(privateKey);
export function getKeys(privateKey: Uint8Array, comment?: string, checkBytes = randomBytes(4)) {
const pubKey = ed25519.getPublicKey(privateKey);
return {
publicKeyBytes: pubKey,
publicKey: formatPublicKey(pubKey, comment),
Expand Down
4 changes: 2 additions & 2 deletions src/tor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export function parseAddress(address: string): Uint8Array {
return skip;
}

export async function getKeys(seed: Uint8Array) {
const { head, prefix, pointBytes } = await ed25519.utils.getExtendedPublicKey(seed);
export function getKeys(seed: Uint8Array) {
const { head, prefix, pointBytes } = ed25519.utils.getExtendedPublicKey(seed);
const added = concatBytes(head, prefix);
return {
publicKeyBytes: pointBytes,
Expand Down
4 changes: 2 additions & 2 deletions test/ipns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import * as ipns from '../lib/esm/ipns.js';
import { hex } from '@scure/base';

describe('ipns', () => {
should('basic', async () => {
should('basic', () => {
const seed = hex.decode('0681d6420abb1ba47acd5c03c8e5ee84185a2673576b262e234e50c46d86f597');
const pub = hex.decode('12c8299ec2c51dffbbcb4f9fccadcee1424cb237e9b30d3cd72d47c18103689d');
const addr = 'ipns://k51qzi5uqu5dgnfwbc46une4upw1vc9hxznymyeykmg6rev1513yrnbyrwmmql';
deepStrictEqual(await ipns.getKeys(seed), {
deepStrictEqual(ipns.getKeys(seed), {
publicKey:
'0x017200240801122012c8299ec2c51dffbbcb4f9fccadcee1424cb237e9b30d3cd72d47c18103689d',
privateKey:
Expand Down
16 changes: 8 additions & 8 deletions test/pgp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ describe('pgp', () => {
deepStrictEqual(pgp.PacketLen.decode(new Uint8Array([0xc5, 0xfb])), 1723);
deepStrictEqual(pgp.PacketLen.decode(new Uint8Array([0xff, 0x00, 0x01, 0x86, 0xa0])), 100000);
});
should('PGP', async () => {
should('PGP', () => {
const edPriv = hex.decode('6c18b9d6dc5d18a933c704c56ed8165a0651b27856ce3345f52f2921666dcef1');
const cvPriv = hex.decode('58cf48afad21cbf3e2d904387df28a1385c8a1b790f28e04ca7eb721fd6c0d6b');
const edSalt = hex.decode('31b33a4b50c662f4');
const edIV = hex.decode('821984c2c70cd60f0fbf650b6e666ead');
const cvSalt = hex.decode('050c1f3e46bfcc8d');
const cvIV = hex.decode('ab54be47dc65e8ac478aa2d1ec7b0c7c');
const pubKey = await pgp.formatPublic(edPriv, cvPriv, USER, CREATED);
const privKey = await pgp.formatPrivate(
const pubKey = pgp.formatPublic(edPriv, cvPriv, USER, CREATED);
const privKey = pgp.formatPrivate(
edPriv,
cvPriv,
USER,
Expand All @@ -85,18 +85,18 @@ describe('pgp', () => {
);
deepStrictEqual(privKey, PRIV, 'privateKey (armor)');
deepStrictEqual(
await pgp.decodeSecretKey(PWD, pgp.privArmor.decode(PRIV)[0].data),
pgp.decodeSecretKey(PWD, pgp.privArmor.decode(PRIV)[0].data),
48893474592257195969419733099033914136114698516948265455201948185088704237297n
);
deepStrictEqual(
await pgp.decodeSecretKey(PWD, pgp.privArmor.decode(PRIV)[3].data),
pgp.decodeSecretKey(PWD, pgp.privArmor.decode(PRIV)[3].data),
48421196023274373923070909897586368745762760188967824324892067105939602853720n
);
});
const NAME_EMAIL = 'a <a>';
should('PGP Keys', async () => {
should('PGP Keys', () => {
const seed = hex.decode('29f47c314ee8b1c77a0b7e4c0043a04a20af46f10132855b79f9ff6c4f8a8ed9');
const { publicKey: pub } = await pgp.getKeys(seed, NAME_EMAIL, PWD, 0);
const { publicKey: pub } = pgp.getKeys(seed, NAME_EMAIL, PWD, 0);
deepStrictEqual(
pub,
`-----BEGIN PGP PUBLIC KEY BLOCK-----
Expand All @@ -114,7 +114,7 @@ fTxMaZcG
-----END PGP PUBLIC KEY BLOCK-----
`
);
const { publicKey: pub2 } = await pgp.getKeys(seed, NAME_EMAIL, PWD, 123);
const { publicKey: pub2 } = pgp.getKeys(seed, NAME_EMAIL, PWD, 123);
deepStrictEqual(pub2 !== pub, true);
deepStrictEqual(
pub2,
Expand Down
4 changes: 2 additions & 2 deletions test/pgp_keygen.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ function exec(command, opt = {}) {
return { status, stdout, stderr };
}

should('basic', async () => {
should('basic', () => {
// Deterministic via scrypt
const seed = randomBytes();
let { publicKey, privateKey, keyId } = await pgp.getKeys(seed, 'user', 'password');
let { publicKey, privateKey, keyId } = pgp.getKeys(seed, 'user', 'password');
const SECRET_KEY_OPT = `--no-tty --batch --yes --passphrase "password"`;

const cleanKeys = () => {
Expand Down
4 changes: 2 additions & 2 deletions test/ssh.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ describe('ssh', () => {
should('pack & unpack ssh privkeys should be the same', () => {
deepStrictEqual(realKey, ssh.PrivateExport.encode(ssh.PrivateExport.decode(realKey)));
});
should('return correct key from seed', async () => {
should('return correct key from seed', () => {
const priv = hex.decode('71e722b077c007d4ae263287878a0bff1816c99f93cf8dcddd995bccefd1d7a3');
const comment = 'user@pc';
const checkBytes = hex.decode('c346f14a');
deepStrictEqual(await ssh.getKeys(priv, comment, checkBytes), EXPECTED);
deepStrictEqual(ssh.getKeys(priv, comment, checkBytes), EXPECTED);
});
});
Loading

0 comments on commit be898f7

Please sign in to comment.