Allow reusing EAX/OCB instances with the same key

This is useful for chunked encryption in draft04
This commit is contained in:
Daniel Huigens 2018-04-12 15:01:37 +02:00
parent e24b46192d
commit 2f849063f9
7 changed files with 352 additions and 328 deletions

View File

@ -47,79 +47,113 @@ class OMAC extends CMAC {
} }
} }
class CTR {
constructor(key) {
if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
this.key = webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']);
this.ctr = this.webCtr;
} else if (util.getNodeCrypto()) { // Node crypto library
this.key = new Buffer(key);
this.ctr = this.nodeCtr;
} else {
// asm.js fallback
this.key = key;
}
}
webCtr(pt, iv) {
return this.key
.then(keyObj => webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, keyObj, pt))
.then(ct => new Uint8Array(ct));
}
nodeCtr(pt, iv) {
pt = new Buffer(pt);
iv = new Buffer(iv);
const en = new nodeCrypto.createCipheriv('aes-' + (this.key.length * 8) + '-ctr', this.key, iv);
const ct = Buffer.concat([en.update(pt), en.final()]);
return Promise.resolve(new Uint8Array(ct));
}
ctr(pt, iv) {
return Promise.resolve(AES_CTR.encrypt(pt, this.key, iv));
}
}
class EAX {
/** /**
* Encrypt plaintext input. * Class to en/decrypt using EAX 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} nonce The nonce (16 bytes)
* @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext output
*/ */
async function encrypt(cipher, plaintext, key, nonce, adata) { constructor(cipher, key) {
if (cipher.substr(0, 3) !== 'aes') { if (cipher.substr(0, 3) !== 'aes') {
throw new Error('EAX mode supports only AES cipher'); throw new Error('EAX mode supports only AES cipher');
} }
const omac = new OMAC(key); const omac = new OMAC(key);
const _nonce = omac.mac(zero, nonce); this.omac = omac.mac.bind(omac);
const _adata = omac.mac(one, adata); const ctr = new CTR(key);
const ciphered = await CTR(plaintext, key, _nonce); this.ctr = ctr.ctr.bind(ctr);
const _ciphered = omac.mac(two, ciphered); }
const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac.mac(*).length === tagLength.
/**
* Encrypt plaintext input.
* @param {Uint8Array} plaintext The cleartext input to be encrypted
* @param {Uint8Array} nonce The nonce (16 bytes)
* @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext output
*/
async encrypt(plaintext, nonce, adata) {
const _nonce = this.omac(zero, nonce);
const _adata = this.omac(one, adata);
const ciphered = await this.ctr(plaintext, _nonce);
const _ciphered = this.omac(two, ciphered);
const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac(*).length === tagLength.
return concat(ciphered, tag); return concat(ciphered, tag);
} }
/** /**
* Decrypt ciphertext input. * 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} ciphertext The ciphertext input to be decrypted
* @param {Uint8Array} key The encryption key
* @param {Uint8Array} nonce The nonce (16 bytes) * @param {Uint8Array} nonce The nonce (16 bytes)
* @param {Uint8Array} adata Associated data to verify * @param {Uint8Array} adata Associated data to verify
* @returns {Promise<Uint8Array>} The plaintext output * @returns {Promise<Uint8Array>} The plaintext output
*/ */
async function decrypt(cipher, ciphertext, key, nonce, adata) { async decrypt(ciphertext, nonce, adata) {
if (cipher.substr(0, 3) !== 'aes') {
throw new Error('EAX mode supports only AES cipher');
}
if (ciphertext.length < tagLength) throw new Error('Invalid EAX ciphertext'); if (ciphertext.length < tagLength) throw new Error('Invalid EAX ciphertext');
const ciphered = ciphertext.subarray(0, ciphertext.length - tagLength); const ciphered = ciphertext.subarray(0, ciphertext.length - tagLength);
const tag = ciphertext.subarray(ciphertext.length - tagLength); const tag = ciphertext.subarray(ciphertext.length - tagLength);
const omac = new OMAC(key); const _nonce = this.omac(zero, nonce);
const _nonce = omac.mac(zero, nonce); const _adata = this.omac(one, adata);
const _adata = omac.mac(one, adata); const _ciphered = this.omac(two, ciphered);
const _ciphered = omac.mac(two, ciphered); const _tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac(*).length === tagLength.
const _tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac.mac(*).length === tagLength.
if (!util.equalsUint8Array(tag, _tag)) throw new Error('Authentication tag mismatch in EAX ciphertext'); if (!util.equalsUint8Array(tag, _tag)) throw new Error('Authentication tag mismatch in EAX ciphertext');
const plaintext = await CTR(ciphered, key, _nonce); const plaintext = await this.ctr(ciphered, _nonce);
return plaintext; return plaintext;
} }
}
/** /**
* Get EAX nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.1|RFC4880bis-04, section 5.16.1}. * Get EAX nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.1|RFC4880bis-04, section 5.16.1}.
* @param {Uint8Array} iv The initialization vector (16 bytes) * @param {Uint8Array} iv The initialization vector (16 bytes)
* @param {Uint8Array} chunkIndex The chunk index (8 bytes) * @param {Uint8Array} chunkIndex The chunk index (8 bytes)
*/ */
function getNonce(iv, chunkIndex) { EAX.getNonce = function(iv, chunkIndex) {
const nonce = iv.slice(); const nonce = iv.slice();
for (let i = 0; i < chunkIndex.length; i++) { for (let i = 0; i < chunkIndex.length; i++) {
nonce[8 + i] ^= chunkIndex[i]; nonce[8 + i] ^= chunkIndex[i];
} }
return nonce; return nonce;
}
export default {
blockLength,
ivLength,
encrypt,
decrypt,
getNonce
}; };
EAX.blockLength = blockLength;
EAX.ivLength = ivLength;
export default EAX;
////////////////////////// //////////////////////////
// // // //
@ -135,27 +169,3 @@ function xor3(a, b, c) {
function concat(...arrays) { function concat(...arrays) {
return util.concatUint8Array(arrays); return util.concatUint8Array(arrays);
} }
function CTR(plaintext, key, iv) {
if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
return webCtr(plaintext, key, iv);
} else if (util.getNodeCrypto()) { // Node crypto library
return nodeCtr(plaintext, key, iv);
} // asm.js fallback
return Promise.resolve(AES_CTR.encrypt(plaintext, key, iv));
}
function webCtr(pt, key, iv) {
return webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt'])
.then(keyObj => webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, keyObj, pt))
.then(ct => new Uint8Array(ct));
}
function nodeCtr(pt, key, iv) {
pt = new Buffer(pt);
key = new Buffer(key);
iv = new Buffer(iv);
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-ctr', key, iv);
const ct = Buffer.concat([en.update(pt), en.final()]);
return Promise.resolve(new Uint8Array(ct));
}

View File

@ -79,7 +79,18 @@ function double(S) {
const zeros_16 = zeros(16); const zeros_16 = zeros(16);
const one = new Uint8Array([1]); const one = new Uint8Array([1]);
function constructKeyVariables(cipher, key, text, adata) { class OCB {
/**
* Class to en/decrypt using OCB mode.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} key The encryption key
*/
constructor(cipher, key) {
this.max_ntz = 0;
this.constructKeyVariables(cipher, key);
}
constructKeyVariables(cipher, key) {
const aes = new ciphers[cipher](key); const aes = new ciphers[cipher](key);
const encipher = aes.encrypt.bind(aes); const encipher = aes.encrypt.bind(aes);
const decipher = aes.decrypt.bind(aes); const decipher = aes.decrypt.bind(aes);
@ -89,24 +100,29 @@ function constructKeyVariables(cipher, key, text, adata) {
const L = []; const L = [];
L[0] = double(L_$); L[0] = double(L_$);
const max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1;
for (let i = 1; i <= max_ntz; i++) {
L[i] = double(L[i - 1]);
}
L.x = L_x; L.x = L_x;
L.$ = L_$; L.$ = L_$;
return { encipher, decipher, L }; this.kv = { encipher, decipher, L };
} }
function hash(kv, key, adata) { extendKeyVariables(text, adata) {
const { L } = this.kv;
const max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1;
for (let i = this.max_ntz + 1; i <= max_ntz; i++) {
L[i] = double(L[i - 1]);
}
this.max_ntz = max_ntz;
}
hash(adata) {
if (!adata.length) { if (!adata.length) {
// Fast path // Fast path
return zeros_16; return zeros_16;
} }
const { encipher, L } = kv; const { encipher, L } = this.kv;
// //
// Consider A as a sequence of 128-bit blocks // Consider A as a sequence of 128-bit blocks
@ -141,14 +157,12 @@ function hash(kv, key, adata) {
/** /**
* Encrypt plaintext input. * Encrypt plaintext input.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} plaintext The cleartext input to be encrypted * @param {Uint8Array} plaintext The cleartext input to be encrypted
* @param {Uint8Array} key The encryption key
* @param {Uint8Array} nonce The nonce (15 bytes) * @param {Uint8Array} nonce The nonce (15 bytes)
* @param {Uint8Array} adata Associated data to sign * @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext output * @returns {Promise<Uint8Array>} The ciphertext output
*/ */
async function encrypt(cipher, plaintext, key, nonce, adata) { async encrypt(plaintext, nonce, adata) {
// //
// Consider P as a sequence of 128-bit blocks // Consider P as a sequence of 128-bit blocks
// //
@ -157,8 +171,8 @@ async function encrypt(cipher, plaintext, key, nonce, adata) {
// //
// Key-dependent variables // Key-dependent variables
// //
const kv = constructKeyVariables(cipher, key, plaintext, adata); this.extendKeyVariables(plaintext, adata);
const { encipher, L } = kv; const { encipher, L } = this.kv;
// //
// Nonce-dependent and per-encryption variables // Nonce-dependent and per-encryption variables
@ -204,7 +218,7 @@ async function encrypt(cipher, plaintext, key, nonce, adata) {
set_xor(checksum, xorInput); set_xor(checksum, xorInput);
pos += plaintext.length; pos += plaintext.length;
} }
const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(kv, key, adata)); const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), this.hash(adata));
// //
// Assemble ciphertext // Assemble ciphertext
@ -216,14 +230,12 @@ async function encrypt(cipher, plaintext, key, nonce, adata) {
/** /**
* Decrypt ciphertext input. * 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} ciphertext The ciphertext input to be decrypted
* @param {Uint8Array} key The encryption key
* @param {Uint8Array} nonce The nonce (15 bytes) * @param {Uint8Array} nonce The nonce (15 bytes)
* @param {Uint8Array} adata Associated data to verify * @param {Uint8Array} adata Associated data to verify
* @returns {Promise<Uint8Array>} The plaintext output * @returns {Promise<Uint8Array>} The plaintext output
*/ */
async function decrypt(cipher, ciphertext, key, nonce, adata) { async decrypt(ciphertext, nonce, adata) {
// //
// Consider C as a sequence of 128-bit blocks // Consider C as a sequence of 128-bit blocks
// //
@ -234,8 +246,8 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) {
// //
// Key-dependent variables // Key-dependent variables
// //
const kv = constructKeyVariables(cipher, key, ciphertext, adata); this.extendKeyVariables(ciphertext, adata);
const { encipher, decipher, L } = kv; const { encipher, decipher, L } = this.kv;
// //
// Nonce-dependent and per-encryption variables // Nonce-dependent and per-encryption variables
@ -281,7 +293,7 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) {
set_xor(checksum, xorInput); set_xor(checksum, xorInput);
pos += ciphertext.length; pos += ciphertext.length;
} }
const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(kv, key, adata)); const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), this.hash(adata));
// //
// Check for validity and assemble plaintext // Check for validity and assemble plaintext
@ -291,25 +303,23 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) {
} }
return P; return P;
} }
}
/** /**
* Get OCB nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2|RFC4880bis-04, section 5.16.2}. * Get OCB nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2|RFC4880bis-04, section 5.16.2}.
* @param {Uint8Array} iv The initialization vector (15 bytes) * @param {Uint8Array} iv The initialization vector (15 bytes)
* @param {Uint8Array} chunkIndex The chunk index (8 bytes) * @param {Uint8Array} chunkIndex The chunk index (8 bytes)
*/ */
function getNonce(iv, chunkIndex) { OCB.getNonce = function(iv, chunkIndex) {
const nonce = iv.slice(); const nonce = iv.slice();
for (let i = 0; i < chunkIndex.length; i++) { for (let i = 0; i < chunkIndex.length; i++) {
nonce[7 + i] ^= chunkIndex[i]; nonce[7 + i] ^= chunkIndex[i];
} }
return nonce; return nonce;
}
export default {
blockLength,
ivLength,
encrypt,
decrypt,
getNonce
}; };
OCB.blockLength = blockLength;
OCB.ivLength = ivLength;
export default OCB;

View File

@ -211,7 +211,7 @@ SecretKey.prototype.encrypt = async function (passphrase) {
arr = [new Uint8Array([253, optionalFields.length])]; arr = [new Uint8Array([253, optionalFields.length])];
arr.push(optionalFields); arr.push(optionalFields);
const mode = crypto[aead]; const mode = crypto[aead];
const encrypted = await mode.encrypt(symmetric, cleartext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); const encrypted = await new mode(symmetric, key).encrypt(cleartext, iv.subarray(0, mode.ivLength), new Uint8Array());
arr.push(util.writeNumber(encrypted.length, 4)); arr.push(util.writeNumber(encrypted.length, 4));
arr.push(encrypted); arr.push(encrypted);
} else { } else {
@ -305,7 +305,7 @@ SecretKey.prototype.decrypt = async function (passphrase) {
if (aead) { if (aead) {
const mode = crypto[aead]; const mode = crypto[aead];
try { try {
cleartext = await mode.decrypt(symmetric, ciphertext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); cleartext = await new mode(symmetric, key).decrypt(ciphertext, iv.subarray(0, mode.ivLength), new Uint8Array());
} catch(err) { } catch(err) {
if (err.message.startsWith('Authentication tag mismatch')) { if (err.message.startsWith('Authentication tag mismatch')) {
throw new Error('Incorrect key passphrase: ' + err.message); throw new Error('Incorrect key passphrase: ' + err.message);

View File

@ -107,15 +107,16 @@ SymEncryptedAEADProtected.prototype.decrypt = 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 - mode.blockLength); // Should be setInt64(13, ...) adataView.setInt32(13 + 4, data.length - mode.blockLength); // Should be setInt64(13, ...)
const decryptedPromises = []; const decryptedPromises = [];
const modeInstance = new mode(cipher, key);
for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { for (let chunkIndex = 0; chunkIndex === 0 || data.length;) {
decryptedPromises.push( decryptedPromises.push(
mode.decrypt(cipher, data.subarray(0, chunkSize), key, mode.getNonce(this.iv, chunkIndexArray), adataArray) modeInstance.decrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray)
); );
data = data.subarray(chunkSize); data = data.subarray(chunkSize);
adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...)
} }
decryptedPromises.push( decryptedPromises.push(
mode.decrypt(cipher, authTag, key, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) modeInstance.decrypt(authTag, mode.getNonce(this.iv, chunkIndexArray), adataTagArray)
); );
this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises))); this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises)));
} else { } else {
@ -148,9 +149,10 @@ 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 = new mode(sessionKeyAlgorithm, key);
for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { for (let chunkIndex = 0; chunkIndex === 0 || data.length;) {
encryptedPromises.push( encryptedPromises.push(
mode.encrypt(sessionKeyAlgorithm, data.subarray(0, chunkSize), key, mode.getNonce(this.iv, chunkIndexArray), adataArray) modeInstance.encrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray)
); );
// We take a chunk of data, encrypt it, and shift `data` to the // We take a chunk of data, encrypt it, and shift `data` to the
// next chunk. After the final chunk, we encrypt a final, empty // next chunk. After the final chunk, we encrypt a final, empty
@ -159,7 +161,7 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith
adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...)
} }
encryptedPromises.push( encryptedPromises.push(
mode.encrypt(sessionKeyAlgorithm, data, key, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) modeInstance.encrypt(data, mode.getNonce(this.iv, chunkIndexArray), adataTagArray)
); );
this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises)); this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises));
} else { } else {

View File

@ -142,7 +142,7 @@ SymEncryptedSessionKey.prototype.decrypt = async function(passphrase) {
if (this.version === 5) { if (this.version === 5) {
const mode = crypto[this.aeadAlgorithm]; const mode = crypto[this.aeadAlgorithm];
const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]);
this.sessionKey = await mode.decrypt(algo, this.encrypted, key, this.iv, adata); this.sessionKey = await new mode(algo, key).decrypt(this.encrypted, this.iv, adata);
} else if (this.encrypted !== null) { } else if (this.encrypted !== null) {
const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null); const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null);
@ -182,7 +182,7 @@ SymEncryptedSessionKey.prototype.encrypt = async function(passphrase) {
const mode = crypto[this.aeadAlgorithm]; const mode = crypto[this.aeadAlgorithm];
this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV
const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]);
this.encrypted = await mode.encrypt(algo, this.sessionKey, key, this.iv, adata); this.encrypted = await new mode(algo, key).encrypt(this.sessionKey, this.iv, adata);
} else { } else {
const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]);
const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); const private_key = util.concatUint8Array([algo_enum, this.sessionKey]);

View File

@ -9,8 +9,6 @@ chai.use(require('chai-as-promised'));
const expect = chai.expect; const expect = chai.expect;
const eax = openpgp.crypto.eax;
function testAESEAX() { function testAESEAX() {
it('Passes all test vectors', async function() { it('Passes all test vectors', async function() {
var vectors = [ var vectors = [
@ -96,28 +94,30 @@ function testAESEAX() {
headerBytes = openpgp.util.hex_to_Uint8Array(vec.header), headerBytes = openpgp.util.hex_to_Uint8Array(vec.header),
ctBytes = openpgp.util.hex_to_Uint8Array(vec.ct); ctBytes = openpgp.util.hex_to_Uint8Array(vec.ct);
const eax = new openpgp.crypto.eax(cipher, keyBytes);
// encryption test // encryption test
let ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); let ct = await eax.encrypt(msgBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.ct.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.ct.toLowerCase());
// decryption test with verification // decryption test with verification
let pt = await eax.decrypt(cipher, ctBytes, keyBytes, nonceBytes, headerBytes); let pt = await eax.decrypt(ctBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase());
// tampering detection test // tampering detection test
ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); ct = await eax.encrypt(msgBytes, nonceBytes, headerBytes);
ct[2] ^= 8; ct[2] ^= 8;
pt = eax.decrypt(cipher, ct, keyBytes, nonceBytes, headerBytes); pt = eax.decrypt(ct, nonceBytes, headerBytes);
await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in EAX ciphertext') await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in EAX ciphertext')
// testing without additional data // testing without additional data
ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, new Uint8Array()); ct = await eax.encrypt(msgBytes, nonceBytes, new Uint8Array());
pt = await eax.decrypt(cipher, ct, keyBytes, nonceBytes, new Uint8Array()); pt = await eax.decrypt(ct, nonceBytes, new Uint8Array());
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase());
// testing with multiple additional data // testing with multiple additional data
ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); ct = await eax.encrypt(msgBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
pt = await eax.decrypt(cipher, ct, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); pt = await eax.decrypt(ct, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase());
} }
}); });

View File

@ -9,8 +9,6 @@ chai.use(require('chai-as-promised'));
const expect = chai.expect; const expect = chai.expect;
const ocb = openpgp.crypto.ocb;
describe('Symmetric AES-OCB', function() { describe('Symmetric AES-OCB', function() {
it('Passes all test vectors', async function() { it('Passes all test vectors', async function() {
const K = '000102030405060708090A0B0C0D0E0F'; const K = '000102030405060708090A0B0C0D0E0F';
@ -124,28 +122,30 @@ describe('Symmetric AES-OCB', function() {
headerBytes = openpgp.util.hex_to_Uint8Array(vec.A), headerBytes = openpgp.util.hex_to_Uint8Array(vec.A),
ctBytes = openpgp.util.hex_to_Uint8Array(vec.C); ctBytes = openpgp.util.hex_to_Uint8Array(vec.C);
const ocb = new openpgp.crypto.ocb(cipher, keyBytes);
// encryption test // encryption test
let ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); let ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.C.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.C.toLowerCase());
// decryption test with verification // decryption test with verification
let pt = await ocb.decrypt(cipher, ctBytes, keyBytes, nonceBytes, headerBytes); let pt = await ocb.decrypt(ctBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase());
// tampering detection test // tampering detection test
ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes);
ct[2] ^= 8; ct[2] ^= 8;
pt = ocb.decrypt(cipher, ct, keyBytes, nonceBytes, headerBytes); pt = ocb.decrypt(ct, nonceBytes, headerBytes);
await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in OCB ciphertext') await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in OCB ciphertext')
// testing without additional data // testing without additional data
ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, new Uint8Array()); ct = await ocb.encrypt(msgBytes, nonceBytes, new Uint8Array());
pt = await ocb.decrypt(cipher, ct, keyBytes, nonceBytes, new Uint8Array()); pt = await ocb.decrypt(ct, nonceBytes, new Uint8Array());
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase());
// testing with multiple additional data // testing with multiple additional data
ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); ct = await ocb.encrypt(msgBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
pt = await ocb.decrypt(cipher, ct, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); pt = await ocb.decrypt(ct, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase());
} }
}); });
@ -162,19 +162,21 @@ describe('Symmetric AES-OCB', function() {
const K = new Uint8Array(KEYLEN / 8); const K = new Uint8Array(KEYLEN / 8);
K[K.length - 1] = TAGLEN; K[K.length - 1] = TAGLEN;
const ocb = new openpgp.crypto.ocb('aes' + KEYLEN, K);
const C = []; const C = [];
let N; let N;
for (let i = 0; i < 128; i++) { for (let i = 0; i < 128; i++) {
const S = new Uint8Array(i); const S = new Uint8Array(i);
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 1, 4)]); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 1, 4)]);
C.push(await ocb.encrypt('aes' + KEYLEN, S, K, N, S)); C.push(await ocb.encrypt(S, N, S));
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 2, 4)]); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 2, 4)]);
C.push(await ocb.encrypt('aes' + KEYLEN, S, K, N, new Uint8Array())); C.push(await ocb.encrypt(S, N, new Uint8Array()));
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 3, 4)]); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 3, 4)]);
C.push(await ocb.encrypt('aes' + KEYLEN, new Uint8Array(), K, N, S)); C.push(await ocb.encrypt(new Uint8Array(), N, S));
} }
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(385, 4)]); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(385, 4)]);
const output = await ocb.encrypt('aes' + KEYLEN, new Uint8Array(), K, N, openpgp.util.concatUint8Array(C)); const output = await ocb.encrypt(new Uint8Array(), N, openpgp.util.concatUint8Array(C));
expect(openpgp.util.Uint8Array_to_hex(output)).to.equal(outputs[KEYLEN].toLowerCase()); expect(openpgp.util.Uint8Array_to_hex(output)).to.equal(outputs[KEYLEN].toLowerCase());
} }
}); });