atagen

malice

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