Implement GCM mode in the new draft
Also, implement additional data for GCM
This commit is contained in:
parent
d5a7cb3037
commit
e061df113c
|
@ -32,93 +32,93 @@ const webCrypto = util.getWebCrypto(); // no GCM support in IE11, Safari 9
|
||||||
const nodeCrypto = util.getNodeCrypto();
|
const nodeCrypto = util.getNodeCrypto();
|
||||||
const Buffer = util.getNodeBuffer();
|
const Buffer = util.getNodeBuffer();
|
||||||
|
|
||||||
|
const blockLength = 16;
|
||||||
const ivLength = 12; // size of the IV in bytes
|
const ivLength = 12; // size of the IV in bytes
|
||||||
const TAG_LEN = 16; // size of the tag in bytes
|
const tagLength = 16; // size of the tag in bytes
|
||||||
const ALGO = 'AES-GCM';
|
const ALGO = 'AES-GCM';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt plaintext input.
|
* Class to en/decrypt using GCM mode.
|
||||||
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
|
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
|
||||||
* @param {Uint8Array} plaintext The cleartext input to be encrypted
|
|
||||||
* @param {Uint8Array} key The encryption key
|
* @param {Uint8Array} key The encryption key
|
||||||
* @param {Uint8Array} iv The initialization vector (12 bytes)
|
|
||||||
* @returns {Promise<Uint8Array>} The ciphertext output
|
|
||||||
*/
|
*/
|
||||||
function encrypt(cipher, plaintext, key, iv) {
|
async function GCM(cipher, key) {
|
||||||
if (cipher.substr(0, 3) !== 'aes') {
|
if (cipher.substr(0, 3) !== 'aes') {
|
||||||
return Promise.reject(new Error('GCM mode supports only AES cipher'));
|
throw new Error('GCM mode supports only AES cipher');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||||
return webEncrypt(plaintext, key, iv);
|
key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']);
|
||||||
} else if (util.getNodeCrypto()) { // Node crypto library
|
|
||||||
return nodeEncrypt(plaintext, key, iv);
|
return {
|
||||||
} // asm.js fallback
|
encrypt: async function(pt, iv, adata=new Uint8Array()) {
|
||||||
return Promise.resolve(AES_GCM.encrypt(plaintext, key, iv));
|
const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata }, key, pt);
|
||||||
|
return new Uint8Array(ct);
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: async function(ct, iv, adata=new Uint8Array()) {
|
||||||
|
const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata }, key, ct);
|
||||||
|
return new Uint8Array(pt);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (util.getNodeCrypto()) { // Node crypto library
|
||||||
|
key = new Buffer(key);
|
||||||
|
|
||||||
|
return {
|
||||||
|
encrypt: async function(pt, iv, adata=new Uint8Array()) {
|
||||||
|
pt = new Buffer(pt);
|
||||||
|
iv = new Buffer(iv);
|
||||||
|
adata = new Buffer(adata);
|
||||||
|
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
|
||||||
|
en.setAAD(adata);
|
||||||
|
const ct = Buffer.concat([en.update(pt), en.final(), en.getAuthTag()]); // append auth tag to ciphertext
|
||||||
|
return new Uint8Array(ct);
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: async function(ct, iv, adata=new Uint8Array()) {
|
||||||
|
ct = new Buffer(ct);
|
||||||
|
iv = new Buffer(iv);
|
||||||
|
adata = new Buffer(adata);
|
||||||
|
const de = new nodeCrypto.createDecipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
|
||||||
|
de.setAAD(adata);
|
||||||
|
de.setAuthTag(ct.slice(ct.length - tagLength, ct.length)); // read auth tag at end of ciphertext
|
||||||
|
const pt = Buffer.concat([de.update(ct.slice(0, ct.length - tagLength)), de.final()]);
|
||||||
|
return new Uint8Array(pt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
encrypt: async function(pt, iv, adata) {
|
||||||
|
return AES_GCM.encrypt(pt, key, iv, adata);
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt: async function(ct, iv, adata) {
|
||||||
|
return AES_GCM.decrypt(ct, key, iv, adata);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt ciphertext input.
|
* Get GCM nonce. Note: this operation is not defined by the standard.
|
||||||
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
|
* A future version of the standard may define GCM mode differently,
|
||||||
* @param {Uint8Array} ciphertext The ciphertext input to be decrypted
|
* hopefully under a different ID (we use Private/Experimental algorithm
|
||||||
* @param {Uint8Array} key The encryption key
|
* ID 100) so that we can maintain backwards compatibility.
|
||||||
* @param {Uint8Array} iv The initialization vector (12 bytes)
|
* @param {Uint8Array} iv The initialization vector (12 bytes)
|
||||||
* @returns {Promise<Uint8Array>} The plaintext output
|
* @param {Uint8Array} chunkIndex The chunk index (8 bytes)
|
||||||
*/
|
*/
|
||||||
function decrypt(cipher, ciphertext, key, iv) {
|
GCM.getNonce = function(iv, chunkIndex) {
|
||||||
if (cipher.substr(0, 3) !== 'aes') {
|
const nonce = iv.slice();
|
||||||
return Promise.reject(new Error('GCM mode supports only AES cipher'));
|
for (let i = 0; i < chunkIndex.length; i++) {
|
||||||
|
nonce[4 + i] ^= chunkIndex[i];
|
||||||
}
|
}
|
||||||
|
return nonce;
|
||||||
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
|
||||||
return webDecrypt(ciphertext, key, iv);
|
|
||||||
} else if (util.getNodeCrypto()) { // Node crypto library
|
|
||||||
return nodeDecrypt(ciphertext, key, iv);
|
|
||||||
} // asm.js fallback
|
|
||||||
return Promise.resolve(AES_GCM.decrypt(ciphertext, key, iv));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
ivLength,
|
|
||||||
encrypt,
|
|
||||||
decrypt
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
GCM.blockLength = blockLength;
|
||||||
|
GCM.ivLength = ivLength;
|
||||||
|
|
||||||
//////////////////////////
|
export default GCM;
|
||||||
// //
|
|
||||||
// Helper functions //
|
|
||||||
// //
|
|
||||||
//////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
function webEncrypt(pt, key, iv) {
|
|
||||||
return webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt'])
|
|
||||||
.then(keyObj => webCrypto.encrypt({ name: ALGO, iv }, keyObj, pt))
|
|
||||||
.then(ct => new Uint8Array(ct));
|
|
||||||
}
|
|
||||||
|
|
||||||
function webDecrypt(ct, key, iv) {
|
|
||||||
return webCrypto.importKey('raw', key, { name: ALGO }, false, ['decrypt'])
|
|
||||||
.then(keyObj => webCrypto.decrypt({ name: ALGO, iv }, keyObj, ct))
|
|
||||||
.then(pt => new Uint8Array(pt));
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeEncrypt(pt, key, iv) {
|
|
||||||
pt = new Buffer(pt);
|
|
||||||
key = new Buffer(key);
|
|
||||||
iv = new Buffer(iv);
|
|
||||||
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
|
|
||||||
const ct = Buffer.concat([en.update(pt), en.final(), en.getAuthTag()]); // append auth tag to ciphertext
|
|
||||||
return Promise.resolve(new Uint8Array(ct));
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeDecrypt(ct, key, iv) {
|
|
||||||
ct = new Buffer(ct);
|
|
||||||
key = new Buffer(key);
|
|
||||||
iv = new Buffer(iv);
|
|
||||||
const de = new nodeCrypto.createDecipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
|
|
||||||
de.setAuthTag(ct.slice(ct.length - TAG_LEN, ct.length)); // read auth tag at end of ciphertext
|
|
||||||
const pt = Buffer.concat([de.update(ct.slice(0, ct.length - TAG_LEN)), de.final()]);
|
|
||||||
return Promise.resolve(new Uint8Array(pt));
|
|
||||||
}
|
|
||||||
|
|
|
@ -120,7 +120,8 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith
|
||||||
);
|
);
|
||||||
this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises)));
|
this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises)));
|
||||||
} else {
|
} else {
|
||||||
this.packets.read(await mode.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv));
|
const modeInstance = await mode(sessionKeyAlgorithm, key);
|
||||||
|
this.packets.read(await modeInstance.decrypt(this.encrypted, this.iv));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
@ -135,6 +136,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith
|
||||||
SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) {
|
SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) {
|
||||||
this.aeadAlgo = config.aead_protect_version === 4 ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.gcm;
|
this.aeadAlgo = config.aead_protect_version === 4 ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.gcm;
|
||||||
const mode = crypto[enums.read(enums.aead, this.aeadAlgo)];
|
const mode = crypto[enums.read(enums.aead, this.aeadAlgo)];
|
||||||
|
const modeInstance = await mode(sessionKeyAlgorithm, key);
|
||||||
this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV
|
this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV
|
||||||
let data = this.packets.write();
|
let data = this.packets.write();
|
||||||
if (config.aead_protect_version === 4) {
|
if (config.aead_protect_version === 4) {
|
||||||
|
@ -149,7 +151,6 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith
|
||||||
adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0);
|
adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0);
|
||||||
adataView.setInt32(13 + 4, data.length); // Should be setInt64(13, ...)
|
adataView.setInt32(13 + 4, data.length); // Should be setInt64(13, ...)
|
||||||
const encryptedPromises = [];
|
const encryptedPromises = [];
|
||||||
const modeInstance = await mode(sessionKeyAlgorithm, key);
|
|
||||||
for (let chunkIndex = 0; chunkIndex === 0 || data.length;) {
|
for (let chunkIndex = 0; chunkIndex === 0 || data.length;) {
|
||||||
encryptedPromises.push(
|
encryptedPromises.push(
|
||||||
modeInstance.encrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray)
|
modeInstance.encrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray)
|
||||||
|
@ -165,7 +166,7 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith
|
||||||
);
|
);
|
||||||
this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises));
|
this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises));
|
||||||
} else {
|
} else {
|
||||||
this.encrypted = await mode.encrypt(sessionKeyAlgorithm, data, key, this.iv);
|
this.encrypted = await modeInstance.encrypt(data, this.iv);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -304,22 +304,23 @@ describe('API functional testing', function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function testAESGCM(plaintext) {
|
function testAESGCM(plaintext, nativeDecrypt) {
|
||||||
symmAlgos.forEach(function(algo) {
|
symmAlgos.forEach(function(algo) {
|
||||||
if(algo.substr(0,3) === 'aes') {
|
if(algo.substr(0,3) === 'aes') {
|
||||||
it(algo, async function() {
|
it(algo, async function() {
|
||||||
const key = await crypto.generateSessionKey(algo);
|
const key = await crypto.generateSessionKey(algo);
|
||||||
const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength);
|
const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength);
|
||||||
|
let modeInstance = await crypto.gcm(algo, key);
|
||||||
|
|
||||||
return crypto.gcm.encrypt(
|
const ciphertext = await modeInstance.encrypt(util.str_to_Uint8Array(plaintext), iv);
|
||||||
algo, util.str_to_Uint8Array(plaintext), key, iv
|
|
||||||
).then(function(ciphertext) {
|
openpgp.config.use_native = nativeDecrypt;
|
||||||
return crypto.gcm.decrypt(algo, ciphertext, key, iv);
|
modeInstance = await crypto.gcm(algo, key);
|
||||||
}).then(function(decrypted) {
|
|
||||||
|
const decrypted = await modeInstance.decrypt(util.str_to_Uint8Array(util.Uint8Array_to_str(ciphertext)), iv);
|
||||||
const decryptedStr = util.Uint8Array_to_str(decrypted);
|
const decryptedStr = util.Uint8Array_to_str(decrypted);
|
||||||
expect(decryptedStr).to.equal(plaintext);
|
expect(decryptedStr).to.equal(plaintext);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -355,7 +356,7 @@ describe('API functional testing', function() {
|
||||||
openpgp.config.use_native = use_nativeVal;
|
openpgp.config.use_native = use_nativeVal;
|
||||||
});
|
});
|
||||||
|
|
||||||
testAESGCM("12345678901234567890123456789012345678901234567890");
|
testAESGCM("12345678901234567890123456789012345678901234567890", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Symmetric AES-GCM (asm.js fallback)', function() {
|
describe('Symmetric AES-GCM (asm.js fallback)', function() {
|
||||||
|
@ -368,7 +369,20 @@ describe('API functional testing', function() {
|
||||||
openpgp.config.use_native = use_nativeVal;
|
openpgp.config.use_native = use_nativeVal;
|
||||||
});
|
});
|
||||||
|
|
||||||
testAESGCM("12345678901234567890123456789012345678901234567890");
|
testAESGCM("12345678901234567890123456789012345678901234567890", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Symmetric AES-GCM (native encrypt, asm.js decrypt)', function() {
|
||||||
|
let use_nativeVal;
|
||||||
|
beforeEach(function() {
|
||||||
|
use_nativeVal = openpgp.config.use_native;
|
||||||
|
openpgp.config.use_native = true;
|
||||||
|
});
|
||||||
|
afterEach(function() {
|
||||||
|
openpgp.config.use_native = use_nativeVal;
|
||||||
|
});
|
||||||
|
|
||||||
|
testAESGCM("12345678901234567890123456789012345678901234567890", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Asymmetric using RSA with eme_pkcs1 padding', function () {
|
it('Asymmetric using RSA with eme_pkcs1 padding', function () {
|
||||||
|
|
|
@ -674,6 +674,36 @@ describe('OpenPGP.js public api tests', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tryTests('GCM mode (draft04, asm.js)', tests, {
|
||||||
|
if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(),
|
||||||
|
beforeEach: function() {
|
||||||
|
openpgp.config.use_native = false;
|
||||||
|
openpgp.config.aead_protect = true;
|
||||||
|
openpgp.config.aead_protect_version = 4;
|
||||||
|
openpgp.config.aead_mode = openpgp.enums.aead.gcm;
|
||||||
|
|
||||||
|
// Monkey-patch AEAD feature flag
|
||||||
|
publicKey.keys[0].users[0].selfCertifications[0].features = [7];
|
||||||
|
publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
|
||||||
|
publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tryTests('GCM mode (draft04, native)', tests, {
|
||||||
|
if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(),
|
||||||
|
beforeEach: function() {
|
||||||
|
openpgp.config.use_native = true;
|
||||||
|
openpgp.config.aead_protect = true;
|
||||||
|
openpgp.config.aead_protect_version = 4;
|
||||||
|
openpgp.config.aead_mode = openpgp.enums.aead.gcm;
|
||||||
|
|
||||||
|
// Monkey-patch AEAD feature flag
|
||||||
|
publicKey.keys[0].users[0].selfCertifications[0].features = [7];
|
||||||
|
publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
|
||||||
|
publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tryTests('EAX mode (asm.js)', tests, {
|
tryTests('EAX mode (asm.js)', tests, {
|
||||||
if: true,
|
if: true,
|
||||||
beforeEach: function() {
|
beforeEach: function() {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user