Skip to content

Commit aad983f

Browse files
committed
fix: WebCrypto API passes the tag with encrypted data.
See: #237 Since the WebCrypto decrypt API expects the AES-GCM tag with the encrypted data, zero bytes of encrypted data is not zero bytes of data. fix: Add tests Add tests to specificly cover the Mixed Backend conditions and logic.
1 parent 49e2eb0 commit aad983f

File tree

2 files changed

+97
-4
lines changed

2 files changed

+97
-4
lines changed

modules/material-management-browser/src/material_helpers.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,16 @@ export function getSubtleFunction<T extends WebCryptoMaterial<T>> (
175175
const { nonZeroByteSubtle, zeroByteSubtle } = backend
176176
const { nonZeroByteCryptoKey, zeroByteCryptoKey } = deriveKey
177177
const algorithm = { name: cipherName, iv, additionalData, tagLength }
178-
if (data.byteLength) {
179-
return nonZeroByteSubtle[subtleFunction](algorithm, nonZeroByteCryptoKey, data)
180-
} else {
178+
/* Precondition: The WebCrypto AES-GCM decrypt API expects the data *and* tag together.
179+
* This means that on decrypt any amount of data less than tagLength is invalid.
180+
* This also means that zero encrypted data will be equal to tagLength.
181+
*/
182+
const dataByteLength = subtleFunction === 'decrypt' ? data.byteLength - tagLength / 8 : data.byteLength
183+
needs(dataByteLength >= 0, 'Invalid data length.')
184+
if (dataByteLength === 0) {
181185
return zeroByteSubtle[subtleFunction](algorithm, zeroByteCryptoKey, data)
186+
} else {
187+
return nonZeroByteSubtle[subtleFunction](algorithm, nonZeroByteCryptoKey, data)
182188
}
183189
}
184190
// This should be impossible

modules/material-management-browser/test/material_helpers.test.ts

+88-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
SignatureKey,
3737
VerificationKey
3838
} from '@aws-crypto/material-management'
39-
import { synchronousRandomValues, getWebCryptoBackend, getZeroByteSubtle } from '@aws-crypto/web-crypto-backend'
39+
import { synchronousRandomValues, getWebCryptoBackend, getZeroByteSubtle, getNonZeroByteBackend } from '@aws-crypto/web-crypto-backend'
4040

4141
chai.use(chaiAsPromised)
4242
const { expect } = chai
@@ -288,6 +288,93 @@ describe('getSubtleFunction', () => {
288288
expect(() => testIvAad(iv, aad)).to.throw()
289289
})
290290

291+
it('can encrypt/decrypt 0 bytes', async () => {
292+
const suite = new WebCryptoAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
293+
const material = new WebCryptoEncryptionMaterial(suite, {})
294+
const udk = synchronousRandomValues(suite.keyLengthBytes)
295+
const trace = { keyName: 'keyName', keyNamespace: 'keyNamespace', flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY }
296+
material.setUnencryptedDataKey(udk, trace)
297+
298+
const backend = await getWebCryptoBackend()
299+
/* All of this _only_ matters in the case of a mixed backend.
300+
* So I force the issue.
301+
*/
302+
const mixedBackend = {
303+
nonZeroByteSubtle: getZeroByteSubtle(backend),
304+
zeroByteSubtle: getNonZeroByteBackend(backend),
305+
randomValues: backend.randomValues
306+
}
307+
308+
const cryptoKey = await importCryptoKey(mixedBackend, material, ['encrypt', 'decrypt'])
309+
material.setCryptoKey(cryptoKey, trace)
310+
311+
const iv = new Uint8Array(suite.ivLength)
312+
const aad = new Uint8Array(1)
313+
const tagLengthBytes = suite.tagLength / 8
314+
315+
// Encrypt
316+
const testEncryptInfo = getSubtleFunction(material, mixedBackend, 'encrypt')
317+
const testEncryptIvAad = testEncryptInfo(new Uint8Array(1))
318+
const testEncryptFunction = testEncryptIvAad(iv, aad)
319+
const testEncryptedData = await testEncryptFunction(new Uint8Array(0))
320+
// Because I encrypted 0 bytes, the data should _only_ be tagLength
321+
expect(testEncryptedData.byteLength).to.equal(tagLengthBytes)
322+
323+
// Decrypt
324+
const testDecryptInfo = getSubtleFunction(material, mixedBackend, 'decrypt')
325+
const testDecryptIvAad = testDecryptInfo(new Uint8Array(1))
326+
const testDecryptFunction = testDecryptIvAad(iv, aad)
327+
const testDecryptedData = await testDecryptFunction(new Uint8Array(testEncryptedData))
328+
329+
// Because I encrypted 0 bytes, the data should be 0 length
330+
expect(testDecryptedData.byteLength).to.equal(0)
331+
})
332+
333+
it('Precondition: The WebCrypto AES-GCM decrypt API expects the data *and* tag together.', async () => {
334+
const suite = new WebCryptoAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
335+
const material = new WebCryptoEncryptionMaterial(suite, {})
336+
const udk = synchronousRandomValues(suite.keyLengthBytes)
337+
const trace = { keyName: 'keyName', keyNamespace: 'keyNamespace', flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY }
338+
material.setUnencryptedDataKey(udk, trace)
339+
340+
const backend = await getWebCryptoBackend()
341+
/* All of this _only_ matters in the case of a mixed backend.
342+
* So I force the issue.
343+
*/
344+
const mixedBackend = {
345+
nonZeroByteSubtle: getZeroByteSubtle(backend),
346+
zeroByteSubtle: getNonZeroByteBackend(backend),
347+
randomValues: backend.randomValues
348+
}
349+
350+
const cryptoKey = await importCryptoKey(mixedBackend, material, ['encrypt', 'decrypt'])
351+
material.setCryptoKey(cryptoKey, trace)
352+
353+
const iv = new Uint8Array(suite.ivLength)
354+
const aad = new Uint8Array(1)
355+
const tagLengthBytes = suite.tagLength / 8
356+
357+
// Encrypt
358+
const testEncryptInfo = getSubtleFunction(material, mixedBackend, 'encrypt')
359+
const testEncryptIvAad = testEncryptInfo(new Uint8Array(1))
360+
const testEncryptFunction = testEncryptIvAad(iv, aad)
361+
const testEncryptedData = await testEncryptFunction(new Uint8Array(0))
362+
363+
// Because I encrypted 0 bytes, the data should _only_ be tagLength
364+
expect(testEncryptedData.byteLength).to.equal(tagLengthBytes)
365+
366+
// Decrypt
367+
const testDecryptInfo = getSubtleFunction(material, mixedBackend, 'decrypt')
368+
const testDecryptIvAad = testDecryptInfo(new Uint8Array(1))
369+
const testDecryptFunction = testDecryptIvAad(iv, aad)
370+
371+
for (let i = 0; tagLengthBytes > i; i++) {
372+
console.log(i)
373+
await expect(testDecryptFunction(new Uint8Array(testEncryptedData.slice(0, i))))
374+
.to.eventually.rejectedWith(Error, 'Invalid data length.')
375+
}
376+
})
377+
291378
it('no kdf, simple backend, can encrypt/decrypt', async () => {
292379
const suite = new WebCryptoAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
293380
const encryptionMaterial = new WebCryptoEncryptionMaterial(suite, {})

0 commit comments

Comments
 (0)