blob: 48940814e44af36d5d64365d2fd8c881146eba53 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
goog.module('tink.hybrid.HybridDecryptWrapperTest');
goog.setTestOnly('tink.hybrid.HybridDecryptWrapperTest');
const Bytes = goog.require('tink.subtle.Bytes');
const HybridDecrypt = goog.require('tink.HybridDecrypt');
const HybridDecryptWrapper = goog.require('tink.hybrid.HybridDecryptWrapper');
const HybridEncrypt = goog.require('tink.HybridEncrypt');
const HybridEncryptWrapper = goog.require('tink.hybrid.HybridEncryptWrapper');
const PbKeyStatusType = goog.require('proto.google.crypto.tink.KeyStatusType');
const PbKeysetKey = goog.require('proto.google.crypto.tink.Keyset.Key');
const PbOutputPrefixType = goog.require('proto.google.crypto.tink.OutputPrefixType');
const PrimitiveSet = goog.require('tink.PrimitiveSet');
const Random = goog.require('tink.subtle.Random');
const SecurityException = goog.require('tink.exception.SecurityException');
const testSuite = goog.require('goog.testing.testSuite');
testSuite({
async testNewHybridDecrypt_nullPrimitiveSet() {
try {
new HybridDecryptWrapper().wrap(null);
fail('Should throw an exception.');
} catch (e) {
assertEquals(ExceptionText.nullPrimitiveSet(), e.toString());
}
},
async testDecrypt_invalidCiphertext() {
const primitiveSets = createDummyPrimitiveSets();
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
// Ciphertext which cannot be decrypted by any primitive in the primitive
// set.
const ciphertext = new Uint8Array([9, 8, 7, 6, 5, 4, 3]);
try {
await hybridDecrypt.decrypt(ciphertext);
fail('Should throw an exception');
} catch (e) {
assertEquals(ExceptionText.cannotBeDecrypted(), e.toString());
}
},
async testDecrypt_shouldWork() {
const primitiveSets = createDummyPrimitiveSets();
const plaintext = Random.randBytes(10);
// As keys are just dummy keys which do not contain key data, the same key
// is used for both encrypt and decrypt.
const key = createDummyKeysetKey(
/** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.TINK,
/** enabled = */ true);
const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
// Get the ciphertext.
const encryptPrimitiveSet = primitiveSets['encryptPrimitiveSet'];
const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
const entry = encryptPrimitiveSet.addPrimitive(encryptPrimitive, key);
// Has to be set to primary as then it is used in encryption.
encryptPrimitiveSet.setPrimary(entry);
const hybridEncrypt = new HybridEncryptWrapper().wrap(encryptPrimitiveSet);
const ciphertext = await hybridEncrypt.encrypt(plaintext);
// Create a primitive set containing the primitive which can be used for
// encryption. Add also few more primitives with the same key as the
// primitive set should decrypt the ciphertext whenever there is at least
// one primitive which does not fail to decrypt the ciphertext.
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
decryptPrimitiveSet.addPrimitive(
new DummyHybridDecrypt(Random.randBytes(5)), key);
decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
decryptPrimitiveSet.addPrimitive(
new DummyHybridDecrypt(Random.randBytes(5)), key);
// Decrypt the ciphertext.
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
const decryptedCiphertext = await hybridDecrypt.decrypt(ciphertext);
// Test that the result is the original plaintext.
assertObjectEquals(plaintext, decryptedCiphertext);
},
async testDecrypt_ciphertextEncryptedByRawPrimitive() {
const primitiveSets = createDummyPrimitiveSets();
const plaintext = Random.randBytes(10);
// As keys are just dummy keys which do not contain key data, the same key
// is used for both encrypt and decrypt.
const key = createDummyKeysetKey(
/** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
/** enabled = */ true);
const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
// Get the ciphertext.
const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
const ciphertext = await encryptPrimitive.encrypt(plaintext);
// Decrypt the ciphertext.
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
const decryptedCiphertext = await hybridDecrypt.decrypt(ciphertext);
// Test that the result is the original plaintext.
assertObjectEquals(plaintext, decryptedCiphertext);
},
async testDecrypt_withContextInfo() {
const primitiveSets = createDummyPrimitiveSets();
const plaintext = Random.randBytes(10);
const contextInfo = Random.randBytes(10);
// As keys are just dummy keys which do not contain key data, the same key
// is used for both encrypt and decrypt.
const key = createDummyKeysetKey(
/** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
/** enabled = */ true);
const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
// Get the ciphertext.
const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
const ciphertext = await encryptPrimitive.encrypt(plaintext, contextInfo);
// Get primitive for decryption.
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
// Check that contextInfo was passed correctly (decryption without
// contextInfo argument should not work, but with contextInfo it should work
// properly).
try {
await hybridDecrypt.decrypt(ciphertext);
fail('An exception should be thrown.');
} catch (e) {
assertEquals(ExceptionText.cannotBeDecrypted(), e.toString());
}
const decryptedCiphertext =
await hybridDecrypt.decrypt(ciphertext, contextInfo);
// Test that the result is the original plaintext.
assertObjectEquals(plaintext, decryptedCiphertext);
},
async testDecrypt_withDisabledPrimitive() {
const primitiveSets = createDummyPrimitiveSets();
const plaintext = Random.randBytes(10);
const key = createDummyKeysetKey(
/** keyId = */ 0xFFFFFFFF, PbOutputPrefixType.RAW,
/** enabled = */ false);
const ciphertextSuffix = new Uint8Array([0, 0, 0, 0xFF]);
const encryptPrimitive = new DummyHybridEncrypt(ciphertextSuffix);
const ciphertext = await encryptPrimitive.encrypt(plaintext);
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const decryptPrimitive = new DummyHybridDecrypt(ciphertextSuffix);
decryptPrimitiveSet.addPrimitive(decryptPrimitive, key);
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
try {
await hybridDecrypt.decrypt(ciphertext);
fail('An exception should be thrown.');
} catch (e) {
assertEquals(ExceptionText.cannotBeDecrypted(), e.toString());
}
},
async testDecrypt_withNullCiphertext() {
const primitiveSets = createDummyPrimitiveSets();
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
try {
await hybridDecrypt.decrypt(null);
fail('An exception should be thrown.');
} catch (e) {
assertEquals(ExceptionText.nullCiphertext(), e.toString());
}
},
});
/** @final */
class ExceptionText {
/** @return {string} */
static nullPrimitiveSet() {
return 'CustomError: Primitive set has to be non-null.';
}
/** @return {string} */
static cannotBeDecrypted() {
return 'CustomError: Decryption failed for the given ciphertext.';
}
/** @return {string} */
static nullCiphertext() {
return 'CustomError: Ciphertext has to be non-null.';
}
}
/**
* Function for creating keys for testing purposes.
*
* @param {number} keyId
* @param {PbOutputPrefixType} outputPrefix
* @param {boolean} enabled
*
* @return {!PbKeysetKey}
*/
const createDummyKeysetKey = function(keyId, outputPrefix, enabled) {
let key = new PbKeysetKey();
if (enabled) {
key.setStatus(PbKeyStatusType.ENABLED);
} else {
key.setStatus(PbKeyStatusType.DISABLED);
}
key.setOutputPrefixType(outputPrefix);
key.setKeyId(keyId);
return key;
};
/**
* Creates a primitive sets for HybridEncrypt and HybridDecrypt with
* 'numberOfPrimitives' primitives. The keys corresponding to the primitives
* have ids from the set [1, ..., numberOfPrimitives] and the primitive
* corresponding to key with id 'numberOfPrimitives' is set to be primary
* whenever opt_withPrimary is set to true (where true is the default value).
*
* @param {boolean=} opt_withPrimary
* @return {{encryptPrimitiveSet:!PrimitiveSet.PrimitiveSet,
* decryptPrimitiveSet:!PrimitiveSet.PrimitiveSet}}
*/
const createDummyPrimitiveSets = function(opt_withPrimary = true) {
const numberOfPrimitives = 5;
const encryptPrimitiveSet = new PrimitiveSet.PrimitiveSet();
const decryptPrimitiveSet = new PrimitiveSet.PrimitiveSet();
for (let i = 1; i < numberOfPrimitives; i++) {
let /** @type {PbOutputPrefixType} */ outputPrefix;
switch (i % 3) {
case 0:
outputPrefix = PbOutputPrefixType.TINK;
break;
case 1:
outputPrefix = PbOutputPrefixType.LEGACY;
break;
default:
outputPrefix = PbOutputPrefixType.RAW;
}
const key =
createDummyKeysetKey(i, outputPrefix, /* enabled = */ i % 4 < 2);
const ciphertextSuffix = new Uint8Array([0, 0, i]);
const hybridEncrypt = new DummyHybridEncrypt(ciphertextSuffix);
encryptPrimitiveSet.addPrimitive(hybridEncrypt, key);
const hybridDecrypt = new DummyHybridDecrypt(ciphertextSuffix);
decryptPrimitiveSet.addPrimitive(hybridDecrypt, key);
}
const key = createDummyKeysetKey(
numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true);
const ciphertextSuffix = new Uint8Array([0, 0, numberOfPrimitives]);
const hybridEncrypt = new DummyHybridEncrypt(ciphertextSuffix);
const encryptEntry = encryptPrimitiveSet.addPrimitive(hybridEncrypt, key);
const hybridDecrypt = new DummyHybridDecrypt(ciphertextSuffix);
const decryptEntry = decryptPrimitiveSet.addPrimitive(hybridDecrypt, key);
if (opt_withPrimary) {
encryptPrimitiveSet.setPrimary(encryptEntry);
decryptPrimitiveSet.setPrimary(decryptEntry);
}
return {
'encryptPrimitiveSet': encryptPrimitiveSet,
'decryptPrimitiveSet': decryptPrimitiveSet
};
};
/**
* @implements {HybridEncrypt}
* @final
*/
class DummyHybridEncrypt {
/** @param {!Uint8Array} ciphertextSuffix */
constructor(ciphertextSuffix) {
this.ciphertextSuffix_ = ciphertextSuffix;
}
/** @override */
async encrypt(plaintext, opt_contextInfo) {
const ciphertext = Bytes.concat(plaintext, this.ciphertextSuffix_);
if (opt_contextInfo) {
return Bytes.concat(ciphertext, opt_contextInfo);
}
return ciphertext;
}
}
/**
* @implements {HybridDecrypt}
* @final
*/
class DummyHybridDecrypt {
/** @param {!Uint8Array} ciphertextSuffix */
constructor(ciphertextSuffix) {
this.ciphertextSuffix_ = ciphertextSuffix;
}
/** @override */
async decrypt(ciphertext, opt_contextInfo) {
if (opt_contextInfo) {
const infoLength = opt_contextInfo.length;
const contextInfo =
ciphertext.slice(ciphertext.length - infoLength, ciphertext.length);
if ([...contextInfo].toString() !== [...opt_contextInfo].toString()) {
throw new SecurityException('Context info does not match.');
}
ciphertext = ciphertext.slice(0, ciphertext.length - infoLength);
}
const plaintext =
ciphertext.slice(0, ciphertext.length - this.ciphertextSuffix_.length);
const suffix = ciphertext.slice(
ciphertext.length - this.ciphertextSuffix_.length, ciphertext.length);
if ([...suffix].toString() === [...this.ciphertextSuffix_].toString()) {
return plaintext;
}
throw new SecurityException(ExceptionText.cannotBeDecrypted());
}
}