streaming RAR extractor jsr.io/@mary/rar
jsr
0

Configure Feed

Select the types of activity you want to include in your feed.

pkg-rar / scripts / build-rar3-long-password-fixture.ts
4.6 kB 127 lines
1/** 2 * @module 3 * 4 * one-shot builder for `tests/fixtures/rar3-long-password.rar`. constructs a 5 * single-entry RAR3 archive whose body is AES-128-CBC encrypted with the 6 * rarbug-flavoured KDF, using a 32-character password (66-byte seed) that 7 * activates the in-place SHA-1 corruption. system `unrar` has been used as 8 * an oracle to confirm the resulting archive decrypts to the expected 9 * plaintext. 10 * 11 * run: `deno run -A scripts/build-rar3-long-password-fixture.ts` 12 */ 13 14import { crc32 } from '@mary/crc32'; 15 16import { rar3DeriveKey } from '../lib/rar3/crypto.ts'; 17import { importAesKey } from '../lib/rar5/crypto.ts'; 18import { BlockFlag, BlockType, CompressMethod, FileFlag, HostOs } from '../lib/rar3/constants.ts'; 19 20const PASSWORD = 'the-rarbug-makes-this-key-funny!'; 21const SALT = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); 22const NAME = 'hello.txt'; 23const PLAINTEXT = new TextEncoder().encode('abcdefghijklmnopqrstuvwxyz012345'); 24 25// 2026-05-27 12:00:00 UTC, DOS-encoded: year-1980=46, month=5, day=27, hour=12. 26const MTIME = ((46 << 25) | (5 << 21) | (27 << 16) | (12 << 11)) >>> 0; 27 28const writeU16LE = (buf: Uint8Array, offset: number, value: number): void => { 29 buf[offset + 0] = value & 0xff; 30 buf[offset + 1] = (value >>> 8) & 0xff; 31}; 32 33const writeU32LE = (buf: Uint8Array, offset: number, value: number): void => { 34 buf[offset + 0] = value & 0xff; 35 buf[offset + 1] = (value >>> 8) & 0xff; 36 buf[offset + 2] = (value >>> 16) & 0xff; 37 buf[offset + 3] = (value >>> 24) & 0xff; 38}; 39 40const sealHeader = (header: Uint8Array, crcEnd: number): Uint8Array => { 41 const crc = crc32(header.subarray(2, crcEnd)) & 0xffff; 42 writeU16LE(header, 0, crc); 43 return header; 44}; 45 46const buildMainBlock = (): Uint8Array => { 47 const header = new Uint8Array(13); 48 header[2] = BlockType.main; 49 writeU16LE(header, 3, 0x0000); 50 writeU16LE(header, 5, 13); 51 // 6 reserved bytes already zero. 52 return sealHeader(header, 13); 53}; 54 55const buildFileBlock = (ciphertextLength: number, plaintextCrc: number): Uint8Array => { 56 const nameBytes = new TextEncoder().encode(NAME); 57 const headerSize = 7 + 25 + nameBytes.length + SALT.length; 58 const header = new Uint8Array(headerSize); 59 60 header[2] = BlockType.file; 61 const flags = BlockFlag.longBlock | FileFlag.password | FileFlag.salt; 62 writeU16LE(header, 3, flags); 63 writeU16LE(header, 5, headerSize); 64 writeU32LE(header, 7, ciphertextLength); // packed_size — overlays add_size 65 writeU32LE(header, 11, PLAINTEXT.length); // unpacked_size 66 header[15] = HostOs.msdos; 67 writeU32LE(header, 16, plaintextCrc); 68 writeU32LE(header, 20, MTIME); 69 header[24] = 29; // extract version 70 header[25] = CompressMethod.stored; 71 writeU16LE(header, 26, nameBytes.length); 72 writeU32LE(header, 28, 0x00000020); // DOS archive attribute 73 header.set(nameBytes, 32); 74 header.set(SALT, 32 + nameBytes.length); 75 76 return sealHeader(header, headerSize); 77}; 78 79const buildEndArcBlock = (): Uint8Array => { 80 const header = new Uint8Array(7); 81 header[2] = BlockType.endArc; 82 writeU16LE(header, 3, 0x0000); 83 writeU16LE(header, 5, 7); 84 return sealHeader(header, 7); 85}; 86 87const encryptBody = async (): Promise<Uint8Array> => { 88 const { iv, key } = rar3DeriveKey(PASSWORD, SALT); 89 90 const aes = await importAesKey(key); 91 92 // pad plaintext with PKCS#7 so web crypto encrypts cleanly, then drop the 93 // trailing pad block. RAR3 ciphertext length matches the padded plaintext 94 // length up to (but not past) the 16-byte boundary. 95 const ct = new Uint8Array( 96 await crypto.subtle.encrypt({ iv, name: 'AES-CBC' }, aes, PLAINTEXT), 97 ); 98 const aligned = (PLAINTEXT.length + 15) & ~15; 99 return ct.subarray(0, aligned); 100}; 101 102const main = async (): Promise<void> => { 103 const ciphertext = await encryptBody(); 104 const plaintextCrc = crc32(PLAINTEXT); 105 106 const signature = new Uint8Array([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00]); 107 const main = buildMainBlock(); 108 const file = buildFileBlock(ciphertext.length, plaintextCrc); 109 const endArc = buildEndArcBlock(); 110 111 const total = signature.length + main.length + file.length + ciphertext.length + endArc.length; 112 const archive = new Uint8Array(total); 113 let pos = 0; 114 for (const part of [signature, main, file, ciphertext, endArc]) { 115 archive.set(part, pos); 116 pos += part.length; 117 } 118 119 const outPath = new URL('../tests/fixtures/rar3-long-password.rar', import.meta.url); 120 Deno.writeFileSync(outPath, archive); 121 console.log(`wrote ${total}-byte fixture to ${outPath.pathname}`); 122 console.log(` password: ${PASSWORD} (${PASSWORD.length} chars)`); 123 console.log(` name: ${NAME}`); 124 console.log(` plaintext: ${PLAINTEXT.length} bytes, crc32 0x${plaintextCrc.toString(16)}`); 125}; 126 127await main();