Skip to content

Commit

Permalink
Merge remote-tracking branch 'wallet/main' into merge-wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
danielailie committed Oct 2, 2024
2 parents ef3db11 + 74bb771 commit 8bc371c
Show file tree
Hide file tree
Showing 43 changed files with 2,069 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src-wallet/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ErrInvariantFailed } from "./errors";

export function guardLength(withLength: { length?: number }, expectedLength: number) {
let actualLength = withLength.length || 0;

if (actualLength != expectedLength) {
throw new ErrInvariantFailed(`wrong length, expected: ${expectedLength}, actual: ${actualLength}`);
}
}
15 changes: 15 additions & 0 deletions src-wallet/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Global configuration of the library.
*
* Generally speaking, this configuration should only be altered on exotic use cases;
* it can be seen as a collection of constants (or, to be more precise, rarely changed variables) that are used throughout the library.
*
* Never alter the configuration within a library!
* Only alter the configuration (if needed) within an (end) application that uses this library.
*/
export class LibraryConfig {
/**
* The human-readable-part of the bech32 addresses.
*/
public static DefaultAddressHrp: string = "erd";
}
8 changes: 8 additions & 0 deletions src-wallet/crypto/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const CipherAlgorithm = "aes-128-ctr";
export const DigestAlgorithm = "sha256";
export const KeyDerivationFunction = "scrypt";

// X25519 public key encryption
export const PubKeyEncVersion = 1;
export const PubKeyEncNonceLength = 24;
export const PubKeyEncCipher = "x25519-xsalsa20-poly1305";
27 changes: 27 additions & 0 deletions src-wallet/crypto/decryptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import crypto from "crypto";
import { EncryptedData } from "./encryptedData";
import { DigestAlgorithm } from "./constants";
import { Err } from "../errors";

export class Decryptor {
static decrypt(data: EncryptedData, password: string): Buffer {
const kdfparams = data.kdfparams;
const salt = Buffer.from(data.salt, "hex");
const iv = Buffer.from(data.iv, "hex");
const ciphertext = Buffer.from(data.ciphertext, "hex");
const derivedKey = kdfparams.generateDerivedKey(Buffer.from(password), salt);
const derivedKeyFirstHalf = derivedKey.slice(0, 16);
const derivedKeySecondHalf = derivedKey.slice(16, 32);

const computedMAC = crypto.createHmac(DigestAlgorithm, derivedKeySecondHalf).update(ciphertext).digest();
const actualMAC = data.mac;

if (computedMAC.toString("hex") !== actualMAC) {
throw new Err("MAC mismatch, possibly wrong password");
}

const decipher = crypto.createDecipheriv(data.cipher, derivedKeyFirstHalf, iv);

return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}
}
36 changes: 36 additions & 0 deletions src-wallet/crypto/derivationParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import scryptsy from "scryptsy";

export class ScryptKeyDerivationParams {
/**
* numIterations
*/
n = 4096;

/**
* memFactor
*/
r = 8;

/**
* pFactor
*/
p = 1;

dklen = 32;

constructor(n = 4096, r = 8, p = 1, dklen = 32) {
this.n = n;
this. r = r;
this.p = p;
this.dklen = dklen;
}

/**
* Will take about:
* - 80-90 ms in Node.js, on a i3-8100 CPU @ 3.60GHz
* - 350-360 ms in browser (Firefox), on a i3-8100 CPU @ 3.60GHz
*/
public generateDerivedKey(password: Buffer, salt: Buffer): Buffer {
return scryptsy(password, salt, this.n, this.r, this.p, this.dklen);
}
}
22 changes: 22 additions & 0 deletions src-wallet/crypto/encrypt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { assert } from "chai";
import { Decryptor } from "./decryptor";
import { EncryptedData } from "./encryptedData";
import { Encryptor } from "./encryptor";

describe("test address", () => {
it("encrypts/decrypts", () => {
const sensitiveData = Buffer.from("my mnemonic");
const encryptedData = Encryptor.encrypt(sensitiveData, "password123");
const decryptedBuffer = Decryptor.decrypt(encryptedData, "password123");

assert.equal(sensitiveData.toString('hex'), decryptedBuffer.toString('hex'));
});

it("encodes/decodes kdfparams", () => {
const sensitiveData = Buffer.from("my mnemonic");
const encryptedData = Encryptor.encrypt(sensitiveData, "password123");
const decodedData = EncryptedData.fromJSON(encryptedData.toJSON());

assert.deepEqual(decodedData, encryptedData, "invalid decoded data");
});
});
65 changes: 65 additions & 0 deletions src-wallet/crypto/encryptedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ScryptKeyDerivationParams } from "./derivationParams";

export class EncryptedData {
id: string;
version: number;
cipher: string;
ciphertext: string;
iv: string;
kdf: string;
kdfparams: ScryptKeyDerivationParams;
salt: string;
mac: string;

constructor(data: Omit<EncryptedData, "toJSON">) {
this.id = data.id;
this.version = data.version;
this.ciphertext = data.ciphertext;
this.iv = data.iv;
this.cipher = data.cipher;
this.kdf = data.kdf;
this.kdfparams = data.kdfparams;
this.mac = data.mac;
this.salt = data.salt;
}

toJSON(): any {
return {
version: this.version,
id: this.id,
crypto: {
ciphertext: this.ciphertext,
cipherparams: { iv: this.iv },
cipher: this.cipher,
kdf: this.kdf,
kdfparams: {
dklen: this.kdfparams.dklen,
salt: this.salt,
n: this.kdfparams.n,
r: this.kdfparams.r,
p: this.kdfparams.p
},
mac: this.mac,
}
};
}

static fromJSON(data: any): EncryptedData {
return new EncryptedData({
version: data.version,
id: data.id,
ciphertext: data.crypto.ciphertext,
iv: data.crypto.cipherparams.iv,
cipher: data.crypto.cipher,
kdf: data.crypto.kdf,
kdfparams: new ScryptKeyDerivationParams(
data.crypto.kdfparams.n,
data.crypto.kdfparams.r,
data.crypto.kdfparams.p,
data.crypto.kdfparams.dklen,
),
salt: data.crypto.kdfparams.salt,
mac: data.crypto.mac,
});
}
}
40 changes: 40 additions & 0 deletions src-wallet/crypto/encryptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import crypto from "crypto";
import { CipherAlgorithm, DigestAlgorithm, KeyDerivationFunction } from "./constants";
import { ScryptKeyDerivationParams } from "./derivationParams";
import { EncryptedData } from "./encryptedData";
import { Randomness } from "./randomness";

interface IRandomness {
id: string;
iv: Buffer;
salt: Buffer;
}

export enum EncryptorVersion {
V4 = 4,
}

export class Encryptor {
static encrypt(data: Buffer, password: string, randomness: IRandomness = new Randomness()): EncryptedData {
const kdParams = new ScryptKeyDerivationParams();
const derivedKey = kdParams.generateDerivedKey(Buffer.from(password), randomness.salt);
const derivedKeyFirstHalf = derivedKey.slice(0, 16);
const derivedKeySecondHalf = derivedKey.slice(16, 32);
const cipher = crypto.createCipheriv(CipherAlgorithm, derivedKeyFirstHalf, randomness.iv);

const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
const mac = crypto.createHmac(DigestAlgorithm, derivedKeySecondHalf).update(ciphertext).digest();

return new EncryptedData({
version: EncryptorVersion.V4,
id: randomness.id,
ciphertext: ciphertext.toString('hex'),
iv: randomness.iv.toString('hex'),
cipher: CipherAlgorithm,
kdf: KeyDerivationFunction,
kdfparams: kdParams,
mac: mac.toString('hex'),
salt: randomness.salt.toString('hex')
});
}
}
7 changes: 7 additions & 0 deletions src-wallet/crypto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from "./constants";
export * from "./encryptor";
export * from "./decryptor";
export * from "./pubkeyEncryptor";
export * from "./pubkeyDecryptor";
export * from "./encryptedData";
export * from "./randomness";
36 changes: 36 additions & 0 deletions src-wallet/crypto/pubkeyDecryptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import crypto from "crypto";
import nacl from "tweetnacl";
import ed2curve from "ed2curve";
import { X25519EncryptedData } from "./x25519EncryptedData";
import { UserPublicKey, UserSecretKey } from "../userKeys";

export class PubkeyDecryptor {
static decrypt(data: X25519EncryptedData, decryptorSecretKey: UserSecretKey): Buffer {
const ciphertext = Buffer.from(data.ciphertext, 'hex');
const edhPubKey = Buffer.from(data.identities.ephemeralPubKey, 'hex');
const originatorPubKeyBuffer = Buffer.from(data.identities.originatorPubKey, 'hex');
const originatorPubKey = new UserPublicKey(originatorPubKeyBuffer);

const authMessage = crypto.createHash('sha256').update(
Buffer.concat([ciphertext, edhPubKey])
).digest();

if (!originatorPubKey.verify(authMessage, Buffer.from(data.mac, 'hex'))) {
throw new Error("Invalid authentication for encrypted message originator");
}

const nonce = Buffer.from(data.nonce, 'hex');
const x25519Secret = ed2curve.convertSecretKey(decryptorSecretKey.valueOf());
const x25519EdhPubKey = ed2curve.convertPublicKey(edhPubKey);
if (x25519EdhPubKey === null) {
throw new Error("Could not convert ed25519 public key to x25519");
}

const decryptedMessage = nacl.box.open(ciphertext, nonce, x25519EdhPubKey, x25519Secret);
if (decryptedMessage === null) {
throw new Error("Failed authentication for given ciphertext");
}

return Buffer.from(decryptedMessage);
}
}
35 changes: 35 additions & 0 deletions src-wallet/crypto/pubkeyEncrypt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { assert } from "chai";
import { loadTestWallet, TestWallet } from "../testutils/wallets";
import { PubkeyEncryptor } from "./pubkeyEncryptor";
import { UserPublicKey, UserSecretKey } from "../userKeys";
import { PubkeyDecryptor } from "./pubkeyDecryptor";
import { X25519EncryptedData } from "./x25519EncryptedData";

describe("test address", () => {
let alice: TestWallet, bob: TestWallet, carol: TestWallet;
const sensitiveData = Buffer.from("alice's secret text for bob");
let encryptedDataOfAliceForBob: X25519EncryptedData;

before(async () => {
alice = await loadTestWallet("alice");
bob = await loadTestWallet("bob");
carol = await loadTestWallet("carol");

encryptedDataOfAliceForBob = PubkeyEncryptor.encrypt(sensitiveData, new UserPublicKey(bob.address.pubkey()), new UserSecretKey(alice.secretKey));
});

it("encrypts/decrypts", () => {
const decryptedData = PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey));
assert.equal(sensitiveData.toString('hex'), decryptedData.toString('hex'));
});

it("fails for different originator", () => {
encryptedDataOfAliceForBob.identities.originatorPubKey = carol.address.hex();
assert.throws(() => PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)), "Invalid authentication for encrypted message originator");
});

it("fails for different DH public key", () => {
encryptedDataOfAliceForBob.identities.ephemeralPubKey = carol.address.hex();
assert.throws(() => PubkeyDecryptor.decrypt(encryptedDataOfAliceForBob, new UserSecretKey(bob.secretKey)), "Invalid authentication for encrypted message originator");
});
});
47 changes: 47 additions & 0 deletions src-wallet/crypto/pubkeyEncryptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import crypto from "crypto";
import ed2curve from "ed2curve";
import nacl from "tweetnacl";
import { UserPublicKey, UserSecretKey } from "../userKeys";
import { PubKeyEncCipher, PubKeyEncNonceLength, PubKeyEncVersion } from "./constants";
import { X25519EncryptedData } from "./x25519EncryptedData";

export class PubkeyEncryptor {
static encrypt(data: Buffer, recipientPubKey: UserPublicKey, authSecretKey: UserSecretKey): X25519EncryptedData {
// create a new x25519 keypair that will be used for EDH
const edhPair = nacl.sign.keyPair();
const recipientDHPubKey = ed2curve.convertPublicKey(recipientPubKey.valueOf());
if (recipientDHPubKey === null) {
throw new Error("Could not convert ed25519 public key to x25519");
}
const edhConvertedSecretKey = ed2curve.convertSecretKey(edhPair.secretKey);

// For the nonce we use a random component and a deterministic one based on the message
// - this is so we won't completely rely on the random number generator
const nonceDeterministic = crypto.createHash('sha256').update(data).digest().slice(0, PubKeyEncNonceLength / 2);
const nonceRandom = nacl.randomBytes(PubKeyEncNonceLength / 2);
const nonce = Buffer.concat([nonceDeterministic, nonceRandom]);
const encryptedMessage = nacl.box(data, nonce, recipientDHPubKey, edhConvertedSecretKey);

// Note that the ciphertext is already authenticated for the ephemeral key - but we want it authenticated by
// the ed25519 key which the user interacts with. A signature over H(ciphertext | edhPubKey)
// would be enough
const authMessage = crypto.createHash('sha256').update(
Buffer.concat([encryptedMessage, edhPair.publicKey])
).digest();

const signature = authSecretKey.sign(authMessage);

return new X25519EncryptedData({
version: PubKeyEncVersion,
nonce: Buffer.from(nonce).toString('hex'),
cipher: PubKeyEncCipher,
ciphertext: Buffer.from(encryptedMessage).toString('hex'),
mac: signature.toString('hex'),
identities: {
recipient: recipientPubKey.hex(),
ephemeralPubKey: Buffer.from(edhPair.publicKey).toString('hex'),
originatorPubKey: authSecretKey.generatePublicKey().hex(),
}
});
}
}
15 changes: 15 additions & 0 deletions src-wallet/crypto/randomness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { utils } from "@noble/ed25519";
import { v4 as uuidv4 } from "uuid";
const crypto = require("crypto");

export class Randomness {
salt: Buffer;
iv: Buffer;
id: string;

constructor(init?: Partial<Randomness>) {
this.salt = init?.salt || Buffer.from(utils.randomBytes(32));
this.iv = init?.iv || Buffer.from(utils.randomBytes(16));
this.id = init?.id || uuidv4({ random: crypto.randomBytes(16) });
}
}
Loading

0 comments on commit 8bc371c

Please sign in to comment.