atagen

malice

public
Star0
Watch0
cf020696433e54fc5fb60de3b67218baef8c4748
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/test/media.test.ts

973f8fbdaf80atagen2026-09-28 09:10 UTC1
import assert from 'node:assert/strict';
973f8fbdaf80atagen2026-09-28 09:10 UTC2
import { test } from 'node:test';
dffd63b58af4atagen2026-09-28 09:10 UTC3
import { addBlockOffset, importAttachmentKey, RangeDecryptor } from '../src/lib/media-decrypt.ts';
dffd63b58af4atagen2026-09-28 09:10 UTC4
import { attachmentContentType, imageContentType } from '../src/lib/media-sniff.ts';
973f8fbdaf80atagen2026-09-28 09:10 UTC5
dffd63b58af4atagen2026-09-28 09:10 UTC6
function randomBytes(length: number): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC7
  const bytes = new Uint8Array(length);
973f8fbdaf80atagen2026-09-28 09:10 UTC8
  crypto.getRandomValues(bytes);
973f8fbdaf80atagen2026-09-28 09:10 UTC9
  return bytes;
973f8fbdaf80atagen2026-09-28 09:10 UTC10
}
973f8fbdaf80atagen2026-09-28 09:10 UTC11
dffd63b58af4atagen2026-09-28 09:10 UTC12
function textBytes(text: string): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  return new TextEncoder().encode(text);
973f8fbdaf80atagen2026-09-28 09:10 UTC14
}
973f8fbdaf80atagen2026-09-28 09:10 UTC15
dffd63b58af4atagen2026-09-28 09:10 UTC16
function concatBytes(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC17
  const total = parts.reduce((sum, part) => sum + part.length, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC18
  const out = new Uint8Array(total);
973f8fbdaf80atagen2026-09-28 09:10 UTC19
  let offset = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC20
  for (const part of parts) {
973f8fbdaf80atagen2026-09-28 09:10 UTC21
    out.set(part, offset);
973f8fbdaf80atagen2026-09-28 09:10 UTC22
    offset += part.length;
973f8fbdaf80atagen2026-09-28 09:10 UTC23
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC24
  return out;
973f8fbdaf80atagen2026-09-28 09:10 UTC25
}
973f8fbdaf80atagen2026-09-28 09:10 UTC26
973f8fbdaf80atagen2026-09-28 09:10 UTC27
function base64url(bytes: Uint8Array): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC28
  let binary = '';
973f8fbdaf80atagen2026-09-28 09:10 UTC29
  for (const byte of bytes) binary += String.fromCharCode(byte);
973f8fbdaf80atagen2026-09-28 09:10 UTC30
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
973f8fbdaf80atagen2026-09-28 09:10 UTC31
}
973f8fbdaf80atagen2026-09-28 09:10 UTC32
dffd63b58af4atagen2026-09-28 09:10 UTC33
async function encryptWhole(
dffd63b58af4atagen2026-09-28 09:10 UTC34
  rawKey: Uint8Array<ArrayBuffer>,
dffd63b58af4atagen2026-09-28 09:10 UTC35
  iv: Uint8Array,
dffd63b58af4atagen2026-09-28 09:10 UTC36
  plaintext: Uint8Array<ArrayBuffer>
dffd63b58af4atagen2026-09-28 09:10 UTC37
): Promise<Uint8Array<ArrayBuffer>> {
973f8fbdaf80atagen2026-09-28 09:10 UTC38
  const encryptKey = await crypto.subtle.importKey('raw', rawKey, { name: 'AES-CTR' }, false, ['encrypt']);
973f8fbdaf80atagen2026-09-28 09:10 UTC39
  const counter = addBlockOffset(iv, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC40
  const cipher = await crypto.subtle.encrypt({ name: 'AES-CTR', counter, length: 64 }, encryptKey, plaintext);
973f8fbdaf80atagen2026-09-28 09:10 UTC41
  return new Uint8Array(cipher);
973f8fbdaf80atagen2026-09-28 09:10 UTC42
}
973f8fbdaf80atagen2026-09-28 09:10 UTC43
973f8fbdaf80atagen2026-09-28 09:10 UTC44
async function decryptRange(
973f8fbdaf80atagen2026-09-28 09:10 UTC45
  key: CryptoKey,
973f8fbdaf80atagen2026-09-28 09:10 UTC46
  iv: Uint8Array,
dffd63b58af4atagen2026-09-28 09:10 UTC47
  ciphertext: Uint8Array<ArrayBuffer>,
973f8fbdaf80atagen2026-09-28 09:10 UTC48
  start: number,
973f8fbdaf80atagen2026-09-28 09:10 UTC49
  end: number,
973f8fbdaf80atagen2026-09-28 09:10 UTC50
  chunkSize: number
973f8fbdaf80atagen2026-09-28 09:10 UTC51
): Promise<Uint8Array> {
973f8fbdaf80atagen2026-09-28 09:10 UTC52
  const alignedStart = start - (start % 16);
973f8fbdaf80atagen2026-09-28 09:10 UTC53
  const slice = ciphertext.subarray(alignedStart, end + 1);
973f8fbdaf80atagen2026-09-28 09:10 UTC54
  const decryptor = new RangeDecryptor(key, iv, alignedStart / 16);
973f8fbdaf80atagen2026-09-28 09:10 UTC55
  const parts: Uint8Array[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC56
  for (let offset = 0; offset < slice.length; offset += chunkSize) {
973f8fbdaf80atagen2026-09-28 09:10 UTC57
    const chunk = slice.subarray(offset, Math.min(offset + chunkSize, slice.length));
973f8fbdaf80atagen2026-09-28 09:10 UTC58
    const plain = await decryptor.push(chunk);
973f8fbdaf80atagen2026-09-28 09:10 UTC59
    if (plain.length > 0) parts.push(plain);
973f8fbdaf80atagen2026-09-28 09:10 UTC60
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC61
  const last = await decryptor.finish();
973f8fbdaf80atagen2026-09-28 09:10 UTC62
  if (last.length > 0) parts.push(last);
973f8fbdaf80atagen2026-09-28 09:10 UTC63
  return concatBytes(parts).subarray(start - alignedStart);
973f8fbdaf80atagen2026-09-28 09:10 UTC64
}
973f8fbdaf80atagen2026-09-28 09:10 UTC65
973f8fbdaf80atagen2026-09-28 09:10 UTC66
async function setup(plaintextLength: number) {
973f8fbdaf80atagen2026-09-28 09:10 UTC67
  const rawKey = randomBytes(32);
973f8fbdaf80atagen2026-09-28 09:10 UTC68
  const iv = randomBytes(16);
973f8fbdaf80atagen2026-09-28 09:10 UTC69
  const key = await importAttachmentKey(base64url(rawKey));
973f8fbdaf80atagen2026-09-28 09:10 UTC70
  const plaintext = randomBytes(plaintextLength);
973f8fbdaf80atagen2026-09-28 09:10 UTC71
  const ciphertext = await encryptWhole(rawKey, iv, plaintext);
973f8fbdaf80atagen2026-09-28 09:10 UTC72
  return { key, iv, plaintext, ciphertext };
973f8fbdaf80atagen2026-09-28 09:10 UTC73
}
973f8fbdaf80atagen2026-09-28 09:10 UTC74
973f8fbdaf80atagen2026-09-28 09:10 UTC75
test('range decryptor: whole read matches the plaintext', async () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC76
  const { key, iv, plaintext, ciphertext } = await setup(1000);
973f8fbdaf80atagen2026-09-28 09:10 UTC77
  const decryptor = new RangeDecryptor(key, iv, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC78
  const decrypted = concatBytes([await decryptor.push(ciphertext), await decryptor.finish()]);
973f8fbdaf80atagen2026-09-28 09:10 UTC79
  assert.deepEqual(decrypted, plaintext);
973f8fbdaf80atagen2026-09-28 09:10 UTC80
});
973f8fbdaf80atagen2026-09-28 09:10 UTC81
973f8fbdaf80atagen2026-09-28 09:10 UTC82
test('range decryptor: unaligned start and end', async () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC83
  const { key, iv, plaintext, ciphertext } = await setup(1000);
973f8fbdaf80atagen2026-09-28 09:10 UTC84
  const start = 37;
973f8fbdaf80atagen2026-09-28 09:10 UTC85
  const end = 601;
973f8fbdaf80atagen2026-09-28 09:10 UTC86
  const got = await decryptRange(key, iv, ciphertext, start, end, 512);
973f8fbdaf80atagen2026-09-28 09:10 UTC87
  assert.deepEqual(got, plaintext.subarray(start, end + 1));
973f8fbdaf80atagen2026-09-28 09:10 UTC88
});
973f8fbdaf80atagen2026-09-28 09:10 UTC89
973f8fbdaf80atagen2026-09-28 09:10 UTC90
test('range decryptor: chunk sizes that split blocks', async () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC91
  const { key, iv, plaintext, ciphertext } = await setup(1000);
973f8fbdaf80atagen2026-09-28 09:10 UTC92
  const start = 100;
973f8fbdaf80atagen2026-09-28 09:10 UTC93
  const end = 900;
973f8fbdaf80atagen2026-09-28 09:10 UTC94
  const got = await decryptRange(key, iv, ciphertext, start, end, 7);
973f8fbdaf80atagen2026-09-28 09:10 UTC95
  assert.deepEqual(got, plaintext.subarray(start, end + 1));
973f8fbdaf80atagen2026-09-28 09:10 UTC96
});
973f8fbdaf80atagen2026-09-28 09:10 UTC97
973f8fbdaf80atagen2026-09-28 09:10 UTC98
test('range decryptor: range starting past the first block', async () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC99
  const { key, iv, plaintext, ciphertext } = await setup(2000);
973f8fbdaf80atagen2026-09-28 09:10 UTC100
  const start = 1040;
973f8fbdaf80atagen2026-09-28 09:10 UTC101
  const end = 1500;
973f8fbdaf80atagen2026-09-28 09:10 UTC102
  const got = await decryptRange(key, iv, ciphertext, start, end, 33);
973f8fbdaf80atagen2026-09-28 09:10 UTC103
  assert.deepEqual(got, plaintext.subarray(start, end + 1));
973f8fbdaf80atagen2026-09-28 09:10 UTC104
});
973f8fbdaf80atagen2026-09-28 09:10 UTC105
973f8fbdaf80atagen2026-09-28 09:10 UTC106
test('sniff: recognises a few magic numbers', () => {
973f8fbdaf80atagen2026-09-28 09:10 UTC107
  assert.equal(imageContentType(Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])), 'image/png');
973f8fbdaf80atagen2026-09-28 09:10 UTC108
  assert.equal(imageContentType(Uint8Array.from([0xff, 0xd8, 0xff, 0xe0])), 'image/jpeg');
973f8fbdaf80atagen2026-09-28 09:10 UTC109
  assert.equal(attachmentContentType(textBytes('OggS')), 'audio/ogg');
973f8fbdaf80atagen2026-09-28 09:10 UTC110
  assert.equal(attachmentContentType(Uint8Array.from([0x1a, 0x45, 0xdf, 0xa3])), 'video/webm');
973f8fbdaf80atagen2026-09-28 09:10 UTC111
  const mp4 = concatBytes([new Uint8Array(4), textBytes('ftyp'), textBytes('isom')]);
973f8fbdaf80atagen2026-09-28 09:10 UTC112
  assert.equal(attachmentContentType(mp4), 'video/mp4');
973f8fbdaf80atagen2026-09-28 09:10 UTC113
  assert.equal(attachmentContentType(textBytes('not a media file')), null);
973f8fbdaf80atagen2026-09-28 09:10 UTC114
});