| /** |
| * @license |
| * Copyright 2020 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| import {SecurityException} from '../exception/security_exception'; |
| import {CryptoFormat} from '../internal/crypto_format'; |
| import * as PrimitiveSet from '../internal/primitive_set'; |
| import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from '../internal/proto'; |
| import * as Bytes from '../subtle/bytes'; |
| import {assertExists} from '../testing/internal/test_utils'; |
| |
| import {AeadWrapper} from './aead_wrapper'; |
| import {Aead} from './internal/aead'; |
| |
| describe('aead wrapper test', function() { |
| it('new aead primitive set without primary', async function() { |
| const primitiveSet = createPrimitiveSet(/* opt_withPrimary = */ false); |
| try { |
| new AeadWrapper().wrap(primitiveSet); |
| // This is an intentionally unsafe partial mock |
| // tslint:disable-next-line:no-any |
| } catch (e: any) { |
| expect(e.toString()).toBe(ExceptionText.primitiveSetWithoutPrimary()); |
| return; |
| } |
| fail('Should throw an exception.'); |
| }); |
| |
| it('new aead primitive should work', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| expect(aead != null && aead != undefined).toBe(true); |
| }); |
| |
| it('encrypt', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| |
| const plaintext = new Uint8Array([0, 1, 2, 3]); |
| |
| const ciphertext = await aead.encrypt(plaintext); |
| expect(ciphertext != null).toBe(true); |
| |
| // Ciphertext should begin with primary key output prefix. |
| expect(ciphertext.subarray(0, CryptoFormat.NON_RAW_PREFIX_SIZE)) |
| .toEqual(assertExists(primitiveSet.getPrimary()).getIdentifier()); |
| }); |
| |
| it('decrypt bad ciphertext', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| |
| const ciphertext = new Uint8Array([9, 8, 7, 6, 5, 4, 3]); |
| |
| try { |
| await aead.decrypt(ciphertext); |
| } catch (e: any) { |
| expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted()); |
| return; |
| } |
| fail('Should throw an exception'); |
| }); |
| |
| it('decrypt with ciphertext encrypted by primary key', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| |
| const plaintext = new Uint8Array([12, 51, 45, 200, 120, 111]); |
| |
| const ciphertext = await aead.encrypt(plaintext); |
| const decryptResult = await aead.decrypt(ciphertext); |
| |
| expect(decryptResult).toEqual(plaintext); |
| }); |
| |
| it('decrypt ciphertext encrypted by non primary key', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| |
| // Encrypt the plaintext with primary. |
| const plaintext = new Uint8Array([0xAA, 0xBB, 0xAB, 0xBA, 0xFF]); |
| const ciphertext = await aead.encrypt(plaintext); |
| |
| // Add a new primary to primitive set and make new AeadSetWrapper with the |
| // updated primitive set. |
| const keyId = 0xFFFFFFFF; |
| const key = |
| createKey(keyId, PbOutputPrefixType.LEGACY, /* enabled = */ true); |
| const entry = |
| primitiveSet.addPrimitive(new DummyAead(new Uint8Array([0xFF])), key); |
| primitiveSet.setPrimary(entry); |
| const aead2 = new AeadWrapper().wrap(primitiveSet); |
| |
| // Check that the ciphertext can be decrypted by the setWrapper with new |
| // primary and that the decryption corresponds to the plaintext. |
| const decryptResult = await aead2.decrypt(ciphertext); |
| |
| expect(decryptResult).toEqual(plaintext); |
| }); |
| |
| it('decrypt ciphertext raw primitive', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| // Create a RAW primitive and add it to primitiveSet. |
| const keyId = 0xFFFFFFFF; |
| const rawKey = |
| createKey(keyId, PbOutputPrefixType.RAW, /* enabled = */ true); |
| const rawKeyAead = new DummyAead(new Uint8Array([0xFF])); |
| primitiveSet.addPrimitive(rawKeyAead, rawKey); |
| |
| // Encrypt the plaintext by aead corresponding to the rawKey. |
| const plaintext = new Uint8Array([0x11, 0x15, 0xAA, 0x54]); |
| const ciphertext = await rawKeyAead.encrypt(plaintext); |
| |
| // Create aead which should be able to decrypt the ciphertext. |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| |
| // Try to decrypt the ciphertext by aead and check that the result |
| // corresponds to the plaintext. |
| const decryptResult = await aead.decrypt(ciphertext); |
| expect(decryptResult).toEqual(plaintext); |
| }); |
| |
| it('decrypt ciphertext disabled primitive', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| |
| // Create a primitive with disabled key and add it to primitiveSet. |
| const keyId = 0xFFFFFFFF; |
| const key = createKey(keyId, PbOutputPrefixType.RAW, /* enabled = */ false); |
| const disabledKeyAead = new DummyAead(new Uint8Array([0xFF])); |
| primitiveSet.addPrimitive(disabledKeyAead, key); |
| |
| // Encrypt the plaintext by a primitive with disabled key. |
| const plaintext = new Uint8Array([0, 1, 2, 3]); |
| const ciphertext = await disabledKeyAead.encrypt(plaintext); |
| |
| // Create aead containing the primitive with disabled key. |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| |
| // Check that the ciphertext cannot be decrypted as disabled keys cannot be |
| // used to neither encryption nor decryption. |
| try { |
| await aead.decrypt(ciphertext); |
| } catch (e: any) { |
| expect(e.toString()).toBe(ExceptionText.cannotBeDecrypted()); |
| return; |
| } |
| fail('An exception should be thrown.'); |
| }); |
| |
| it('encrypt decrypt, associated data should be passed', async function() { |
| const primitiveSet = createPrimitiveSet(); |
| const aead = new AeadWrapper().wrap(primitiveSet); |
| const plaintext = new Uint8Array([0, 1, 2, 3, 4, 5, 6]); |
| const aad = new Uint8Array([8, 9, 10, 11, 12]); |
| |
| // Encrypt the plaintext with aad. The ciphertext should end with aad if |
| // it was passed correctly. |
| const ciphertext = await aead.encrypt(plaintext, aad); |
| const ciphertextAad = |
| ciphertext.slice(ciphertext.length - aad.length, ciphertext.length); |
| expect(ciphertextAad).toEqual(aad); |
| |
| // Decrypt the ciphertext with aad. It is possible only if aad was passed |
| // correctly. |
| const decryptedCiphertext = await aead.decrypt(ciphertext, aad); |
| expect(decryptedCiphertext).toEqual(plaintext); |
| }); |
| }); |
| |
| /** |
| * Class holding texts for each type of exception. |
| * @final |
| */ |
| class ExceptionText { |
| static nullPrimitiveSet(): string { |
| return 'SecurityException: Primitive set has to be non-null.'; |
| } |
| |
| static primitiveSetWithoutPrimary(): string { |
| return 'SecurityException: Primary has to be non-null.'; |
| } |
| |
| static cannotBeDecrypted(): string { |
| return 'SecurityException: Decryption failed for the given ciphertext.'; |
| } |
| } |
| |
| /** Function for creating keys for testing purposes. */ |
| function createKey( |
| 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 set 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 createPrimitiveSet(opt_withPrimary: boolean = true): |
| PrimitiveSet.PrimitiveSet<Aead> { |
| const numberOfPrimitives = 5; |
| |
| const primitiveSet = new PrimitiveSet.PrimitiveSet<DummyAead>(DummyAead); |
| 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 = createKey(i, outputPrefix, /* enabled = */ i % 4 < 2); |
| primitiveSet.addPrimitive(new DummyAead(new Uint8Array([i])), key); |
| } |
| |
| const key = createKey( |
| numberOfPrimitives, PbOutputPrefixType.TINK, /* enabled = */ true); |
| const aead = new DummyAead(new Uint8Array([numberOfPrimitives])); |
| const entry = primitiveSet.addPrimitive(aead, key); |
| if (opt_withPrimary) { |
| primitiveSet.setPrimary(entry); |
| } |
| |
| return primitiveSet; |
| } |
| |
| /** @final */ |
| class DummyAead extends Aead { |
| constructor(private readonly primitiveIdentifier: Uint8Array) { |
| super(); |
| } |
| |
| /** @override*/ |
| async encrypt(plaintext: Uint8Array, opt_associatedData?: Uint8Array) { |
| const result = Bytes.concat(plaintext, this.primitiveIdentifier); |
| if (opt_associatedData) { |
| return Bytes.concat(result, opt_associatedData); |
| } |
| return result; |
| } |
| |
| /** @override*/ |
| async decrypt(ciphertext: Uint8Array, opt_associatedData?: Uint8Array) { |
| if (opt_associatedData) { |
| const aad = ciphertext.subarray( |
| ciphertext.length - opt_associatedData.length, ciphertext.length); |
| |
| if ([...aad].toString() != [...opt_associatedData].toString()) { |
| throw new SecurityException(ExceptionText.cannotBeDecrypted()); |
| } |
| ciphertext = ciphertext.subarray(0, ciphertext.length - aad.length); |
| } |
| |
| const primitiveIdentifier = ciphertext.subarray( |
| ciphertext.length - this.primitiveIdentifier.length, ciphertext.length); |
| if ([...primitiveIdentifier].toString() != |
| [...this.primitiveIdentifier].toString()) { |
| throw new SecurityException(ExceptionText.cannotBeDecrypted()); |
| } |
| |
| return ciphertext.subarray( |
| 0, ciphertext.length - this.primitiveIdentifier.length); |
| } |
| } |