Skip to content

Commit 57fcb26

Browse files
authored
Encode transaction message using AES-GCM (#640)
* Fixed #633 - Updated doc in transaction search criteria * Fixed #637
1 parent ffca22d commit 57fcb26

File tree

6 files changed

+57
-47
lines changed

6 files changed

+57
-47
lines changed

src/core/crypto/Crypto.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as crypto from 'crypto';
2121
import * as CryptoJS from 'crypto-js';
2222

2323
export class Crypto {
24+
private static AES_ALGO = 'aes-256-gcm';
2425
/**
2526
* Encrypt data
2627
* @param {string} data
@@ -93,19 +94,18 @@ export class Crypto {
9394
}
9495
// Processing
9596
const keyPair = KeyPair.createKeyPairFromPrivateKeyString(senderPriv);
96-
const pk = convert.hexToUint8(recipientPub);
97-
const encKey = utility.ua2words(utility.catapult_crypto.deriveSharedKey(keyPair.privateKey, pk), 32);
98-
const encIv = {
99-
iv: utility.ua2words(iv, 16),
100-
};
101-
const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Hex.parse(msg), encKey, encIv);
97+
const encKey = Buffer.from(utility.catapult_crypto.deriveSharedKey(keyPair.privateKey, convert.hexToUint8(recipientPub)), 32);
98+
const encIv = Buffer.from(iv);
99+
const cipher = crypto.createCipheriv(Crypto.AES_ALGO, encKey, encIv);
100+
const encrypted = Buffer.concat([cipher.update(Buffer.from(convert.hexToUint8(msg))), cipher.final()]);
101+
const tag = cipher.getAuthTag();
102102
// Result
103-
const result = convert.uint8ToHex(iv) + CryptoJS.enc.Hex.stringify(encrypted.ciphertext);
103+
const result = tag.toString('hex') + encIv.toString('hex') + encrypted.toString('hex');
104104
return result;
105105
};
106106

107107
/**
108-
* Encode a message
108+
* Encode a message using AES-GCM algorithm
109109
*
110110
* @param {string} senderPriv - A sender private key
111111
* @param {string} recipientPub - A recipient public key
@@ -119,7 +119,7 @@ export class Crypto {
119119
throw new Error('Missing argument !');
120120
}
121121
// Processing
122-
const iv = Crypto.randomBytes(16);
122+
const iv = Crypto.randomBytes(12);
123123
const encoded = Crypto._encode(senderPriv, recipientPub, isHexString ? msg : convert.utf8ToHex(msg), iv);
124124
// Result
125125
return encoded;
@@ -131,31 +131,28 @@ export class Crypto {
131131
* @param {string} recipientPrivate - A recipient private key
132132
* @param {string} senderPublic - A sender public key
133133
* @param {Uint8Array} payload - An encrypted message payload in bytes
134-
* @param {Uint8Array} iv - 16-byte AES initialization vector
134+
* @param {Uint8Array} tagAndIv - 16-bytes AES auth tag and 12-byte AES initialization vector
135135
* @return {string} - The decoded payload as hex
136136
*/
137-
public static _decode = (recipientPrivate: string, senderPublic: string, payload: Uint8Array, iv: Uint8Array): string => {
137+
public static _decode = (recipientPrivate: string, senderPublic: string, payload: Uint8Array, tagAndIv: Uint8Array): string => {
138138
// Error
139139
if (!recipientPrivate || !senderPublic || !payload) {
140140
throw new Error('Missing argument !');
141141
}
142142
// Processing
143143
const keyPair = KeyPair.createKeyPairFromPrivateKeyString(recipientPrivate);
144-
const pk = convert.hexToUint8(senderPublic);
145-
const encKey = utility.ua2words(utility.catapult_crypto.deriveSharedKey(keyPair.privateKey, pk), 32);
146-
const encIv = {
147-
iv: utility.ua2words(iv, 16),
148-
};
149-
const encrypted = {
150-
ciphertext: utility.ua2words(payload, payload.length),
151-
};
152-
const plain = CryptoJS.AES.decrypt(encrypted, encKey, encIv);
144+
const encKey = Buffer.from(utility.catapult_crypto.deriveSharedKey(keyPair.privateKey, convert.hexToUint8(senderPublic)), 32);
145+
const encIv = Buffer.from(new Uint8Array(tagAndIv.buffer, 16, 12));
146+
const encTag = Buffer.from(new Uint8Array(tagAndIv.buffer, 0, 16));
147+
const cipher = crypto.createDecipheriv(Crypto.AES_ALGO, encKey, encIv);
148+
cipher.setAuthTag(encTag);
149+
const decrypted = Buffer.concat([cipher.update(Buffer.from(payload)), cipher.final()]);
153150
// Result
154-
return CryptoJS.enc.Hex.stringify(plain);
151+
return decrypted.toString('hex');
155152
};
156153

157154
/**
158-
* Decode an encrypted message payload
155+
* Decode an encrypted (AES-GCM algorithm) message payload
159156
*
160157
* @param {string} recipientPrivate - A recipient private key
161158
* @param {string} senderPublic - A sender public key
@@ -169,10 +166,15 @@ export class Crypto {
169166
}
170167
// Processing
171168
const binPayload = convert.hexToUint8(payload);
172-
const payloadBuffer = new Uint8Array(binPayload.buffer, 16);
173-
const iv = new Uint8Array(binPayload.buffer, 0, 16);
174-
const decoded = Crypto._decode(recipientPrivate, senderPublic, payloadBuffer, iv);
175-
return decoded.toUpperCase();
169+
const payloadBuffer = new Uint8Array(binPayload.buffer, 16 + 12); //tag + iv
170+
const tagAndIv = new Uint8Array(binPayload.buffer, 0, 16 + 12);
171+
try {
172+
const decoded = Crypto._decode(recipientPrivate, senderPublic, payloadBuffer, tagAndIv);
173+
return decoded.toUpperCase();
174+
} catch {
175+
// To return empty string rather than error throwing if authentication failed
176+
return '';
177+
}
176178
};
177179

178180
/**

src/model/transaction/TransferTransaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export class TransferTransaction extends Transaction {
171171
if (this.message.type === MessageType.PersistentHarvestingDelegationMessage) {
172172
if (this.mosaics.length > 0) {
173173
throw new Error('PersistentDelegationRequestTransaction should be created without Mosaic');
174-
} else if (!/^[0-9a-fA-F]{208}$/.test(this.message.payload)) {
174+
} else if (!/^[0-9a-fA-F]{200}$/.test(this.message.payload)) {
175175
throw new Error('PersistentDelegationRequestTransaction message is invalid');
176176
}
177177
}

test/core/crypto/crypto.spec.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -133,32 +133,40 @@ describe('crypto tests', () => {
133133
describe('test vector cipher', () => {
134134
it('test vector cipher', () => {
135135
// Arrange:
136-
const Private_Key = '3140f94c79f249787d1ec75a97a885980eb8f0a7d9b7aa03e7200296e422b2b6';
136+
const Private_Key = '3140F94C79F249787D1EC75A97A885980EB8F0A7D9B7AA03E7200296E422B2B6';
137137

138138
const Public_Keys = 'C62827148875ACAF05D25D29B1BB1D947396A89CE41CB48888AE6961D9991DDF';
139139

140+
const tags = [
141+
'1B3370262014CB148F7A8CC88D344370',
142+
'D02C42DBF4507B312BA7C6DC2AC11029',
143+
'4924AD697E57E2B84B0677D1B5AA9EF6',
144+
'C3670B1C2021CD5590CD5922C23E93A0',
145+
'19B0DFA6563E22BF5935C229EB212314',
146+
];
147+
140148
const ivs = [
141-
'a73ff5c32f8fd055b09775817a6a3f95',
142-
'91246c2d5493867c4fa3e78f85963677',
143-
'9f8e33d82374dad6aac0e3dbe7aea704',
144-
'6acdf8e01acc8074ddc807281b6af888',
145-
'f2e9f18aeb374965f54d2f4e31189a8f',
149+
'A73FF5C32F8FD055B0977581',
150+
'91246C2D5493867C4FA3E78F',
151+
'9F8E33D82374DAD6AAC0E3DB',
152+
'6ACDF8E01ACC8074DDC80728',
153+
'F2E9F18AEB374965F54D2F4E',
146154
];
147155

148156
const cipherText = [
149-
'EEF67A32E1FE96AF1401DF42DD356A1CAEC5B6B36576357C22232049D174F63E',
150-
'F94355BEF2CBF73E06AF2FF57BB8D72D7090062379062B60E8EF37EA858D8FF4',
151-
'18FF3AB60B01D5D39CFDD50ADDE0F49ECAEE4355B224D0D8A0607455A3DFA823',
152-
'E64795B1B980A6101E9C12824FAA5A4DFC1467F767AA3DC5A990F3A28692A1FA',
153-
'17817662A9B61AF1E9C6F3D7C1D02CAAACEA586E4BD777A68C0765D5231619F3',
157+
'1B1398B84750C9DDEE99164AA1A54C89E9705FDCEBACD05A7B75F1E716',
158+
'7C7B81434A62C8E3056A943334BEE80C9D283BFF27B87D41ABDD3F46FA',
159+
'47DB888FE244202E7E73A1B9CD853C3CD591E872F965386E22306EE568',
160+
'C0BBDF03BF0038BFE662E78553357695CD9D2CC06CCE9A7BCF7460379B',
161+
'44FB7EA399DE68C017081E371AD8F544C50BE707E6D7AF5B4B65D815FB',
154162
];
155163

156164
const clearText = [
157-
'86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c',
158-
'86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c',
159-
'86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c',
160-
'86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c',
161-
'86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c',
165+
'86DDB9E713A8EBF67A51830EFF03B837E147C20D75E67B2A54AA29E98C',
166+
'86DDB9E713A8EBF67A51830EFF03B837E147C20D75E67B2A54AA29E98C',
167+
'86DDB9E713A8EBF67A51830EFF03B837E147C20D75E67B2A54AA29E98C',
168+
'86DDB9E713A8EBF67A51830EFF03B837E147C20D75E67B2A54AA29E98C',
169+
'86DDB9E713A8EBF67A51830EFF03B837E147C20D75E67B2A54AA29E98C',
162170
];
163171

164172
for (let i = 0; i < ivs.length; ++i) {
@@ -168,7 +176,7 @@ describe('crypto tests', () => {
168176
// Act:
169177
const encoded = Crypto._encode(Private_Key, Public_Keys, clearText[i], iv);
170178
// Assert:
171-
expect(encoded.toUpperCase()).to.deep.equal(ivs[i].toUpperCase() + cipherText[i].toUpperCase());
179+
expect(encoded.toUpperCase()).to.deep.equal(tags[i].toUpperCase() + ivs[i].toUpperCase() + cipherText[i].toUpperCase());
172180
}
173181
});
174182
});

test/model/message/PersistentHarvestingDelegationMessage.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('PersistentHarvestingDelegationMessage', () => {
4545
recipient.publicKey,
4646
NetworkType.MIJIN_TEST,
4747
);
48-
expect(encryptedMessage.payload.length).to.be.equal(208);
48+
expect(encryptedMessage.payload.length).to.be.equal(200);
4949
expect(encryptedMessage.type).to.be.equal(MessageType.PersistentHarvestingDelegationMessage);
5050
});
5151

test/model/transaction/PersistentDelegationRequestTransaction.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('PersistentDelegationRequestTransaction', () => {
6868
NetworkType.MIJIN_TEST,
6969
);
7070

71-
expect(persistentDelegationRequestTransaction.message.payload.length).to.be.equal(192 + messageMarker.length);
71+
expect(persistentDelegationRequestTransaction.message.payload.length).to.be.equal(184 + messageMarker.length);
7272
expect(persistentDelegationRequestTransaction.message.payload.includes(messageMarker)).to.be.true;
7373
expect(persistentDelegationRequestTransaction.mosaics.length).to.be.equal(0);
7474
expect(persistentDelegationRequestTransaction.recipientAddress).to.be.instanceof(Address);

test/model/transaction/TransferTransaction.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ describe('TransferTransaction', () => {
267267
PersistentHarvestingDelegationMessage.create(delegatedPrivateKey, recipientPublicKey, NetworkType.MIJIN_TEST),
268268
NetworkType.MIJIN_TEST,
269269
);
270-
expect(transferTransaction.message.payload.length).to.be.equal(192 + messageMarker.length);
270+
expect(transferTransaction.message.payload.length).to.be.equal(184 + messageMarker.length);
271271
expect(transferTransaction.message.payload.includes(messageMarker)).to.be.true;
272272
expect(transferTransaction.mosaics.length).to.be.equal(0);
273273
expect(transferTransaction.recipientAddress).to.be.instanceof(Address);

0 commit comments

Comments
 (0)