Skip to content

Commit

Permalink
Merge pull request #8 from ElrondNetwork/x25519-encryption
Browse files Browse the repository at this point in the history
X25519 encryption
  • Loading branch information
ccorcoveanu authored Aug 24, 2022
2 parents 6058931 + b48d3b8 commit e414dea
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src-wallet/crypto/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export const Version = 4;
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";
2 changes: 1 addition & 1 deletion src-wallet/crypto/decryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DigestAlgorithm } from "./constants";
import { Err } from "../errors";

export class Decryptor {
public static decrypt(data: EncryptedData, password: string): Buffer {
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");
Expand Down
2 changes: 1 addition & 1 deletion src-wallet/crypto/encryptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CipherAlgorithm, DigestAlgorithm, Version, KeyDerivationFunction } from
import {EncryptedData} from "./encryptedData";

export class Encryptor {
public static encrypt(data: Buffer, password: string, randomness: Randomness = new Randomness()): EncryptedData {
static encrypt(data: Buffer, password: string, randomness: Randomness = new Randomness()): EncryptedData {
const kdParams = new ScryptKeyDerivationParams();
const derivedKey = kdParams.generateDerivedKey(Buffer.from(password), randomness.salt);
const derivedKeyFirstHalf = derivedKey.slice(0, 16);
Expand Down
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 nacl from "tweetnacl";
import ed2curve from "ed2curve";
import crypto from "crypto";
import { X25519EncryptedData } from "./x25519EncryptedData";
import { UserPublicKey, UserSecretKey } from "../userKeys";
import { PubKeyEncCipher, PubKeyEncNonceLength, PubKeyEncVersion } from "./constants";

export class PubkeyEncryptor {
static encrypt(data: Buffer, recipientPubKey: UserPublicKey, authSecretKey: UserSecretKey): X25519EncryptedData {
// create a new x225519 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 elrond 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(),
}
});
}
}
45 changes: 45 additions & 0 deletions src-wallet/crypto/x25519EncryptedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export class X25519EncryptedData {
nonce: string;
version: number;
cipher: string;
ciphertext: string;
mac: string;
identities: {
recipient: string,
ephemeralPubKey: string,
originatorPubKey: string,
};

constructor(data: Omit<X25519EncryptedData, "toJSON">) {
this.nonce = data.nonce;
this.version = data.version;
this.cipher = data.cipher;
this.ciphertext = data.ciphertext;
this.mac = data.mac;
this.identities = data.identities;
}

toJSON(): any {
return {
version: this.version,
nonce: this.nonce,
identities: this.identities,
crypto: {
ciphertext: this.ciphertext,
cipher: this.cipher,
mac: this.mac,
}
};
}

static fromJSON(data: any): X25519EncryptedData {
return new X25519EncryptedData({
nonce: data.nonce,
version: data.version,
ciphertext: data.crypto.ciphertext,
cipher: data.crypto.cipher,
mac: data.crypto.mac,
identities: data.identities,
});
}
}

0 comments on commit e414dea

Please sign in to comment.