Implement GCM mode in the new draft

Also, implement additional data for GCM
This commit is contained in:
Daniel Huigens 2018-04-19 17:15:25 +02:00
parent d5a7cb3037
commit e061df113c
4 changed files with 129 additions and 84 deletions

View File

@ -32,93 +32,93 @@ const webCrypto = util.getWebCrypto(); // no GCM support in IE11, Safari 9
const nodeCrypto = util.getNodeCrypto();
const Buffer = util.getNodeBuffer();
const blockLength = 16;
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';
/**
* Encrypt plaintext input.
* Class to en/decrypt using GCM mode.
* @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} 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') {
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
return webEncrypt(plaintext, key, iv);
} else if (util.getNodeCrypto()) { // Node crypto library
return nodeEncrypt(plaintext, key, iv);
} // asm.js fallback
return Promise.resolve(AES_GCM.encrypt(plaintext, key, iv));
key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']);
return {
encrypt: async function(pt, iv, adata=new Uint8Array()) {
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.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} ciphertext The ciphertext input to be decrypted
* @param {Uint8Array} key The encryption key
* Get GCM nonce. Note: this operation is not defined by the standard.
* A future version of the standard may define GCM mode differently,
* hopefully under a different ID (we use Private/Experimental algorithm
* ID 100) so that we can maintain backwards compatibility.
* @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) {
if (cipher.substr(0, 3) !== 'aes') {
return Promise.reject(new Error('GCM mode supports only AES cipher'));
GCM.getNonce = function(iv, chunkIndex) {
const nonce = iv.slice();
for (let i = 0; i < chunkIndex.length; i++) {
nonce[4 + i] ^= chunkIndex[i];
}
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
return nonce;
};
GCM.blockLength = blockLength;
GCM.ivLength = ivLength;
//////////////////////////
// //
// 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));
}
export default GCM;

View File

@ -120,7 +120,8 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith
);
this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises)));
} 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;
};
@ -135,6 +136,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith
SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) {
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 modeInstance = await mode(sessionKeyAlgorithm, key);
this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV
let data = this.packets.write();
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);
adataView.setInt32(13 + 4, data.length); // Should be setInt64(13, ...)
const encryptedPromises = [];
const modeInstance = await mode(sessionKeyAlgorithm, key);
for (let chunkIndex = 0; chunkIndex === 0 || data.length;) {
encryptedPromises.push(
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));
} else {
this.encrypted = await mode.encrypt(sessionKeyAlgorithm, data, key, this.iv);
this.encrypted = await modeInstance.encrypt(data, this.iv);
}
return true;
};

View File

@ -304,21 +304,22 @@ describe('API functional testing', function() {
});
}
function testAESGCM(plaintext) {
function testAESGCM(plaintext, nativeDecrypt) {
symmAlgos.forEach(function(algo) {
if(algo.substr(0,3) === 'aes') {
it(algo, async function() {
const key = await crypto.generateSessionKey(algo);
const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength);
let modeInstance = await crypto.gcm(algo, key);
return crypto.gcm.encrypt(
algo, util.str_to_Uint8Array(plaintext), key, iv
).then(function(ciphertext) {
return crypto.gcm.decrypt(algo, ciphertext, key, iv);
}).then(function(decrypted) {
const decryptedStr = util.Uint8Array_to_str(decrypted);
expect(decryptedStr).to.equal(plaintext);
});
const ciphertext = await modeInstance.encrypt(util.str_to_Uint8Array(plaintext), iv);
openpgp.config.use_native = nativeDecrypt;
modeInstance = await crypto.gcm(algo, key);
const decrypted = await modeInstance.decrypt(util.str_to_Uint8Array(util.Uint8Array_to_str(ciphertext)), iv);
const decryptedStr = util.Uint8Array_to_str(decrypted);
expect(decryptedStr).to.equal(plaintext);
});
}
});
@ -355,7 +356,7 @@ describe('API functional testing', function() {
openpgp.config.use_native = use_nativeVal;
});
testAESGCM("12345678901234567890123456789012345678901234567890");
testAESGCM("12345678901234567890123456789012345678901234567890", true);
});
describe('Symmetric AES-GCM (asm.js fallback)', function() {
@ -368,7 +369,20 @@ describe('API functional testing', function() {
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 () {

View File

@ -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, {
if: true,
beforeEach: function() {