blob: 971eed43f2e9430c31816382d5711cfcfb66d18d [file] [log] [blame]
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {SecurityException} from '../exception/security_exception';
import * as PrimitiveSet from '../internal/primitive_set';
import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto';
import * as Bytes from '../subtle/bytes';
import * as Random from '../subtle/random';
import {HybridDecryptWrapper} from './hybrid_decrypt_wrapper';
import {HybridEncryptWrapper} from './hybrid_encrypt_wrapper';
import {HybridDecrypt} from './internal/hybrid_decrypt';
import {HybridEncrypt} from './internal/hybrid_encrypt';
describe('hybrid decrypt wrapper test', function() {
it('decrypt, invalid ciphertext', async function() {
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) {
expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
}
});
it('decrypt, should work', async function() {
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.
expect(decryptedCiphertext).toEqual(plaintext);
});
it('decrypt, ciphertext encrypted by raw primitive', async function() {
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.
expect(decryptedCiphertext).toEqual(plaintext);
});
it('decrypt, with context info', async function() {
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) {
expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
}
const decryptedCiphertext =
await hybridDecrypt.decrypt(ciphertext, contextInfo);
// Test that the result is the original plaintext.
expect(decryptedCiphertext).toEqual(plaintext);
});
it('decrypt, with disabled primitive', async function() {
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) {
expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
}
});
it('decrypt, with empty ciphertext', async function() {
const primitiveSets = createDummyPrimitiveSets();
const decryptPrimitiveSet = primitiveSets['decryptPrimitiveSet'];
const hybridDecrypt = new HybridDecryptWrapper().wrap(decryptPrimitiveSet);
try {
await hybridDecrypt.decrypt(new Uint8Array(0));
fail('An exception should be thrown.');
} catch (e) {
expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted());
}
});
});
/** @final */
class ExceptionText {
static nullPrimitiveSet(): string {
return 'SecurityException: Primitive set has to be non-null.';
}
static cannotBeDecrypted(): string {
return 'SecurityException: Decryption failed for the given ciphertext.';
}
static nullCiphertext(): string {
return 'SecurityException: Ciphertext has to be non-null.';
}
}
/** Function for creating keys for testing purposes. */
function createDummyKeysetKey(
keyId: number, outputPrefix: PbOutputPrefixType,
enabled: boolean): PbKeysetKey {
const 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).
*/
function createDummyPrimitiveSets(opt_withPrimary: boolean = true): {
encryptPrimitiveSet: PrimitiveSet.PrimitiveSet<DummyHybridEncrypt>,
decryptPrimitiveSet: PrimitiveSet.PrimitiveSet<DummyHybridDecrypt>
} {
const numberOfPrimitives = 5;
const encryptPrimitiveSet =
new PrimitiveSet.PrimitiveSet<DummyHybridEncrypt>(DummyHybridEncrypt);
const decryptPrimitiveSet =
new PrimitiveSet.PrimitiveSet<DummyHybridDecrypt>(DummyHybridDecrypt);
for (let i = 1; i < numberOfPrimitives; i++) {
let outputPrefix: PbOutputPrefixType;
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
};
}
/** @final */
class DummyHybridEncrypt extends HybridEncrypt {
constructor(private readonly ciphertextSuffix: Uint8Array) {
super();
}
/** @override */
async encrypt(plaintext: Uint8Array, opt_contextInfo?: Uint8Array) {
const ciphertext = Bytes.concat(plaintext, this.ciphertextSuffix);
if (opt_contextInfo) {
return Bytes.concat(ciphertext, opt_contextInfo);
}
return ciphertext;
}
}
/** @final */
class DummyHybridDecrypt extends HybridDecrypt {
constructor(private readonly ciphertextSuffix: Uint8Array) {
super();
}
/** @override */
async decrypt(ciphertext: Uint8Array, opt_contextInfo?: Uint8Array) {
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());
}
}