atagen

malice

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

Blame: web/src/lib/media-decrypt.ts

973f8fbdaf80atagen2026-09-28 09:10 UTC1
// AES-256-CTR range decryption for Matrix attachments (the only cipher Matrix
973f8fbdaf80atagen2026-09-28 09:10 UTC2
// has, per vendor/macltok's AttachmentEncryption). WebCrypto's AES-CTR handles
973f8fbdaf80atagen2026-09-28 09:10 UTC3
// a partial final block within one call but not a resumed one, so a block that
973f8fbdaf80atagen2026-09-28 09:10 UTC4
// isn't a multiple of 16 bytes has to wait for more ciphertext before it can be
973f8fbdaf80atagen2026-09-28 09:10 UTC5
// decrypted; see RangeDecryptor.push.
973f8fbdaf80atagen2026-09-28 09:10 UTC6
973f8fbdaf80atagen2026-09-28 09:10 UTC7
const BLOCK_SIZE = 16;
973f8fbdaf80atagen2026-09-28 09:10 UTC8
dffd63b58af4atagen2026-09-28 09:10 UTC9
function decodeBase64(value: string): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC10
  const normalised = value.replace(/-/g, '+').replace(/_/g, '/');
973f8fbdaf80atagen2026-09-28 09:10 UTC11
  const padded = normalised + '='.repeat((4 - (normalised.length % 4)) % 4);
973f8fbdaf80atagen2026-09-28 09:10 UTC12
  const binary = atob(padded);
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  const bytes = new Uint8Array(binary.length);
973f8fbdaf80atagen2026-09-28 09:10 UTC14
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
973f8fbdaf80atagen2026-09-28 09:10 UTC15
  return bytes;
973f8fbdaf80atagen2026-09-28 09:10 UTC16
}
973f8fbdaf80atagen2026-09-28 09:10 UTC17
973f8fbdaf80atagen2026-09-28 09:10 UTC18
export async function importAttachmentKey(k: string): Promise<CryptoKey> {
973f8fbdaf80atagen2026-09-28 09:10 UTC19
  return crypto.subtle.importKey('raw', decodeBase64(k), { name: 'AES-CTR' }, false, ['decrypt']);
973f8fbdaf80atagen2026-09-28 09:10 UTC20
}
973f8fbdaf80atagen2026-09-28 09:10 UTC21
dffd63b58af4atagen2026-09-28 09:10 UTC22
export function decodeAttachmentIv(iv: string): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC23
  const bytes = decodeBase64(iv);
973f8fbdaf80atagen2026-09-28 09:10 UTC24
  if (bytes.length >= BLOCK_SIZE) return bytes.subarray(0, BLOCK_SIZE);
973f8fbdaf80atagen2026-09-28 09:10 UTC25
  const padded = new Uint8Array(BLOCK_SIZE);
973f8fbdaf80atagen2026-09-28 09:10 UTC26
  padded.set(bytes);
973f8fbdaf80atagen2026-09-28 09:10 UTC27
  return padded;
973f8fbdaf80atagen2026-09-28 09:10 UTC28
}
973f8fbdaf80atagen2026-09-28 09:10 UTC29
973f8fbdaf80atagen2026-09-28 09:10 UTC30
/** iv + blocks, added as a big-endian 64-bit value into the low 8 bytes only, per the Matrix attachment convention. */
dffd63b58af4atagen2026-09-28 09:10 UTC31
export function addBlockOffset(iv: Uint8Array, blocks: number): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC32
  const counter = iv.slice();
973f8fbdaf80atagen2026-09-28 09:10 UTC33
  const view = new DataView(counter.buffer, counter.byteOffset, counter.byteLength);
973f8fbdaf80atagen2026-09-28 09:10 UTC34
  const low = view.getBigUint64(8);
973f8fbdaf80atagen2026-09-28 09:10 UTC35
  view.setBigUint64(8, (low + BigInt(blocks)) & 0xffffffffffffffffn);
973f8fbdaf80atagen2026-09-28 09:10 UTC36
  return counter;
973f8fbdaf80atagen2026-09-28 09:10 UTC37
}
973f8fbdaf80atagen2026-09-28 09:10 UTC38
dffd63b58af4atagen2026-09-28 09:10 UTC39
function concat(a: Uint8Array<ArrayBuffer>, b: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer> {
973f8fbdaf80atagen2026-09-28 09:10 UTC40
  if (a.length === 0) return b;
973f8fbdaf80atagen2026-09-28 09:10 UTC41
  if (b.length === 0) return a;
973f8fbdaf80atagen2026-09-28 09:10 UTC42
  const out = new Uint8Array(a.length + b.length);
973f8fbdaf80atagen2026-09-28 09:10 UTC43
  out.set(a, 0);
973f8fbdaf80atagen2026-09-28 09:10 UTC44
  out.set(b, a.length);
973f8fbdaf80atagen2026-09-28 09:10 UTC45
  return out;
973f8fbdaf80atagen2026-09-28 09:10 UTC46
}
973f8fbdaf80atagen2026-09-28 09:10 UTC47
973f8fbdaf80atagen2026-09-28 09:10 UTC48
/**
973f8fbdaf80atagen2026-09-28 09:10 UTC49
 * Decrypts a ciphertext range that starts `startBlock` 16-byte blocks into the
973f8fbdaf80atagen2026-09-28 09:10 UTC50
 * attachment. Feed it ciphertext in arrival order via `push`, then `finish` once
973f8fbdaf80atagen2026-09-28 09:10 UTC51
 * the source is exhausted to flush the trailing partial block.
973f8fbdaf80atagen2026-09-28 09:10 UTC52
 */
973f8fbdaf80atagen2026-09-28 09:10 UTC53
export class RangeDecryptor {
dffd63b58af4atagen2026-09-28 09:10 UTC54
  private buffered: Uint8Array<ArrayBuffer> = new Uint8Array(0);
973f8fbdaf80atagen2026-09-28 09:10 UTC55
  private blocksConsumed = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC56
973f8fbdaf80atagen2026-09-28 09:10 UTC57
  constructor(
973f8fbdaf80atagen2026-09-28 09:10 UTC58
    private readonly key: CryptoKey,
973f8fbdaf80atagen2026-09-28 09:10 UTC59
    private readonly iv: Uint8Array,
973f8fbdaf80atagen2026-09-28 09:10 UTC60
    private readonly startBlock: number
973f8fbdaf80atagen2026-09-28 09:10 UTC61
  ) {}
973f8fbdaf80atagen2026-09-28 09:10 UTC62
dffd63b58af4atagen2026-09-28 09:10 UTC63
  async push(chunk: Uint8Array<ArrayBuffer>): Promise<Uint8Array> {
973f8fbdaf80atagen2026-09-28 09:10 UTC64
    const data = concat(this.buffered, chunk);
973f8fbdaf80atagen2026-09-28 09:10 UTC65
    const wholeLen = data.length - (data.length % BLOCK_SIZE);
973f8fbdaf80atagen2026-09-28 09:10 UTC66
    if (wholeLen === 0) {
973f8fbdaf80atagen2026-09-28 09:10 UTC67
      this.buffered = data;
973f8fbdaf80atagen2026-09-28 09:10 UTC68
      return new Uint8Array(0);
973f8fbdaf80atagen2026-09-28 09:10 UTC69
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC70
    this.buffered = data.subarray(wholeLen);
973f8fbdaf80atagen2026-09-28 09:10 UTC71
    return this.decrypt(data.subarray(0, wholeLen));
973f8fbdaf80atagen2026-09-28 09:10 UTC72
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC73
973f8fbdaf80atagen2026-09-28 09:10 UTC74
  async finish(): Promise<Uint8Array> {
973f8fbdaf80atagen2026-09-28 09:10 UTC75
    if (this.buffered.length === 0) return new Uint8Array(0);
973f8fbdaf80atagen2026-09-28 09:10 UTC76
    const plain = await this.decrypt(this.buffered);
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    this.buffered = new Uint8Array(0);
973f8fbdaf80atagen2026-09-28 09:10 UTC78
    return plain;
973f8fbdaf80atagen2026-09-28 09:10 UTC79
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC80
dffd63b58af4atagen2026-09-28 09:10 UTC81
  private async decrypt(ciphertext: Uint8Array<ArrayBuffer>): Promise<Uint8Array> {
973f8fbdaf80atagen2026-09-28 09:10 UTC82
    const counter = addBlockOffset(this.iv, this.startBlock + this.blocksConsumed);
973f8fbdaf80atagen2026-09-28 09:10 UTC83
    this.blocksConsumed += Math.ceil(ciphertext.length / BLOCK_SIZE);
973f8fbdaf80atagen2026-09-28 09:10 UTC84
    const plain = await crypto.subtle.decrypt({ name: 'AES-CTR', counter, length: 64 }, this.key, ciphertext);
973f8fbdaf80atagen2026-09-28 09:10 UTC85
    return new Uint8Array(plain);
973f8fbdaf80atagen2026-09-28 09:10 UTC86
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC87
}