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 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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue
Block a user