Skip to content

Commit

Permalink
Randomly generate IVs
Browse files Browse the repository at this point in the history
We include the IV in the encrypted payload, so we can let the server
choose them.
  • Loading branch information
ravi-signal committed Oct 29, 2024
1 parent a2d005c commit cb7b2fe
Show file tree
Hide file tree
Showing 4 changed files with 18 additions and 17 deletions.
11 changes: 8 additions & 3 deletions workers/src/encrypt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only

import { describe, expect, it } from 'vitest';
import { randBytes, readableStreamFrom, readAll, authenticateAndDecrypt } from './testutil';
import {
randBytes,
readableStreamFrom,
readAll,
authenticateAndDecryptWithIv
} from './testutil';
import { Encrypter, streamEncrypt } from './encrypt';

describe('streamEncrypt', () => {
Expand All @@ -26,7 +31,7 @@ describe('streamEncrypt', () => {

await writePromise;
const encrypted = await readPromise;
const decrypted = await authenticateAndDecrypt(iv, keyBytes, hmacKey, encrypted);
const decrypted = await authenticateAndDecryptWithIv(iv, keyBytes, hmacKey, encrypted);

expect(encrypted.length).toBe(encrypter.encryptedLength(plaintext.length));
expect(decrypted).toEqual(plaintext);
Expand All @@ -43,7 +48,7 @@ describe('streamEncrypt', () => {
const { readable: actual, writable: dst } = new TransformStream();
const encrypt = streamEncrypt(encrypter, source, dst, 1024);
const ciphertext = await readAll(actual);
const decrypted = await authenticateAndDecrypt(iv, keyBytes, hmacKey, ciphertext);
const decrypted = await authenticateAndDecryptWithIv(iv, keyBytes, hmacKey, ciphertext);
await encrypt;
expect(decrypted).toEqual(plaintext);
expect(ciphertext.length).toBe(encrypter.encryptedLength(plaintextLength));
Expand Down
6 changes: 2 additions & 4 deletions workers/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,12 @@ describe('usage', async () => {
describe('copy', () => {
const encryptionKey = randBytes(32);
const hmacKey = randBytes(32);
const iv = randBytes(16);
const plaintext = randBytes(1024 * 3 + 7);

function validRequest(source: Uint8Array = plaintext, key = 'abc', scheme = 'r2') {
return {
encryptionKey: Buffer.from(encryptionKey).toString('base64'),
hmacKey: Buffer.from(hmacKey).toString('base64'),
iv: Buffer.from(iv).toString('base64'),
source: { scheme, key },
expectedSourceLength: source.length,
dst: 'my/abc'
Expand All @@ -187,7 +185,7 @@ describe('copy', () => {
expect(res.status, await res.text()).toBe(400);
});

it.each(['encryptionKey', 'hmacKey', 'iv', 'expectedSourceLength'])('rejects bad base64 encoded %s', async (badprop: string) => {
it.each(['encryptionKey', 'hmacKey', 'expectedSourceLength'])('rejects bad base64 encoded %s', async (badprop: string) => {
const request: Record<string, unknown> = validRequest();
request[badprop] = 'aa&bb';
const body = JSON.stringify(request);
Expand Down Expand Up @@ -286,7 +284,7 @@ describe('copy', () => {
});
expect(res.status, await res.text()).toBe(204);
const payload = await toArray(await env.BACKUP_BUCKET.get('my/abc'));
const decrypted = await authenticateAndDecrypt(iv, encryptionKey, hmacKey, payload!);
const decrypted = await authenticateAndDecrypt(encryptionKey, hmacKey, payload!);
expect(decrypted).toEqual(plaintext);
});

Expand Down
11 changes: 2 additions & 9 deletions workers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ async function usageHandler(request: IRequest, env: Env): Promise<Response> {
interface CopyRequest {
encryptionKey: string,
hmacKey: string,
iv: string,
source: SourceDescriptor,
expectedSourceLength: number,
dst: string
Expand Down Expand Up @@ -155,7 +154,6 @@ function isCopyRequest(o: unknown): o is CopyRequest {
&& typeof o === 'object'
&& 'encryptionKey' in o && typeof (o.encryptionKey) === 'string'
&& 'hmacKey' in o && typeof (o.hmacKey) === 'string'
&& 'iv' in o && typeof (o.iv) === 'string'
&& 'source' in o && isSourceDescriptor(o.source)
&& 'expectedSourceLength' in o && typeof (o.expectedSourceLength) === 'number'
&& 'dst' in o && typeof (o.dst) === 'string';
Expand Down Expand Up @@ -183,13 +181,8 @@ async function copyHandler(request: IRequest, env: Env): Promise<Response> {
return error(400, 'invalid hmac key, must be length 32');
}

const iv = b64decode(copyRequest.iv);
if (iv == null) {
return error(400, 'invalid iv, must be base64');
}
if (iv.length != 16) {
return error(400, 'invalid iv, must be length 16');
}
const iv = new Uint8Array(16);
crypto.getRandomValues(iv);

const s = await source(env, copyRequest.source);
if (s == null) {
Expand Down
7 changes: 6 additions & 1 deletion workers/src/testutil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ export async function decrypt(iv: Uint8Array, key: Uint8Array, data: Uint8Array)
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, await aesKey(key), data));
}

export async function authenticateAndDecrypt(iv: Uint8Array, key: Uint8Array, hmacKey: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
export async function authenticateAndDecrypt(key: Uint8Array, hmacKey: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
// Use the IV appended to the ciphertext
return await authenticateAndDecryptWithIv(ciphertext.subarray(0, 16), key, hmacKey, ciphertext)
}

export async function authenticateAndDecryptWithIv(iv: Uint8Array, key: Uint8Array, hmacKey: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
expect(ciphertext.subarray(0, 16)).toEqual(iv);
const encrypted = ciphertext!.subarray(16, ciphertext.length - 32);
const hmac = ciphertext.subarray(ciphertext.length - 32, ciphertext.length);
Expand Down

0 comments on commit cb7b2fe

Please sign in to comment.