Skip to content
This repository has been archived by the owner on May 18, 2022. It is now read-only.

Nem cipher encode and decode. #59

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions src/core/nem/NemCrypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2021 SYMBOL
*
* Licensed under the Apache License, Version 2.0 (the "License"),
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Key } from '@core';
import Ed25519 from '@external';
import { keccakHash } from '@utils';
import * as Crypto from 'crypto';
import { keccak256 } from 'js-sha3';

export class NemCrypto {
/**
* AES cipher algorithm (internal)
*/
private static algorithm = 'aes-256-cbc';

/**
* Derive key for cipher
*
* @param shared - shared secret
* @param salt - A salt
* @param privateKey - A private key
* @param publicKey - A public key
*
* @returns Keccak-256 hash
*/
private static deriveKey(shared: Uint8Array, salt: Uint8Array, privateKey: Key, publicKey: Key): number[] {
Ed25519.crypto_shared_key(shared, publicKey.toBytes(), [...privateKey.toBytes()].reverse(), keccakHash);

for (let i = 0; i < salt.length; i++) {
shared[i] ^= salt[i];
}

return keccak256.digest(shared);
}

/**
* Encode a message
*
* @param privateKey - A sender private key
* @param publicKey - A recipient public key
* @param message - A text message (max 976 bytes)
* @param customIv - An initialization vector
* @param customSalt - A salt
*
* @returns The encoded message
*/
static encode(privateKey: Key, publicKey: Key, message: Uint8Array, customIv?: Uint8Array, customSalt?: Uint8Array): Uint8Array {
// Max payload size is 1024 bytes included iv and salt
if (message.length > 976) throw new Error('Invalid message size!');
if (customIv && customIv.length !== 16) throw new Error('Invalid iv size!');
if (customSalt && customSalt.length !== 32) throw new Error('Invalid salt size!');

const iv = customIv ? customIv : Crypto.randomBytes(16);
const salt = customSalt ? customSalt : Crypto.randomBytes(32);

const shared = new Uint8Array(32);
const key = this.deriveKey(shared, salt, privateKey, publicKey);

const cipher = Crypto.createCipheriv(this.algorithm, Buffer.from(key), iv);
const encrypted = Buffer.concat([cipher.update(message), cipher.final()]);

return Buffer.concat([salt, iv, encrypted]);
}

/**
* Decode an encrypted message payload
*
* @param privateKey - A recipient private key
* @param publicKey - A sender public key
* @param payload - An encrypted message payload (max 1024 bytes)
*
* @returns The decoded message
*/
static decode(privateKey: Key, publicKey: Key, payload: Uint8Array): Uint8Array {
if (payload.length > 1024) throw new Error('Invalid payload size!');

// 32 byte for salt
const salt = payload.slice(0, 32);
// 16 byte for iv
const iv = payload.slice(32, 48);
// 32 byte for payload

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is wrong

const message = payload.slice(48);

const shared = new Uint8Array(32);
const key = this.deriveKey(shared, salt, privateKey, publicKey);

const decipher = Crypto.createDecipheriv(this.algorithm, Buffer.from(key), iv);
return Buffer.concat([decipher.update(message), decipher.final()]);
}
}
10 changes: 9 additions & 1 deletion src/core/nem/external/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ interface INacl {
* @param data - The data to verify.
* @param hasher - Hasher function example KeccakHasher.
*/
crypto_verify_hash(signature: Uint8Array, publicKey: Uint8Array, data: Uint8Array, hasher: Hasher);
crypto_verify_hash(signature: Uint8Array, publicKey: Uint8Array, data: Uint8Array, hasher: Hasher): boolean;
/**
* Derive a shared key from private key based on the hash function.
* @param shared - shared secret.
* @param publicKey - The public key.
* @param privateKey - The private key.
* @param hashFunc - Hash function use to hash the private key.
*/
crypto_shared_key(shared: Uint8Array, publicKey: Uint8Array, privateKey: number[], hashFunc: (data: Uint8Array) => number[]): void;
crypto_modL(r: Uint8Array, x: Float64Array): Uint8Array;
/* number of public key bytes */
crypto_sign_PUBLICKEYBYTES: number;
Expand Down
67 changes: 67 additions & 0 deletions src/core/nem/external/nacl-fast.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,22 @@
return 0;
}

function crypto_shared_key(shared, pk, sk, hash) {
var d = new Uint8Array(64);
var p = [gf(), gf(), gf(), gf()];

d = hash(sk);

d[0] &= 248;
d[31] &= 127;
d[31] |= 64;

var q = [gf(), gf(), gf(), gf()];
unpack(q, pk);
scalarmult(p, q, d);
pack(shared, p);
}

function crypto_sign_hash(sm, keypair, data, hasher) {
var privHash = new Uint8Array(64);
var seededHash = new Uint8Array(64);
Expand Down Expand Up @@ -811,6 +827,55 @@
return 0;
}

function unpack(r, p) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps export unpackneg from js script rather than duplicating it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, make sense 👍🏼

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a double-check on the code, compare with unpackneg vs unpack most of the code is the same but here is the different part.

in unpackneg
if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]);

in unpack
if (par25519(r[0]) !== (p[31]>>7)) Z(r[0], gf0, r[0]);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah dang, right unpackneg() = unpack AND negate, technically we could extract and/or parametrize this, but not sure if's good idea. cc @Jaguar0625

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, i'd recommend refactoring unpack and unpackneg

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gimre-xymcity or could we negate after calling unpackneg?

var t = gf(), chk = gf(), num = gf(),
den = gf(), den2 = gf(), den4 = gf(),
den6 = gf();

set25519(r[2], gf1);
unpack25519(r[1], p);

// num = u = y^2 - 1
// den = v = d * y^2 + 1
S(num, r[1]);
M(den, num, D);
Z(num, num, r[2]);
A(den, r[2], den);

// r[0] = x = sqrt(u / v)
S(den2, den);
S(den4, den2);
M(den6, den4, den2);
M(t, den6, num);
M(t, t, den);

pow2523(t, t);
M(t, t, num);
M(t, t, den);
M(t, t, den);
M(r[0], t, den);

S(chk, r[0]);
M(chk, chk, den);
if (neq25519(chk, num)) {
M(r[0], r[0], I);
}

S(chk, r[0]);
M(chk, chk, den);
if (neq25519(chk, num)) {
console.log("not a valid Ed25519EncodedGroupElement.");
return -1;
}

if (par25519(r[0]) !== (p[31]>>7)) {
Z(r[0], gf0, r[0]);
}

M(r[3], r[0], r[1]);
return 0;
}

var crypto_scalarmult_BYTES = 32,
crypto_scalarmult_SCALARBYTES = 32,
crypto_sign_PUBLICKEYBYTES = 32;
Expand All @@ -819,6 +884,8 @@
crypto_sign_keypair: crypto_sign_keypair,
crypto_sign_hash: crypto_sign_hash,
crypto_verify_hash: crypto_verify_hash,
crypto_shared_key: crypto_shared_key,

crypto_modL: modL,
crypto_sign_PUBLICKEYBYTES: crypto_sign_PUBLICKEYBYTES,
};
Expand Down
1 change: 1 addition & 0 deletions src/core/nem/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './NemAddress';
export * from './NemCrypto';
export * from './NemEnums';
export * from './NemKeyPair';
export * from './NemNetwork';
Expand Down
38 changes: 29 additions & 9 deletions test/BasicVectorTest.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { deriveSharedKey, deriveSharedSecret, encode, Key, KeyPair, Network, SymbolIdGenerator } from '@core';
import { decode, deriveSharedKey, deriveSharedSecret, encode, Key, KeyPair, NemCrypto, Network, SymbolIdGenerator } from '@core';
import { Converter } from '@utils';
import { toBufferLE } from 'bigint-buffer';
import { expect } from 'chai';
Expand Down Expand Up @@ -98,17 +98,37 @@ export const CipherVectorTester = (testCipherVectorFile: string): void => {
describe('cipher - test vector', () => {
tester.run(
testCipherVectorFile,
(item: { privateKey: string; otherPublicKey: string; tag: string; iv: string; cipherText: string; clearText: string }) => {
(item: {
privateKey: string;
otherPublicKey: string;
tag: string;
salt: string;
iv: string;
cipherText: string;
clearText: string;
}) => {
// Arrange:
const privateKey = Key.createFromHex(item.privateKey);
const otherPublicKey = Key.createFromHex(item.otherPublicKey);
const clearText = Converter.hexToUint8(item.clearText);
const iv = Converter.hexToUint8(item.iv);
const salt = item.salt ? Converter.hexToUint8(item.salt) : undefined;

// Act:
const encoded = encode(
Key.createFromHex(item.privateKey),
Key.createFromHex(item.otherPublicKey),
Converter.hexToUint8(item.clearText),
Converter.hexToUint8(item.iv),
);
const encoded = salt
? NemCrypto.encode(privateKey, otherPublicKey, clearText, iv, salt)
: encode(privateKey, otherPublicKey, clearText, iv);

const decoded = salt
? NemCrypto.decode(privateKey, otherPublicKey, Converter.hexToUint8(item.salt + item.iv + item.cipherText))
: decode(privateKey, otherPublicKey, encoded);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is all this conditional based on salt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this vector test is shared between Symbol and Nem.

salt is only used in nem.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the conditionals look awkward to me, especially for tests, so i'd prefer two functions. but ask @gimre-xymcity his opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with what @Jaguar0625 has written, would rather split in 2


// Assert:
const message = ` from ${item.clearText}`;
expect(Converter.uint8ToHex(encoded), `cipher ${message}`).equal(`${item.tag}${item.iv}${item.cipherText}`);
expect(Converter.uint8ToHex(encoded), `cipher encode ${message}`).equal(
`${salt ? item.salt : item.tag}${item.iv}${item.cipherText}`,
);
expect(Converter.uint8ToHex(decoded), `cipher decoded ${message}`).equal(item.clearText);
},
'cipher test',
);
Expand Down
113 changes: 113 additions & 0 deletions test/core/nem/NemCrypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Key, NemCrypto, NemKeyPair } from '@core';
import { Converter } from '@utils';
import { expect } from 'chai';

describe('Nem crypto cipher', () => {
// Arrange:
const deterministicSenderPrivateKey = Key.createFromHex('575DBB3062267EFF57C970A336EBBC8FBCFE12C5BD3ED7BC11EB0481D7704CED');
const deterministicReceiverPrivateKey = Key.createFromHex('5B0E3FA5D3B49A79022D7C1E121BA1CBBF4DB5821F47AB8C708EF88DEFC29BFE');
const sender = new NemKeyPair(deterministicSenderPrivateKey);
const recipient = new NemKeyPair(deterministicReceiverPrivateKey);
const message = 'Nem is awesome!';
const encryptedHex =
'26A5EEBD8B959F664DB060DE9E8265BD4A4597D2BE9DAA2C481042879BEA9F995790143301428700938A0ABB7D81224996058BECCAB21970239EE94A5C587429';

describe('encode & decode message', () => {
it('Can encode message with sender private key', () => {
// Arrange:
const iv = Converter.hexToUint8('5790143301428700938A0ABB7D812249');
const salt = Converter.hexToUint8('26A5EEBD8B959F664DB060DE9E8265BD4A4597D2BE9DAA2C481042879BEA9F99');

// Act:
const encoded = NemCrypto.encode(sender.privateKey, recipient.publicKey, Converter.utf8ToUint8(message), iv, salt);

// Assert:
expect(encryptedHex).equal(Converter.uint8ToHex(encoded));
});

it('Can decode message with recipient private key', () => {
// Act:
const decoded = NemCrypto.decode(recipient.privateKey, sender.publicKey, Converter.hexToUint8(encryptedHex));

// Assert:
expect(message).equal(Converter.uint8ToUtf8(decoded));
});

it('Roundtrip decode encode', () => {
// Act:
const decrypted = NemCrypto.decode(recipient.privateKey, sender.publicKey, Converter.hexToUint8(encryptedHex));
const encrypted = NemCrypto.encode(sender.privateKey, recipient.publicKey, decrypted);

// Assert:
expect(Converter.uint8ToUtf8(decrypted)).equal(message);
expect(encrypted.length).equal(Converter.hexToUint8(encryptedHex).length);
});

it('Roundtrip encode decode', () => {
// Act:
const encrypted = NemCrypto.encode(sender.privateKey, recipient.publicKey, Converter.utf8ToUint8(message));
const decrypted = NemCrypto.decode(recipient.privateKey, sender.publicKey, encrypted);

// Assert:
expect(Converter.uint8ToUtf8(decrypted)).equal(message);
});

it('Can encode message in max size 976 btyes', () => {
// Arrange:
const message = new Uint8Array(976);

// Act:
const encoded = () => NemCrypto.encode(sender.privateKey, recipient.publicKey, message);

// Assert:
expect(encoded).to.not.throw(Error);
});

it('Encoding throw error if message exceed 976 btyes', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a tests checking that 976 is actually passing ;)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'bytes' (multiple)

// Arrange:
const message = new Uint8Array(977);

// Act:
const encoded = () => NemCrypto.encode(sender.privateKey, recipient.publicKey, message);

// Assert:
expect(encoded).to.throw(Error);
});

it('Encoding throw error if iv exceed 16 btyes', () => {
// Arrange:
const message = new Uint8Array(32);
const customIv = new Uint8Array(17);

// Act:
const encoded = () => NemCrypto.encode(sender.privateKey, recipient.publicKey, message, customIv);

// Assert:
expect(encoded).to.throw(Error);
});

it('Encoding throw error if salt exceed 32 btyes', () => {
// Arrange:
const message = new Uint8Array(32);
const customIv = new Uint8Array(16);
const customSalt = new Uint8Array(33);

// Act:
const encoded = () => NemCrypto.encode(sender.privateKey, recipient.publicKey, message, customIv, customSalt);

// Assert:
expect(encoded).to.throw(Error);
});

it('Decoding throw error if payload exceed 1024 btyes', () => {
// Arrange:
const exceedBtyesPayload = new Uint8Array(1025);

// Act:
const decoded = () => NemCrypto.decode(recipient.privateKey, sender.publicKey, exceedBtyesPayload);

// Assert:
expect(decoded).to.throw(Error);
});
});
});
2 changes: 1 addition & 1 deletion test/test-vector
5 changes: 5 additions & 0 deletions test/vector-tests/AllTests.vector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ describe('Nem', () => {
const networks = NemNetwork.list();
AddressMosaicIdTester(networks, vectorFile);
});

describe('test-cipher vector', () => {
const vectorFile = path.join(__dirname, '../test-vector/nem/4.test-cipher.json');
CipherVectorTester(vectorFile);
});
});

describe('Symbol', () => {
Expand Down