atagen

malice

public
Star0
Watch0
973f8fbdaf80e2ef6679dd071233fe6b853344b2
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/test/media.test.tsTypescript110 lines4.3 KB
1
import assert from 'node:assert/strict';
2
import { test } from 'node:test';
3
import { addBlockOffset, importAttachmentKey, RangeDecryptor } from '../src/lib/media-decrypt.js';
4
import { attachmentContentType, imageContentType } from '../src/lib/media-sniff.js';
5
6
function randomBytes(length: number): Uint8Array {
7
  const bytes = new Uint8Array(length);
8
  crypto.getRandomValues(bytes);
9
  return bytes;
10
}
11
12
function textBytes(text: string): Uint8Array {
13
  return new TextEncoder().encode(text);
14
}
15
16
function concatBytes(parts: Uint8Array[]): Uint8Array {
17
  const total = parts.reduce((sum, part) => sum + part.length, 0);
18
  const out = new Uint8Array(total);
19
  let offset = 0;
20
  for (const part of parts) {
21
    out.set(part, offset);
22
    offset += part.length;
23
  }
24
  return out;
25
}
26
27
function base64url(bytes: Uint8Array): string {
28
  let binary = '';
29
  for (const byte of bytes) binary += String.fromCharCode(byte);
30
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
31
}
32
33
async function encryptWhole(rawKey: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array> {
34
  const encryptKey = await crypto.subtle.importKey('raw', rawKey, { name: 'AES-CTR' }, false, ['encrypt']);
35
  const counter = addBlockOffset(iv, 0);
36
  const cipher = await crypto.subtle.encrypt({ name: 'AES-CTR', counter, length: 64 }, encryptKey, plaintext);
37
  return new Uint8Array(cipher);
38
}
39
40
async function decryptRange(
41
  key: CryptoKey,
42
  iv: Uint8Array,
43
  ciphertext: Uint8Array,
44
  start: number,
45
  end: number,
46
  chunkSize: number
47
): Promise<Uint8Array> {
48
  const alignedStart = start - (start % 16);
49
  const slice = ciphertext.subarray(alignedStart, end + 1);
50
  const decryptor = new RangeDecryptor(key, iv, alignedStart / 16);
51
  const parts: Uint8Array[] = [];
52
  for (let offset = 0; offset < slice.length; offset += chunkSize) {
53
    const chunk = slice.subarray(offset, Math.min(offset + chunkSize, slice.length));
54
    const plain = await decryptor.push(chunk);
55
    if (plain.length > 0) parts.push(plain);
56
  }
57
  const last = await decryptor.finish();
58
  if (last.length > 0) parts.push(last);
59
  return concatBytes(parts).subarray(start - alignedStart);
60
}
61
62
async function setup(plaintextLength: number) {
63
  const rawKey = randomBytes(32);
64
  const iv = randomBytes(16);
65
  const key = await importAttachmentKey(base64url(rawKey));
66
  const plaintext = randomBytes(plaintextLength);
67
  const ciphertext = await encryptWhole(rawKey, iv, plaintext);
68
  return { key, iv, plaintext, ciphertext };
69
}
70
71
test('range decryptor: whole read matches the plaintext', async () => {
72
  const { key, iv, plaintext, ciphertext } = await setup(1000);
73
  const decryptor = new RangeDecryptor(key, iv, 0);
74
  const decrypted = concatBytes([await decryptor.push(ciphertext), await decryptor.finish()]);
75
  assert.deepEqual(decrypted, plaintext);
76
});
77
78
test('range decryptor: unaligned start and end', async () => {
79
  const { key, iv, plaintext, ciphertext } = await setup(1000);
80
  const start = 37;
81
  const end = 601;
82
  const got = await decryptRange(key, iv, ciphertext, start, end, 512);
83
  assert.deepEqual(got, plaintext.subarray(start, end + 1));
84
});
85
86
test('range decryptor: chunk sizes that split blocks', async () => {
87
  const { key, iv, plaintext, ciphertext } = await setup(1000);
88
  const start = 100;
89
  const end = 900;
90
  const got = await decryptRange(key, iv, ciphertext, start, end, 7);
91
  assert.deepEqual(got, plaintext.subarray(start, end + 1));
92
});
93
94
test('range decryptor: range starting past the first block', async () => {
95
  const { key, iv, plaintext, ciphertext } = await setup(2000);
96
  const start = 1040;
97
  const end = 1500;
98
  const got = await decryptRange(key, iv, ciphertext, start, end, 33);
99
  assert.deepEqual(got, plaintext.subarray(start, end + 1));
100
});
101
102
test('sniff: recognises a few magic numbers', () => {
103
  assert.equal(imageContentType(Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])), 'image/png');
104
  assert.equal(imageContentType(Uint8Array.from([0xff, 0xd8, 0xff, 0xe0])), 'image/jpeg');
105
  assert.equal(attachmentContentType(textBytes('OggS')), 'audio/ogg');
106
  assert.equal(attachmentContentType(Uint8Array.from([0x1a, 0x45, 0xdf, 0xa3])), 'video/webm');
107
  const mp4 = concatBytes([new Uint8Array(4), textBytes('ftyp'), textBytes('isom')]);
108
  assert.equal(attachmentContentType(mp4), 'video/mp4');
109
  assert.equal(attachmentContentType(textBytes('not a media file')), null);
110
});