-
Notifications
You must be signed in to change notification settings - Fork 1
Nem cipher encode and decode. #59
base: main
Are you sure you want to change the base?
Changes from 10 commits
2825500
0b865ba
a1e7f5e
bcd808d
ae54bb6
640e6c1
06f20b0
303f4c2
8b906f7
6dadb16
285932d
b63b6d1
102141d
2d8d2b7
cb3dd4b
aed8479
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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()]); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -811,6 +827,55 @@ | |
return 0; | ||
} | ||
|
||
function unpack(r, p) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps export There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, make sense 👍🏼 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did a double-check on the code, compare with in in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea, i'd recommend refactoring unpack and unpackneg There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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, | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is all this conditional based on salt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this vector test is shared between Symbol and Nem.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
); | ||
|
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', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add a tests checking that 976 is actually passing ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment is wrong