package pdf import "core:fmt" import "core:mem/virtual" import "core:slice" import "core:strings" import "core:testing" // The RC4 keystreams below are RFC 6229's; the AES-CBC blocks are NIST // SP 800-38A's. Everything that is specific to PDF -- the /O and /U values, the // derived file keys, the per-object keys, the R6 hash -- was computed with an // independent implementation (Python, cross-checked against PyPDF2, whose R6 // hash comes from qpdf) and is pasted here as a literal. Nothing in this file // asks crypt.odin to check its own work. // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- // The first /ID string used by every hand-built dictionary here: 10 11 .. 1f. @(private = "file") crypt_t_id0 := [16]byte{ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, } @(private = "file") crypt_t_new_doc :: proc(t: ^testing.T) -> ^Document { doc := new(Document) if err := virtual.arena_init_growing(&doc.arena); err != nil { testing.fail_now(t, "arena") } doc.allocator = virtual.arena_allocator(&doc.arena) doc.backing = context.allocator doc.xref = make([dynamic]Xref_Entry, doc.allocator) doc.cache = make(map[u32]Object, 8, doc.allocator) doc.loading = make(map[u32]bool, 4, doc.allocator) doc.objstm_cache = make(map[u32]^Objstm, 4, doc.allocator) doc.pages = make([dynamic]^Dict, doc.allocator) doc.page_refs = make([dynamic]Ref, doc.allocator) doc.repairs = make([dynamic]Repair, doc.allocator) doc.trailer = new_dict(doc.allocator) return doc } // crypt_t_str stores a byte string in the document arena and returns it as a // PDF string object, the way the parser would have. @(private = "file") crypt_t_str :: proc(doc: ^Document, val: []byte) -> Object { b := make([]byte, len(val), doc.allocator) copy(b, val) return transmute(String)b } // crypt_t_id installs a two-element /ID array in the trailer. @(private = "file") crypt_t_id :: proc(doc: ^Document, id0: []byte) { a := new_array(doc.allocator, 2) array_push(a, crypt_t_str(doc, id0)) array_push(a, crypt_t_str(doc, id0)) dict_set(doc.trailer, "ID", a) } @(private = "file") crypt_t_cf :: proc(doc: ^Document, cfm: string, length: i64) -> ^Dict { cf := new_dict(doc.allocator, 2) std := new_dict(doc.allocator, 4) dict_set(std, "Type", Name("CryptFilter")) dict_set(std, "CFM", Name(cfm)) if length > 0 { dict_set(std, "Length", length) } dict_set(cf, "StdCF", std) return cf } // --------------------------------------------------------------------------- // RC4 -- RFC 6229 test vectors // --------------------------------------------------------------------------- @(private = "file") Rc4_Vector :: struct { key: []byte, at_0: []byte, // keystream bytes 0..15 at_16: []byte, // keystream bytes 16..31 at_240: []byte, // keystream bytes 240..255 at_256: []byte, } @(test) test_crypt_rc4_rfc6229 :: proc(t: ^testing.T) { vectors := []Rc4_Vector{ { // 40-bit key = {0x01, 0x02, 0x03, 0x04, 0x05}, at_0 = {0xb2, 0x39, 0x63, 0x05, 0xf0, 0x3d, 0xc0, 0x27, 0xcc, 0xc3, 0x52, 0x4a, 0x0a, 0x11, 0x18, 0xa8}, at_16 = {0x69, 0x82, 0x94, 0x4f, 0x18, 0xfc, 0x82, 0xd5, 0x89, 0xc4, 0x03, 0xa4, 0x7a, 0x0d, 0x09, 0x19}, at_240 = {0x28, 0xcb, 0x11, 0x32, 0xc9, 0x6c, 0xe2, 0x86, 0x42, 0x1d, 0xca, 0xad, 0xb8, 0xb6, 0x9e, 0xae}, at_256 = {0x1c, 0xfc, 0xf6, 0x2b, 0x03, 0xed, 0xdb, 0x64, 0x1d, 0x77, 0xdf, 0xcf, 0x7f, 0x8d, 0x8c, 0x93}, }, { // 56-bit key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, at_0 = {0x29, 0x3f, 0x02, 0xd4, 0x7f, 0x37, 0xc9, 0xb6, 0x33, 0xf2, 0xaf, 0x52, 0x85, 0xfe, 0xb4, 0x6b}, at_16 = {0xe6, 0x20, 0xf1, 0x39, 0x0d, 0x19, 0xbd, 0x84, 0xe2, 0xe0, 0xfd, 0x75, 0x20, 0x31, 0xaf, 0xc1}, at_240 = {0x91, 0x4f, 0x02, 0x53, 0x1c, 0x92, 0x18, 0x81, 0x0d, 0xf6, 0x0f, 0x67, 0xe3, 0x38, 0x15, 0x4c}, at_256 = {0xd0, 0xfd, 0xb5, 0x83, 0x07, 0x3c, 0xe8, 0x5a, 0xb8, 0x39, 0x17, 0x74, 0x0e, 0xc0, 0x11, 0xd5}, }, { // 64-bit key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, at_0 = {0x97, 0xab, 0x8a, 0x1b, 0xf0, 0xaf, 0xb9, 0x61, 0x32, 0xf2, 0xf6, 0x72, 0x58, 0xda, 0x15, 0xa8}, at_16 = {0x82, 0x63, 0xef, 0xdb, 0x45, 0xc4, 0xa1, 0x86, 0x84, 0xef, 0x87, 0xe6, 0xb1, 0x9e, 0x5b, 0x09}, at_240 = {0x96, 0x36, 0xeb, 0xc9, 0x84, 0x19, 0x26, 0xf4, 0xf7, 0xd1, 0xf3, 0x62, 0xbd, 0xdf, 0x6e, 0x18}, at_256 = {0xd0, 0xa9, 0x90, 0xff, 0x2c, 0x05, 0xfe, 0xf5, 0xb9, 0x03, 0x73, 0xc9, 0xff, 0x4b, 0x87, 0x0a}, }, { // 128-bit, the length every PDF above R2 uses key = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, at_0 = {0x9a, 0xc7, 0xcc, 0x9a, 0x60, 0x9d, 0x1e, 0xf7, 0xb2, 0x93, 0x28, 0x99, 0xcd, 0xe4, 0x1b, 0x97}, at_16 = {0x52, 0x48, 0xc4, 0x95, 0x90, 0x14, 0x12, 0x6a, 0x6e, 0x8a, 0x84, 0xf1, 0x1d, 0x1a, 0x9e, 0x1c}, at_240 = {0x06, 0x59, 0x02, 0xe4, 0xb6, 0x20, 0xf6, 0xcc, 0x36, 0xc8, 0x58, 0x9f, 0x66, 0x43, 0x2f, 0x2b}, at_256 = {0xd3, 0x9d, 0x56, 0x6b, 0xc6, 0xbc, 0xe3, 0x01, 0x07, 0x68, 0x15, 0x15, 0x49, 0xf3, 0x87, 0x3f}, }, { // 40-bit, non-sequential key key = {0x83, 0x32, 0x22, 0x77, 0x2a}, at_0 = {0x80, 0xad, 0x97, 0xbd, 0xc9, 0x73, 0xdf, 0x8a, 0x2e, 0x87, 0x9e, 0x92, 0xa4, 0x97, 0xef, 0xda}, at_16 = {0x20, 0xf0, 0x60, 0xc2, 0xf2, 0xe5, 0x12, 0x65, 0x01, 0xd3, 0xd4, 0xfe, 0xa1, 0x0d, 0x5f, 0xc0}, at_240 = {0xfa, 0xa1, 0x48, 0xe9, 0x90, 0x46, 0x18, 0x1f, 0xec, 0x6b, 0x20, 0x85, 0xf3, 0xb2, 0x0e, 0xd9}, at_256 = {0xf0, 0xda, 0xf5, 0xba, 0xb3, 0xd5, 0x96, 0x83, 0x98, 0x57, 0x84, 0x6f, 0x73, 0xfb, 0xfe, 0x5a}, }, } // Encrypting zeroes yields the keystream, which is what RFC 6229 tabulates. stream: [272]byte for v, i in vectors { slice.zero(stream[:]) rc4_crypt(v.key, stream[:], stream[:]) testing.expectf(t, slice.equal(stream[0:16], v.at_0), "vector %d offset 0: %x", i, stream[0:16]) testing.expectf(t, slice.equal(stream[16:32], v.at_16), "vector %d offset 16: %x", i, stream[16:32]) testing.expectf(t, slice.equal(stream[240:256], v.at_240), "vector %d offset 240: %x", i, stream[240:256]) testing.expectf(t, slice.equal(stream[256:272], v.at_256), "vector %d offset 256: %x", i, stream[256:272]) } } @(test) test_crypt_rc4_classic :: proc(t: ^testing.T) { Case :: struct { key, plain, cipher: []byte, } cases := []Case{ {transmute([]byte)string("Key"), transmute([]byte)string("Plaintext"), {0xbb, 0xf3, 0x16, 0xe8, 0xd9, 0x40, 0xaf, 0x0a, 0xd3}}, {transmute([]byte)string("Wiki"), transmute([]byte)string("pedia"), {0x10, 0x21, 0xbf, 0x04, 0x20}}, {transmute([]byte)string("Secret"), transmute([]byte)string("Attack at dawn"), {0x45, 0xa0, 0x1f, 0x64, 0x5f, 0xc3, 0x5b, 0x38, 0x35, 0x52, 0x54, 0x4b, 0x9b, 0xf5}}, } out: [64]byte for c, i in cases { rc4_crypt(c.key, out[:len(c.plain)], c.plain) testing.expectf(t, slice.equal(out[:len(c.plain)], c.cipher), "case %d encrypt: %x", i, out[:len(c.plain)]) // RC4 is an XOR with a keystream, so decryption is the same operation. rc4_crypt(c.key, out[:len(c.cipher)], c.cipher) testing.expectf(t, slice.equal(out[:len(c.plain)], c.plain), "case %d decrypt: %x", i, out[:len(c.plain)]) } } @(test) test_crypt_rc4_aliasing_and_empty_key :: proc(t: ^testing.T) { // In-place is how decrypt_string uses it for the /O and /U round trips. buf := [9]byte{'P', 'l', 'a', 'i', 'n', 't', 'e', 'x', 't'} want := [9]byte{0xbb, 0xf3, 0x16, 0xe8, 0xd9, 0x40, 0xaf, 0x0a, 0xd3} rc4_crypt(transmute([]byte)string("Key"), buf[:], buf[:]) testing.expectf(t, slice.equal(buf[:], want[:]), "in place: %x", buf) // An empty key would divide by zero in the key schedule; it passes through. src := [4]byte{1, 2, 3, 4} dst: [4]byte rc4_crypt(nil, dst[:], src[:]) testing.expect(t, slice.equal(dst[:], src[:]), "empty key should pass data through") } // --------------------------------------------------------------------------- // AES-CBC -- NIST SP 800-38A test vectors // --------------------------------------------------------------------------- @(private = "file") crypt_t_nist_iv := [16]byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, } @(private = "file") crypt_t_nist_plain := [64]byte{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a, 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51, 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef, 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10, } @(test) test_crypt_aes_cbc_nist :: proc(t: ^testing.T) { // SP 800-38A F.2.1 / F.2.2, AES-128. key128 := [16]byte{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, } want128 := [64]byte{ 0x76, 0x49, 0xab, 0xac, 0x81, 0x19, 0xb2, 0x46, 0xce, 0xe9, 0x8e, 0x9b, 0x12, 0xe9, 0x19, 0x7d, 0x50, 0x86, 0xcb, 0x9b, 0x50, 0x72, 0x19, 0xee, 0x95, 0xdb, 0x11, 0x3a, 0x91, 0x76, 0x78, 0xb2, 0x73, 0xbe, 0xd6, 0xb8, 0xe3, 0xc1, 0x74, 0x3b, 0x71, 0x16, 0xe6, 0x9e, 0x22, 0x22, 0x95, 0x16, 0x3f, 0xf1, 0xca, 0xa1, 0x68, 0x1f, 0xac, 0x09, 0x12, 0x0e, 0xca, 0x30, 0x75, 0x86, 0xe1, 0xa7, } // SP 800-38A F.2.5 / F.2.6, AES-256. key256 := [32]byte{ 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, } want256 := [64]byte{ 0xf5, 0x8c, 0x4c, 0x04, 0xd6, 0xe5, 0xf1, 0xba, 0x77, 0x9e, 0xab, 0xfb, 0x5f, 0x7b, 0xfb, 0xd6, 0x9c, 0xfc, 0x4e, 0x96, 0x7e, 0xdb, 0x80, 0x8d, 0x67, 0x9f, 0x77, 0x7b, 0xc6, 0x70, 0x2c, 0x7d, 0x39, 0xf2, 0x33, 0x69, 0xa9, 0xd9, 0xba, 0xcf, 0xa5, 0x30, 0xe2, 0x63, 0x04, 0x23, 0x14, 0x61, 0xb2, 0xeb, 0x05, 0xe2, 0xc3, 0x9b, 0xe9, 0xfc, 0xda, 0x6c, 0x19, 0x07, 0x8c, 0x6a, 0x9d, 0x1b, } out: [64]byte back: [64]byte testing.expect(t, aes_cbc_encrypt(out[:], crypt_t_nist_plain[:], key128[:], crypt_t_nist_iv[:])) testing.expectf(t, slice.equal(out[:], want128[:]), "aes-128 encrypt: %x", out) testing.expect(t, aes_cbc_decrypt(back[:], want128[:], key128[:], crypt_t_nist_iv[:])) testing.expectf(t, slice.equal(back[:], crypt_t_nist_plain[:]), "aes-128 decrypt: %x", back) testing.expect(t, aes_cbc_encrypt(out[:], crypt_t_nist_plain[:], key256[:], crypt_t_nist_iv[:])) testing.expectf(t, slice.equal(out[:], want256[:]), "aes-256 encrypt: %x", out) testing.expect(t, aes_cbc_decrypt(back[:], want256[:], key256[:], crypt_t_nist_iv[:])) testing.expectf(t, slice.equal(back[:], crypt_t_nist_plain[:]), "aes-256 decrypt: %x", back) // In-place decryption: crypt_aes_decrypt relies on dst and src aliasing // being harmless. copy(back[:], want256[:]) testing.expect(t, aes_cbc_decrypt(back[:], back[:], key256[:], crypt_t_nist_iv[:])) testing.expectf(t, slice.equal(back[:], crypt_t_nist_plain[:]), "aes-256 in place: %x", back) } @(test) test_crypt_aes_cbc_rejects_bad_shapes :: proc(t: ^testing.T) { key := [16]byte{} iv := [16]byte{} buf := [64]byte{} testing.expect(t, !aes_cbc_encrypt(buf[:], buf[:17], key[:], iv[:]), "partial block") testing.expect(t, !aes_cbc_decrypt(buf[:], buf[:17], key[:], iv[:]), "partial block") testing.expect(t, !aes_cbc_encrypt(buf[:15], buf[:16], key[:], iv[:]), "short destination") testing.expect(t, !aes_cbc_encrypt(buf[:16], buf[:16], key[:7], iv[:]), "7-byte key") testing.expect(t, !aes_cbc_encrypt(buf[:16], buf[:16], key[:], iv[:8]), "8-byte iv") testing.expect(t, !aes_cbc_encrypt(buf[:16], buf[:16], nil, iv[:]), "no key") // Zero blocks is legal and does nothing. testing.expect(t, aes_cbc_encrypt(buf[:0], buf[:0], key[:], iv[:]), "empty input") } // --------------------------------------------------------------------------- // Key derivation, revisions 2 to 4 // --------------------------------------------------------------------------- @(private = "file") Kd_Case :: struct { name: string, version: i64, revision: i64, length: i64, // /Length, 0 to omit cfm: string, // "" for V < 4 permissions: i64, encrypt_metadata: bool, user_password: string, owner_password: string, o: []byte, u: []byte, want_key: []byte, want_method: Crypt_Method, } @(private = "file") crypt_t_build :: proc(doc: ^Document, c: Kd_Case) -> ^Dict { ed := new_dict(doc.allocator, 12) dict_set(ed, "Filter", Name("Standard")) dict_set(ed, "V", c.version) dict_set(ed, "R", c.revision) if c.length > 0 { dict_set(ed, "Length", c.length) } dict_set(ed, "P", c.permissions) dict_set(ed, "O", crypt_t_str(doc, c.o)) dict_set(ed, "U", crypt_t_str(doc, c.u)) if !c.encrypt_metadata { dict_set(ed, "EncryptMetadata", false) } if c.cfm != "" { dict_set(ed, "CF", crypt_t_cf(doc, c.cfm, 16)) dict_set(ed, "StmF", Name("StdCF")) dict_set(ed, "StrF", Name("StdCF")) } crypt_t_id(doc, crypt_t_id0[:]) dict_set(doc.trailer, "Encrypt", ed) return ed } // The five revision/filter combinations, each with an /Encrypt dictionary // built by an independent implementation. @(private = "file") crypt_t_cases := []Kd_Case{ { name = "R2 RC4 40-bit, empty user password", version = 1, revision = 2, length = 0, cfm = "", permissions = -1, encrypt_metadata = true, user_password = "", owner_password = "owner", o = {0xc9, 0x24, 0x22, 0x68, 0x7f, 0xac, 0xee, 0x68, 0x6e, 0x37, 0x3f, 0x10, 0xb5, 0xc7, 0xd0, 0x47, 0x38, 0x05, 0x31, 0x52, 0xf7, 0xe2, 0xee, 0x30, 0xe1, 0x1c, 0x69, 0xec, 0x44, 0x25, 0x76, 0xab}, u = {0x53, 0xbc, 0x45, 0x8d, 0xf0, 0xc3, 0x97, 0xc5, 0xf6, 0xd1, 0xaa, 0x43, 0xe3, 0x59, 0x33, 0x3f, 0x52, 0xd3, 0xaa, 0x81, 0xc2, 0xab, 0xf0, 0xe9, 0xc1, 0x1f, 0x59, 0x2c, 0xb3, 0xad, 0x8c, 0xe5}, want_key = {0xdd, 0xc8, 0xcf, 0x63, 0x76}, want_method = .Rc4, }, { name = "R3 RC4 128-bit, empty user password", version = 2, revision = 3, length = 128, cfm = "", permissions = -4, encrypt_metadata = true, user_password = "", owner_password = "owner", o = {0x56, 0x6f, 0xa8, 0x73, 0xee, 0x33, 0xc7, 0x97, 0xcd, 0x3b, 0x90, 0x4f, 0xda, 0xdf, 0x81, 0x4a, 0xfa, 0x34, 0xdf, 0x9a, 0x38, 0xf6, 0xed, 0x41, 0xb9, 0x84, 0xe2, 0xc6, 0xda, 0x2a, 0xa6, 0xf5}, u = {0xc3, 0x60, 0xd1, 0xa9, 0xe2, 0xbe, 0x8a, 0xde, 0x6b, 0x1f, 0x07, 0xcc, 0x97, 0xa5, 0x0a, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, want_key = {0xf0, 0x4b, 0x6a, 0x74, 0x07, 0x17, 0x51, 0x50, 0x48, 0xf1, 0xe6, 0x38, 0x8e, 0x8a, 0xb2, 0x77}, want_method = .Rc4, }, { name = "R3 RC4 40-bit, user password 'user'", version = 2, revision = 3, length = 40, cfm = "", permissions = -64, encrypt_metadata = true, user_password = "user", owner_password = "owner", o = {0x3c, 0x48, 0x21, 0x62, 0x00, 0x8f, 0xaf, 0xcb, 0x22, 0x8b, 0x7d, 0xb3, 0xc4, 0x3a, 0x10, 0x90, 0xbc, 0x5b, 0x56, 0xe9, 0xb1, 0x55, 0x6e, 0x89, 0xfc, 0x06, 0x56, 0xfd, 0x29, 0x1f, 0x49, 0x08}, u = {0x42, 0xf1, 0xc3, 0x99, 0x3f, 0x9b, 0xa0, 0x35, 0x59, 0x22, 0x27, 0x0c, 0xb4, 0xf0, 0x49, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, want_key = {0x33, 0x4c, 0xc2, 0x25, 0xc5}, want_method = .Rc4, }, { name = "R4 crypt filter /V2 (RC4 128-bit)", version = 4, revision = 4, length = 128, cfm = "V2", permissions = -1340, encrypt_metadata = true, user_password = "", owner_password = "owner", o = {0x56, 0x6f, 0xa8, 0x73, 0xee, 0x33, 0xc7, 0x97, 0xcd, 0x3b, 0x90, 0x4f, 0xda, 0xdf, 0x81, 0x4a, 0xfa, 0x34, 0xdf, 0x9a, 0x38, 0xf6, 0xed, 0x41, 0xb9, 0x84, 0xe2, 0xc6, 0xda, 0x2a, 0xa6, 0xf5}, u = {0xa5, 0x62, 0xc9, 0x4b, 0x96, 0x94, 0x67, 0x3f, 0x4b, 0x05, 0xcd, 0xfc, 0xe9, 0x9a, 0x0d, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, want_key = {0x3f, 0xe3, 0xcb, 0x9a, 0x5d, 0xea, 0x1e, 0xac, 0x1c, 0xea, 0xb4, 0xe4, 0xb4, 0x21, 0x23, 0xfb}, want_method = .Rc4, }, { // /EncryptMetadata false: the four 0xFF bytes fold into the key, so // the same password and the same /O give a different key. name = "R4 crypt filter /AESV2, EncryptMetadata false", version = 4, revision = 4, length = 128, cfm = "AESV2", permissions = -1340, encrypt_metadata = false, user_password = "", owner_password = "owner", o = {0x56, 0x6f, 0xa8, 0x73, 0xee, 0x33, 0xc7, 0x97, 0xcd, 0x3b, 0x90, 0x4f, 0xda, 0xdf, 0x81, 0x4a, 0xfa, 0x34, 0xdf, 0x9a, 0x38, 0xf6, 0xed, 0x41, 0xb9, 0x84, 0xe2, 0xc6, 0xda, 0x2a, 0xa6, 0xf5}, u = {0x18, 0x15, 0x6a, 0x0f, 0x71, 0x2b, 0xdc, 0x8c, 0x21, 0x03, 0xd3, 0xf0, 0x50, 0x3a, 0xf0, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, want_key = {0xd8, 0x1a, 0x8d, 0x4a, 0x89, 0x5f, 0x5a, 0xeb, 0x51, 0xfa, 0x19, 0xb8, 0x91, 0x91, 0x37, 0x34}, want_method = .Aes_128, }, } @(test) test_crypt_key_derivation :: proc(t: ^testing.T) { for c in crypt_t_cases { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, c) err := crypt_init(doc, transmute([]byte)c.user_password) testing.expectf(t, err == .None, "%s: crypt_init = %v", c.name, err) if doc.encrypt == nil { testing.expectf(t, false, "%s: no encryption state", c.name) continue } testing.expectf(t, slice.equal(doc.encrypt.key, c.want_key), "%s: key = %x, want %x", c.name, doc.encrypt.key, c.want_key) testing.expectf(t, doc.encrypt.authenticated_as == .User, "%s: authenticated as %v", c.name, doc.encrypt.authenticated_as) testing.expectf(t, doc.encrypt.stream_method == c.want_method, "%s: stream method %v", c.name, doc.encrypt.stream_method) testing.expectf(t, doc.encrypt.revision == int(c.revision), "%s: revision %d", c.name, doc.encrypt.revision) } } @(test) test_crypt_owner_password :: proc(t: ^testing.T) { // The same documents, opened with the owner password instead. The file key // is identical -- /O only ever encodes the user password. for c in crypt_t_cases { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, c) err := crypt_init(doc, transmute([]byte)c.owner_password) testing.expectf(t, err == .None, "%s: crypt_init = %v", c.name, err) if doc.encrypt == nil { continue } testing.expectf(t, doc.encrypt.authenticated_as == .Owner, "%s: authenticated as %v, want Owner", c.name, doc.encrypt.authenticated_as) testing.expectf(t, slice.equal(doc.encrypt.key, c.want_key), "%s: owner key = %x, want %x", c.name, doc.encrypt.key, c.want_key) } } @(test) test_crypt_wrong_password :: proc(t: ^testing.T) { for c in crypt_t_cases { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, c) err := crypt_init(doc, transmute([]byte)string("definitely not it")) testing.expectf(t, err == .Wrong_Password, "%s: err = %v", c.name, err) testing.expectf(t, doc.encrypt == nil, "%s: encryption state left behind after a failed password", c.name) } // A file with a non-empty user password must not open on the empty one. doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[2]) testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "empty password accepted") } // --------------------------------------------------------------------------- // Per-object keys (Algorithm 1) // --------------------------------------------------------------------------- @(test) test_crypt_object_key :: proc(t: ^testing.T) { enc128 := Encryption{ key = []byte{0xf0, 0x4b, 0x6a, 0x74, 0x07, 0x17, 0x51, 0x50, 0x48, 0xf1, 0xe6, 0x38, 0x8e, 0x8a, 0xb2, 0x77}, } enc40 := Encryption{ key = []byte{0xdd, 0xc8, 0xcf, 0x63, 0x76}, } enc256 := Encryption{ key = []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, }, } Case :: struct { name: string, enc: ^Encryption, method: Crypt_Method, ref: Ref, want: []byte, } e128, e40, e256 := enc128, enc40, enc256 cases := []Case{ {"128-bit RC4, object 7 gen 0", &e128, .Rc4, {7, 0}, {0xaa, 0x9b, 0xec, 0x87, 0xa8, 0x18, 0xe4, 0xcb, 0x60, 0x88, 0x96, 0x3e, 0x64, 0xc3, 0x11, 0x41}}, // The "sAlT" suffix is the only difference from the line above. {"128-bit AES, object 7 gen 0", &e128, .Aes_128, {7, 0}, {0x58, 0x2d, 0xb9, 0xf3, 0xe9, 0xca, 0xe2, 0x66, 0xed, 0x0a, 0x49, 0x07, 0xa3, 0x6e, 0xbd, 0x22}}, // n + 5 = 10 bytes for a 40-bit file key, not 16. {"40-bit RC4, object 3 gen 5", &e40, .Rc4, {3, 5}, {0x42, 0x42, 0x24, 0x85, 0x5e, 0xc1, 0x48, 0x3e, 0x0e, 0xd4}}, // Only the low three bytes of the number and the low two of the // generation take part. {"128-bit RC4, object 0xABCDEF gen 0x1234", &e128, .Rc4, {0xABCDEF, 0x1234}, {0x18, 0x02, 0xdf, 0xb8, 0x5c, 0x35, 0x6b, 0x7e, 0x00, 0xe6, 0x37, 0x9a, 0xe3, 0xeb, 0x73, 0xc1}}, // AES-256 uses the file key untouched for every object. {"256-bit AES uses the file key", &e256, .Aes_256, {7, 0}, e256.key}, } out: [32]byte for c in cases { n := crypt_object_key(c.enc, c.method, c.ref, out[:]) testing.expectf(t, n == len(c.want), "%s: %d bytes, want %d", c.name, n, len(c.want)) if n != len(c.want) { continue } testing.expectf(t, slice.equal(out[:n], c.want), "%s: %x", c.name, out[:n]) } // A file key of an impossible length yields nothing rather than a bad key. bad := Encryption{key = make([]byte, 20)} defer delete(bad.key) testing.expect(t, crypt_object_key(&bad, .Rc4, {1, 0}, out[:]) == 0, "20-byte RC4 key") testing.expect(t, crypt_object_key(&bad, .Aes_256, {1, 0}, out[:]) == 0, "20-byte AES-256 key") empty := Encryption{} testing.expect(t, crypt_object_key(&empty, .Rc4, {1, 0}, out[:]) == 0, "empty key") } // --------------------------------------------------------------------------- // End-to-end decryption // --------------------------------------------------------------------------- @(test) test_crypt_decrypt_rc4_string :: proc(t: ^testing.T) { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[1]) // R3, RC4 128-bit testing.expect(t, crypt_init(doc, nil) == .None) // Produced outside this library from the same file key. cipher := []byte{ 0x0b, 0x73, 0xcf, 0xc8, 0x51, 0x25, 0x25, 0x2c, 0x20, 0x46, 0x94, 0x78, 0x4d, 0xb0, 0x30, 0x40, 0xa0, 0xf3, 0x3e, } want := transmute([]byte)string("The quick brown fox") got := decrypt_string(doc, Ref{7, 0}, cipher) testing.expectf(t, slice.equal(got, want), "string: %q", string(got)) // The stream filter is the same one here, so the same bytes decrypt the // same way through the stream path. got = decrypt_stream(doc, Ref{7, 0}, cipher) testing.expectf(t, slice.equal(got, want), "stream: %q", string(got)) // A different object number is a different key, so it must not decrypt. got = decrypt_string(doc, Ref{8, 0}, cipher) testing.expect(t, !slice.equal(got, want), "object 8 decrypted with object 7's key") } @(test) test_crypt_decrypt_aes128_string :: proc(t: ^testing.T) { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[4]) // R4, AESV2, EncryptMetadata false testing.expect(t, crypt_init(doc, nil) == .None) // 16-byte IV, then AES-128-CBC over PKCS#7-padded plaintext. cipher := []byte{ 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x47, 0x35, 0x63, 0xcc, 0xd0, 0x02, 0xf6, 0x35, 0xe9, 0xde, 0xb8, 0xb2, 0x5d, 0x64, 0x56, 0xe5, 0x58, 0x5a, 0xa1, 0xef, 0xc3, 0xfe, 0xa0, 0x89, 0xc1, 0x84, 0xd0, 0xa1, 0xae, 0x17, 0x2e, 0x6c, } want := transmute([]byte)string("Hello, encrypted PDF!") got := decrypt_string(doc, Ref{7, 0}, cipher) testing.expectf(t, slice.equal(got, want), "%q", string(got)) // 21 bytes of plaintext in 32 bytes of ciphertext: the padding is gone. testing.expectf(t, len(got) == 21, "length %d", len(got)) } @(test) test_crypt_never_decrypted :: proc(t: ^testing.T) { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) // Make /Encrypt indirect, the way real files write it, so crypt_init can // record which object must never be decrypted. doc.cache[9] = ed dict_set(doc.trailer, "Encrypt", Ref{9, 0}) testing.expect(t, crypt_init(doc, nil) == .None) testing.expect(t, doc.encrypt.ref == Ref{9, 0}, "encrypt ref not recorded") data := []byte{1, 2, 3, 4, 5, 6, 7, 8} // The /Encrypt dictionary's own strings are the input to the key. testing.expect(t, slice.equal(decrypt_string(doc, Ref{9, 0}, data), data), "/Encrypt dictionary was decrypted") // Ref{0,0} is an object that came out of an object stream and was already // decrypted with its container. testing.expect(t, slice.equal(decrypt_string(doc, Ref{0, 0}, data), data), "object-stream member was decrypted twice") // Empty input never allocates. testing.expect(t, len(decrypt_string(doc, Ref{7, 0}, nil)) == 0, "empty input") } // --------------------------------------------------------------------------- // Crypt filters // --------------------------------------------------------------------------- @(test) test_crypt_identity_filter :: proc(t: ^testing.T) { // /StmF /Identity with a real /StrF: strings are encrypted, streams are not. doc := crypt_t_new_doc(t) defer close(doc) c := crypt_t_cases[3] // R4 /V2 ed := crypt_t_build(doc, c) dict_set(ed, "StmF", Name("Identity")) testing.expect(t, crypt_init(doc, nil) == .None) testing.expect(t, doc.encrypt.stream_method == .Identity, "stream method") testing.expect(t, doc.encrypt.string_method == .Rc4, "string method") data := []byte{'n', 'o', 't', ' ', 'c', 'i', 'p', 'h', 'e', 'r'} testing.expect(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, data), data), "/Identity stream filter modified the data") testing.expect(t, !slice.equal(decrypt_string(doc, Ref{7, 0}, data), data), "/StrF should still have decrypted") // A /CFM of /None is the other spelling of the same thing. doc2 := crypt_t_new_doc(t) defer close(doc2) ed2 := crypt_t_build(doc2, c) dict_set(ed2, "CF", crypt_t_cf(doc2, "None", 0)) testing.expect(t, crypt_init(doc2, nil) == .None) testing.expect(t, doc2.encrypt.stream_method == .Identity, "/CFM /None stream") testing.expect(t, slice.equal(decrypt_stream(doc2, Ref{7, 0}, data), data), "/CFM /None modified the data") } @(test) test_crypt_embedded_files_only :: proc(t: ^testing.T) { // The "encrypt the attachments, leave the document readable" configuration: // /StmF and /StrF are /Identity and only /EFF names a filter. Page content // must come through untouched. doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[3]) dict_set(ed, "StmF", Name("Identity")) dict_set(ed, "StrF", Name("Identity")) dict_set(ed, "EFF", Name("StdCF")) testing.expect(t, crypt_init(doc, nil) == .None) testing.expect(t, doc.encrypt.stream_method == .Identity, "stream method") testing.expect(t, doc.encrypt.string_method == .Identity, "string method") testing.expect(t, doc.encrypt.embedded_method == .Rc4, "embedded method") content := transmute([]byte)string("BT /F1 12 Tf (hi) Tj ET") testing.expect(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, content), content), "page content was decrypted by an embedded-file-only filter") testing.expect(t, slice.equal(decrypt_string(doc, Ref{7, 0}, content), content), "a string was decrypted by an embedded-file-only filter") testing.expect(t, !slice.equal(decrypt_embedded_file(doc, Ref{7, 0}, content), content), "an embedded file was not decrypted") } @(test) test_crypt_filter_edge_cases :: proc(t: ^testing.T) { // /StmF naming a filter /CF does not define: degrade to Identity and record // the damage rather than refusing the file. doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[3]) dict_set(ed, "StmF", Name("NoSuchFilter")) testing.expect(t, crypt_init(doc, nil) == .None) testing.expect(t, doc.encrypt.stream_method == .Identity, "missing filter") testing.expect(t, doc.is_damaged, "missing crypt filter should be recorded") // An unrecognised /CFM is a scheme we would silently mangle every object // with, so it is an error. doc2 := crypt_t_new_doc(t) defer close(doc2) ed2 := crypt_t_build(doc2, crypt_t_cases[3]) dict_set(ed2, "CF", crypt_t_cf(doc2, "AESV9", 32)) testing.expect(t, crypt_init(doc2, nil) == .Unsupported_Encryption, "unknown /CFM") testing.expect(t, doc2.encrypt == nil, "state left behind") // A crypt filter /Length written in bits instead of bytes. 128 and 16 must // both give a 16-byte key, which is what makes the case in crypt_t_cases // (which writes 16) and this one agree. doc3 := crypt_t_new_doc(t) defer close(doc3) ed3 := crypt_t_build(doc3, crypt_t_cases[3]) dict_set(ed3, "CF", crypt_t_cf(doc3, "V2", 128)) testing.expect(t, crypt_init(doc3, nil) == .None) testing.expectf(t, len(doc3.encrypt.key) == 16, "key length %d", len(doc3.encrypt.key)) testing.expect(t, slice.equal(doc3.encrypt.key, crypt_t_cases[3].want_key), "key") } // --------------------------------------------------------------------------- // Revisions 5 and 6 (AES-256) // --------------------------------------------------------------------------- @(private = "file") crypt_t_filekey := [32]byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, } @(private = "file") R56_Case :: struct { name: string, revision: i64, o, oe: []byte, u, ue: []byte, perms: []byte, } @(private = "file") crypt_t_r56_cases := []R56_Case{ { name = "R5 (deprecated Adobe extension, bare SHA-256)", revision = 5, u = {0x65, 0x88, 0x6c, 0x9f, 0x45, 0x07, 0xdf, 0x2b, 0x52, 0x6c, 0x2b, 0xcb, 0x9b, 0x33, 0xcd, 0x5d, 0xba, 0xb3, 0x88, 0xf3, 0x14, 0x3e, 0x03, 0x9e, 0x01, 0x20, 0x83, 0x49, 0x29, 0xc7, 0x04, 0xca, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8}, ue = {0x69, 0xc8, 0xe0, 0xaf, 0x10, 0x2e, 0x3f, 0xb1, 0x89, 0x9f, 0x4b, 0x93, 0x06, 0x25, 0xb0, 0xb3, 0xcc, 0x90, 0x20, 0xf6, 0x3c, 0x77, 0x4a, 0x21, 0x3b, 0x8c, 0xb4, 0xe3, 0x0f, 0x0e, 0xbe, 0xcd}, o = {0xf2, 0x1b, 0xbf, 0xb1, 0x73, 0xd3, 0xa7, 0xfb, 0x7f, 0xad, 0xdd, 0x08, 0x6f, 0x6f, 0x05, 0xe4, 0xc9, 0xb8, 0xff, 0x3a, 0x51, 0xbe, 0x6f, 0x1c, 0x81, 0xf2, 0x1b, 0x94, 0x0f, 0xa5, 0xac, 0x9b, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8}, oe = {0x67, 0x0b, 0x2b, 0x88, 0xaf, 0x0d, 0x7e, 0xd9, 0x3c, 0x14, 0x33, 0x66, 0xb1, 0x1e, 0x7e, 0xad, 0xf4, 0xf7, 0x7b, 0xaa, 0x68, 0x2f, 0x41, 0x07, 0x87, 0xb7, 0xfc, 0x7e, 0x34, 0x71, 0xac, 0x87}, perms = {0xb4, 0x5b, 0xaa, 0xc2, 0x1d, 0xad, 0xf0, 0x36, 0x8e, 0xe2, 0x6f, 0xd5, 0x91, 0x2c, 0xae, 0xa8}, }, { name = "R6 (ISO 32000-2, Algorithm 2.B)", revision = 6, u = {0xed, 0x88, 0x42, 0x72, 0x92, 0x52, 0x99, 0x61, 0xae, 0xd2, 0x36, 0xc4, 0x6f, 0xa5, 0x10, 0xb2, 0x69, 0xbe, 0xdf, 0xf2, 0xbe, 0xb2, 0xf0, 0x19, 0x6f, 0xce, 0x1e, 0x7a, 0xe3, 0x50, 0x8a, 0x15, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8}, ue = {0x40, 0x68, 0xff, 0xf1, 0xbf, 0xab, 0xf4, 0x98, 0x48, 0xd9, 0x78, 0x54, 0x92, 0xdd, 0x59, 0xda, 0x97, 0x4d, 0x44, 0x19, 0x74, 0x9f, 0x58, 0x1f, 0xff, 0x12, 0x97, 0xb9, 0x73, 0x9f, 0x1e, 0x9a}, o = {0xb9, 0xa6, 0x58, 0x86, 0x99, 0x3b, 0xd1, 0x65, 0xbe, 0x49, 0x92, 0x6b, 0x72, 0x8c, 0x5c, 0x4f, 0x56, 0xba, 0xf2, 0xa0, 0xee, 0x47, 0x82, 0x33, 0x25, 0xc6, 0xee, 0x8f, 0xfa, 0xd6, 0x05, 0xdb, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8}, oe = {0xb4, 0x80, 0x18, 0xe0, 0xe3, 0x46, 0xc8, 0x2d, 0x90, 0xb0, 0xed, 0xcf, 0xcc, 0x1c, 0x31, 0x38, 0xea, 0x8a, 0x47, 0x6b, 0x34, 0x02, 0xf8, 0x0e, 0x42, 0x4e, 0x89, 0x40, 0xc2, 0x73, 0x5d, 0x4b}, perms = {0xb4, 0x5b, 0xaa, 0xc2, 0x1d, 0xad, 0xf0, 0x36, 0x8e, 0xe2, 0x6f, 0xd5, 0x91, 0x2c, 0xae, 0xa8}, }, } @(private = "file") crypt_t_build_r56 :: proc(doc: ^Document, c: R56_Case) -> ^Dict { ed := new_dict(doc.allocator, 12) dict_set(ed, "Filter", Name("Standard")) dict_set(ed, "V", i64(5)) dict_set(ed, "R", c.revision) dict_set(ed, "Length", i64(256)) dict_set(ed, "P", i64(-3904)) dict_set(ed, "O", crypt_t_str(doc, c.o)) dict_set(ed, "U", crypt_t_str(doc, c.u)) dict_set(ed, "OE", crypt_t_str(doc, c.oe)) dict_set(ed, "UE", crypt_t_str(doc, c.ue)) dict_set(ed, "Perms", crypt_t_str(doc, c.perms)) dict_set(ed, "CF", crypt_t_cf(doc, "AESV3", 32)) dict_set(ed, "StmF", Name("StdCF")) dict_set(ed, "StrF", Name("StdCF")) // /ID takes no part in the R5/R6 key, but real files still carry one. crypt_t_id(doc, crypt_t_id0[:]) dict_set(doc.trailer, "Encrypt", ed) return ed } @(test) test_crypt_aes256 :: proc(t: ^testing.T) { for c in crypt_t_r56_cases { // User password (empty). doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build_r56(doc, c) err := crypt_init(doc, nil) testing.expectf(t, err == .None, "%s: crypt_init = %v", c.name, err) if doc.encrypt == nil { testing.expectf(t, false, "%s: no encryption state", c.name) continue } testing.expectf(t, slice.equal(doc.encrypt.key, crypt_t_filekey[:]), "%s: user key = %x", c.name, doc.encrypt.key) testing.expectf(t, doc.encrypt.authenticated_as == .User, "%s: %v", c.name, doc.encrypt.authenticated_as) testing.expectf(t, doc.encrypt.stream_method == .Aes_256, "%s: method", c.name) // /Perms decrypts to the same /P, so nothing should have been repaired. testing.expectf(t, !doc.is_damaged, "%s: /Perms check failed", c.name) // Owner password: a different wrapped key, the same file key. doc2 := crypt_t_new_doc(t) defer close(doc2) crypt_t_build_r56(doc2, c) err = crypt_init(doc2, transmute([]byte)string("owner")) testing.expectf(t, err == .None, "%s owner: crypt_init = %v", c.name, err) if doc2.encrypt == nil { continue } testing.expectf(t, slice.equal(doc2.encrypt.key, crypt_t_filekey[:]), "%s: owner key = %x", c.name, doc2.encrypt.key) testing.expectf(t, doc2.encrypt.authenticated_as == .Owner, "%s owner: %v", c.name, doc2.encrypt.authenticated_as) // Wrong password. doc3 := crypt_t_new_doc(t) defer close(doc3) crypt_t_build_r56(doc3, c) testing.expectf(t, crypt_init(doc3, transmute([]byte)string("nope")) == .Wrong_Password, "%s: wrong password accepted", c.name) } } @(test) test_crypt_aes256_stream :: proc(t: ^testing.T) { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build_r56(doc, crypt_t_r56_cases[1]) // R6 testing.expect(t, crypt_init(doc, nil) == .None) // Encrypted outside this library with the file key directly: AES-256 does // not derive a per-object key, so the object number is irrelevant. cipher := []byte{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x3a, 0xee, 0xe0, 0x20, 0x26, 0xc8, 0x6a, 0xf8, 0xe6, 0x2d, 0x73, 0xc1, 0xcc, 0xe3, 0xed, 0x73, 0x41, 0x0a, 0x22, 0x1b, 0x82, 0x91, 0x46, 0x04, 0x97, 0x5d, 0x3a, 0xaa, 0x12, 0x75, 0x20, 0x57, } want := transmute([]byte)string("R6 stream payload") testing.expectf(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, cipher), want), "object 7: %q", string(decrypt_stream(doc, Ref{7, 0}, cipher))) testing.expectf(t, slice.equal(decrypt_stream(doc, Ref{4001, 9}, cipher), want), "object 4001: %q", string(decrypt_stream(doc, Ref{4001, 9}, cipher))) } @(test) test_crypt_r6_hash :: proc(t: ^testing.T) { Case :: struct { revision: int, password: string, salt: []byte, udata: []byte, want: []byte, } cases := []Case{ { // Algorithm 2.B with an empty password: the loop still runs. 6, "", {0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8}, nil, {0xed, 0x88, 0x42, 0x72, 0x92, 0x52, 0x99, 0x61, 0xae, 0xd2, 0x36, 0xc4, 0x6f, 0xa5, 0x10, 0xb2, 0x69, 0xbe, 0xdf, 0xf2, 0xbe, 0xb2, 0xf0, 0x19, 0x6f, 0xce, 0x1e, 0x7a, 0xe3, 0x50, 0x8a, 0x15}, }, { 6, "password", {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, nil, {0xbb, 0xa2, 0xc9, 0xd8, 0xe7, 0x68, 0x12, 0x06, 0x75, 0x1a, 0x9b, 0xb3, 0x9f, 0x2c, 0xf5, 0xeb, 0x40, 0x8f, 0x48, 0xfc, 0x2d, 0x3a, 0x44, 0xd1, 0x53, 0xbc, 0x2f, 0xff, 0xaf, 0x9c, 0xc8, 0x7c}, }, { // R5 is the same input hashed once. 5, "password", {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, nil, {0xab, 0x47, 0xa5, 0x51, 0xc8, 0x47, 0x88, 0x48, 0x19, 0x01, 0x9c, 0x30, 0xe7, 0xb5, 0x0c, 0xb3, 0xa2, 0x6d, 0xf8, 0xbe, 0x39, 0xfb, 0xbf, 0x39, 0x43, 0xc6, 0x1c, 0x55, 0x47, 0xfc, 0x55, 0xf7}, }, } // The owner check feeds the whole 48-byte /U value in as udata. udata: [48]byte for i in 0 ..< 48 { udata[i] = byte(i) } owner_case := Case{ 6, "owner", {0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8}, udata[:], {0xf9, 0x27, 0xe1, 0xbc, 0xd5, 0x68, 0x60, 0x51, 0xb0, 0x6e, 0xd7, 0x55, 0x55, 0x73, 0x65, 0xeb, 0xf0, 0x0c, 0x92, 0x2d, 0x88, 0x89, 0xb4, 0x67, 0xed, 0x5c, 0xff, 0xa0, 0x89, 0xb8, 0x8d, 0xdd}, } out: [32]byte for c in cases { crypt_hash_r56(c.revision, transmute([]byte)c.password, c.salt, c.udata, out[:]) testing.expectf(t, slice.equal(out[:], c.want), "R%d %q: %x", c.revision, c.password, out) } crypt_hash_r56(owner_case.revision, transmute([]byte)owner_case.password, owner_case.salt, owner_case.udata, out[:]) testing.expectf(t, slice.equal(out[:], owner_case.want), "owner hash: %x", out) } @(test) test_crypt_r6_long_password :: proc(t: ^testing.T) { // A password over 127 bytes is truncated, so 127 and 200 bytes of the same // character must hash identically. This is also the case that would blow // the fixed buffers in Algorithm 2.B if the truncation were missing. long: [200]byte for i in 0 ..< len(long) { long[i] = 'x' } a, b: [32]byte crypt_hash_r56(6, long[:127], {1, 2, 3, 4, 5, 6, 7, 8}, nil, a[:]) crypt_hash_r56(6, long[:], {1, 2, 3, 4, 5, 6, 7, 8}, nil, b[:]) testing.expect(t, slice.equal(a[:], b[:]), "a >127-byte password was not truncated") crypt_hash_r56(6, long[:126], {1, 2, 3, 4, 5, 6, 7, 8}, nil, b[:]) testing.expect(t, !slice.equal(a[:], b[:]), "126 and 127 bytes hashed the same") } // --------------------------------------------------------------------------- // Malformed input // --------------------------------------------------------------------------- @(test) test_crypt_aes_short_and_garbage :: proc(t: ^testing.T) { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[4]) // R4 /AESV2 testing.expect(t, crypt_init(doc, nil) == .None) junk: [64]byte for i in 0 ..< len(junk) { junk[i] = byte(i * 37 + 11) } // Every length up to and including a full block plus a partial one. None of // these can be a valid payload, and none of them may crash. for n in 0 ..= 47 { got := decrypt_string(doc, Ref{7, 0}, junk[:n]) if n == 0 { testing.expect(t, len(got) == 0, "empty in, empty out") continue } if n < 32 { testing.expectf(t, len(got) == 0, "length %d gave %d bytes", n, len(got)) } else { // 32..47 decrypts one or two blocks of noise; the padding check // almost always rejects it, and when it does not the result is // still bounded by the input. testing.expectf(t, len(got) < n, "length %d gave %d bytes", n, len(got)) } } // Ciphertext that decrypts cleanly but to bytes 00..0f, whose last byte is // 0x0f while the preceding bytes are not: invalid PKCS#7. bad_pad := []byte{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x4e, 0x89, 0x37, 0x98, 0x56, 0xc5, 0x2e, 0x44, 0x57, 0x60, 0x84, 0x6c, 0xac, 0x7a, 0xba, 0x62, } doc6 := crypt_t_new_doc(t) defer close(doc6) crypt_t_build_r56(doc6, crypt_t_r56_cases[1]) testing.expect(t, crypt_init(doc6, nil) == .None) testing.expect(t, len(decrypt_stream(doc6, Ref{7, 0}, bad_pad)) == 0, "invalid PKCS#7 padding should decrypt to nothing") testing.expect(t, doc6.is_damaged, "a bad payload should be recorded as a repair") } @(test) test_crypt_malformed_dictionaries :: proc(t: ^testing.T) { // No /Encrypt at all. { doc := crypt_t_new_doc(t) defer close(doc) testing.expect(t, crypt_init(doc, nil) == .None, "unencrypted") testing.expect(t, doc.encrypt == nil, "unencrypted document has encryption state") data := []byte{1, 2, 3} testing.expect(t, slice.equal(decrypt_string(doc, Ref{1, 0}, data), data), "passthrough") testing.expect(t, slice.equal(decrypt_stream(doc, Ref{1, 0}, data), data), "passthrough") testing.expect(t, !document_is_encrypted(doc), "document_is_encrypted") } // /Encrypt pointing at an object that is not there. { doc := crypt_t_new_doc(t) defer close(doc) dict_set(doc.trailer, "Encrypt", Ref{99, 0}) testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "dangling /Encrypt") } // /Encrypt that is a number. { doc := crypt_t_new_doc(t) defer close(doc) dict_set(doc.trailer, "Encrypt", i64(42)) testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "/Encrypt is a number") } // A security handler we do not have. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) dict_set(ed, "Filter", Name("FOPN_foweb")) testing.expect(t, crypt_init(doc, nil) == .Unsupported_Encryption, "third-party handler") } // A version from the future. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) dict_set(ed, "V", i64(9)) testing.expect(t, crypt_init(doc, nil) == .Unsupported_Encryption, "/V 9") } // /O and /U truncated to nothing: no crash, no match. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) dict_set(ed, "O", crypt_t_str(doc, {1, 2, 3})) dict_set(ed, "U", crypt_t_str(doc, {})) testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "truncated /O and /U") } // /O and /U absent entirely. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) delete_key(ed, "O") delete_key(ed, "U") testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "missing /O and /U") } // R6 with /UE missing: the password checks out but there is no key to // unwrap, which is a broken dictionary rather than a wrong password. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build_r56(doc, crypt_t_r56_cases[1]) delete_key(ed, "UE") testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "R6 without /UE") } // R5/R6 /U truncated below its 48 bytes. { doc := crypt_t_new_doc(t) defer close(doc) c := crypt_t_r56_cases[1] ed := crypt_t_build_r56(doc, c) dict_set(ed, "U", crypt_t_str(doc, c.u[:20])) testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "truncated R6 /U") } // A /Length that is nonsense clamps to the widest legal key instead of // sizing an allocation from the file. 1000000 bits clamps to 128, which is // what this document actually uses, so it still opens. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) dict_set(ed, "Length", i64(1_000_000)) testing.expect(t, crypt_init(doc, nil) == .None, "absurd /Length") testing.expectf(t, len(doc.encrypt.key) == 16, "key length %d", len(doc.encrypt.key)) } // A negative /Length falls back to the 40-bit default, which is not this // document's key length, so it fails the password check rather than // indexing a negative-length slice. { doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) dict_set(ed, "Length", i64(-99)) testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "negative /Length") } // Missing /ID. The key derivation folds in an empty string, which is what // other readers do, so this must not crash. { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[1]) delete_key(doc.trailer, "ID") testing.expect(t, crypt_init(doc, nil) == .Wrong_Password, "missing /ID") } } @(test) test_crypt_revision_repair :: proc(t: ^testing.T) { // R6 with /V written as 4: the revision decides the algorithms, so trust it // and note the damage. Files from at least one producer do this. doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build_r56(doc, crypt_t_r56_cases[1]) dict_set(ed, "V", i64(4)) testing.expect(t, crypt_init(doc, nil) == .None, "R6 with /V 4") testing.expect(t, doc.encrypt != nil) testing.expect(t, slice.equal(doc.encrypt.key, crypt_t_filekey[:]), "key") testing.expect(t, doc.is_damaged, "the /V mismatch should be recorded") // /V 3 is the unpublished Adobe variant; it decrypts as /V 2. doc2 := crypt_t_new_doc(t) defer close(doc2) ed2 := crypt_t_build(doc2, crypt_t_cases[1]) dict_set(ed2, "V", i64(3)) testing.expect(t, crypt_init(doc2, nil) == .None, "/V 3") testing.expect(t, slice.equal(doc2.encrypt.key, crypt_t_cases[1].want_key), "/V 3 key") // A missing /R is inferred from /V. doc3 := crypt_t_new_doc(t) defer close(doc3) ed3 := crypt_t_build(doc3, crypt_t_cases[1]) delete_key(ed3, "R") testing.expect(t, crypt_init(doc3, nil) == .None, "missing /R") testing.expect(t, doc3.encrypt.revision == 3, "inferred revision") } // --------------------------------------------------------------------------- // Reporting // --------------------------------------------------------------------------- @(test) test_crypt_permissions :: proc(t: ^testing.T) { // /P = -1340 is 0xFFFFFAC4: printing, accessibility extraction and // high-resolution printing allowed, everything else denied. doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[3]) testing.expect(t, crypt_init(doc, nil) == .None) want := Permissions{.Print, .Extract, .Print_High_Res} got := encryption_permissions(doc) testing.expectf(t, got == want, "user permissions %v, want %v", got, want) testing.expectf(t, doc.encrypt.permission_bits == -1340, "/P read as %d", doc.encrypt.permission_bits) // The owner is not bound by the restrictions they set. doc2 := crypt_t_new_doc(t) defer close(doc2) crypt_t_build(doc2, crypt_t_cases[3]) testing.expect(t, crypt_init(doc2, transmute([]byte)string("owner")) == .None) all := Permissions{.Print, .Modify, .Copy, .Annotate, .Fill_Forms, .Extract, .Assemble, .Print_High_Res} testing.expectf(t, encryption_permissions(doc2) == all, "owner permissions %v", encryption_permissions(doc2)) // An unencrypted document allows everything. doc3 := crypt_t_new_doc(t) defer close(doc3) testing.expect(t, encryption_permissions(doc3) == all, "unencrypted permissions") testing.expect(t, encryption_permissions(nil) == all, "nil document") // R2 only ever defined four of the bits. doc4 := crypt_t_new_doc(t) defer close(doc4) crypt_t_build(doc4, crypt_t_cases[0]) testing.expect(t, crypt_init(doc4, nil) == .None) r2 := encryption_permissions(doc4) testing.expectf(t, r2 == {.Print, .Modify, .Copy, .Annotate}, "R2 permissions %v", r2) // /P written as the equivalent unsigned value, which some producers do. doc5 := crypt_t_new_doc(t) defer close(doc5) ed := crypt_t_build(doc5, crypt_t_cases[3]) dict_set(ed, "P", i64(4294965956)) // 0xFFFFFAC4 testing.expect(t, crypt_init(doc5, nil) == .None, "unsigned /P") testing.expectf(t, doc5.encrypt.permission_bits == -1340, "unsigned /P read as %d", doc5.encrypt.permission_bits) } @(test) test_crypt_method_name :: proc(t: ^testing.T) { Case :: struct { index: int, want: string, } cases := []Case{ {0, "RC4 40-bit"}, {1, "RC4 128-bit"}, {2, "RC4 40-bit"}, {3, "RC4 128-bit"}, {4, "AES-128"}, } for c in cases { doc := crypt_t_new_doc(t) defer close(doc) kd := crypt_t_cases[c.index] crypt_t_build(doc, kd) testing.expect(t, crypt_init(doc, transmute([]byte)kd.user_password) == .None) testing.expectf(t, encryption_method_name(doc) == c.want, "%s: %q, want %q", kd.name, encryption_method_name(doc), c.want) testing.expect(t, document_is_encrypted(doc), "document_is_encrypted") } doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build_r56(doc, crypt_t_r56_cases[1]) testing.expect(t, crypt_init(doc, nil) == .None) testing.expectf(t, encryption_method_name(doc) == "AES-256", "%q", encryption_method_name(doc)) testing.expect(t, encryption_method_name(nil) == "none", "nil document") doc2 := crypt_t_new_doc(t) defer close(doc2) testing.expect(t, encryption_method_name(doc2) == "none", "unencrypted") } @(test) test_crypt_destroy_wipes_the_key :: proc(t: ^testing.T) { doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[1]) testing.expect(t, crypt_init(doc, nil) == .None) enc := doc.encrypt crypt_destroy(doc) testing.expect(t, doc.encrypt == nil, "handler still attached") zero := true for b in enc.key { if b != 0 { zero = false } } testing.expect(t, zero, "the file key was left in memory") // Idempotent: `close` calls it again after an explicit teardown. crypt_destroy(doc) crypt_destroy(nil) } @(test) test_crypt_v5_without_crypt_filters :: proc(t: ^testing.T) { // /CF is required for V5, and a file that omits it can only have meant // AES-256. Left as /Identity the document would render as noise. doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build_r56(doc, crypt_t_r56_cases[1]) delete_key(ed, "CF") delete_key(ed, "StmF") delete_key(ed, "StrF") testing.expect(t, crypt_init(doc, nil) == .None, "V5 without /CF") testing.expect(t, doc.encrypt.stream_method == .Aes_256, "stream method") testing.expect(t, doc.encrypt.string_method == .Aes_256, "string method") testing.expect(t, doc.is_damaged, "the missing /CF should be recorded") cipher := []byte{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x3a, 0xee, 0xe0, 0x20, 0x26, 0xc8, 0x6a, 0xf8, 0xe6, 0x2d, 0x73, 0xc1, 0xcc, 0xe3, 0xed, 0x73, 0x41, 0x0a, 0x22, 0x1b, 0x82, 0x91, 0x46, 0x04, 0x97, 0x5d, 0x3a, 0xaa, 0x12, 0x75, 0x20, 0x57, } testing.expect(t, slice.equal(decrypt_stream(doc, Ref{7, 0}, cipher), transmute([]byte)string("R6 stream payload")), "decrypt") // An explicit /Identity is a deliberate configuration and is respected. doc2 := crypt_t_new_doc(t) defer close(doc2) ed2 := crypt_t_build_r56(doc2, crypt_t_r56_cases[1]) dict_set(ed2, "StmF", Name("Identity")) testing.expect(t, crypt_init(doc2, nil) == .None) testing.expect(t, doc2.encrypt.stream_method == .Identity, "explicit /Identity overridden") } @(test) test_crypt_r2_ignores_length :: proc(t: ^testing.T) { // R2 is 40-bit whatever /Length claims. A file that writes 128 there still // has to be opened with a 5-byte key. doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[0]) dict_set(ed, "Length", i64(128)) testing.expect(t, crypt_init(doc, nil) == .None, "R2 with /Length 128") testing.expectf(t, len(doc.encrypt.key) == 5, "key length %d", len(doc.encrypt.key)) testing.expect(t, slice.equal(doc.encrypt.key, crypt_t_cases[0].want_key), "key") } @(test) test_crypt_long_password :: proc(t: ^testing.T) { // Algorithm 2 truncates to 32 bytes, so a 32-byte password and a 500-byte // one that starts with it are the same password. The 500-byte form is also // what would overrun a fixed padding buffer. long: [500]byte for i in 0 ..< len(long) { long[i] = byte('a' + i % 26) } a, b: [32]byte crypt_pad_password(long[:], a[:]) crypt_pad_password(long[:32], b[:]) testing.expect(t, slice.equal(a[:], b[:]), "password not truncated to 32 bytes") // A short password is extended from the padding string, and an empty one // becomes the padding string entire. crypt_pad_password(nil, a[:]) testing.expect(t, slice.equal(a[:], CRYPT_PAD[:]), "empty password padding") crypt_pad_password(transmute([]byte)string("ab"), a[:]) testing.expect(t, a[0] == 'a' && a[1] == 'b' && a[2] == CRYPT_PAD[0], "short password padding") // And the whole path survives one. doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[1]) testing.expect(t, crypt_init(doc, long[:]) == .Wrong_Password, "500-byte password") doc2 := crypt_t_new_doc(t) defer close(doc2) crypt_t_build_r56(doc2, crypt_t_r56_cases[1]) testing.expect(t, crypt_init(doc2, long[:]) == .Wrong_Password, "500-byte R6 password") } @(test) test_crypt_cyclic_encrypt_reference :: proc(t: ^testing.T) { // /Encrypt pointing at an object that is itself a reference to /Encrypt. // resolve gives up after MAX_REF_CHAIN hops; crypt_init must not spin. doc := crypt_t_new_doc(t) defer close(doc) doc.cache[9] = Ref{9, 0} dict_set(doc.trailer, "Encrypt", Ref{9, 0}) testing.expect(t, crypt_init(doc, nil) == .Bad_Encrypt_Dict, "self-referential /Encrypt") // A two-object cycle. doc2 := crypt_t_new_doc(t) defer close(doc2) doc2.cache[9] = Ref{10, 0} doc2.cache[10] = Ref{9, 0} dict_set(doc2.trailer, "Encrypt", Ref{9, 0}) testing.expect(t, crypt_init(doc2, nil) == .Bad_Encrypt_Dict, "cyclic /Encrypt") } @(test) test_crypt_dictionary_sweep :: proc(t: ^testing.T) { // Every combination of the structural fields a file controls, over a // dictionary whose /O and /U are truncated to a range of lengths. Nothing // here is expected to decrypt; the assertions are the invariants that must // hold whatever the file says. versions := []i64{0, 1, 2, 4, 5, 9, -1, 1 << 40} revisions := []i64{0, 2, 3, 4, 5, 6, -3} lengths := []i64{0, 5, 40, 41, 128, 256, -8, 1 << 33} cfms := []string{"", "None", "V2", "AESV2", "AESV3", "Identity"} cuts := []int{0, 1, 15, 16, 31, 32, 40, 47, 48} base := crypt_t_cases[3] n := 0 for v in versions { for r in revisions { for l in lengths { for cfm in cfms { cut := cuts[n % len(cuts)] n += 1 doc := crypt_t_new_doc(t) defer close(doc) c := base c.version, c.revision, c.length, c.cfm = v, r, l, cfm c.o = base.o[:min(cut, len(base.o))] c.u = base.u[:min(cut, len(base.u))] ed := crypt_t_build(doc, c) // R5/R6 fields, so the V5 branches are exercised too. dict_set(ed, "OE", crypt_t_str(doc, crypt_t_r56_cases[1].oe[:min(cut, 32)])) dict_set(ed, "UE", crypt_t_str(doc, crypt_t_r56_cases[1].ue[:min(cut, 32)])) dict_set(ed, "Perms", crypt_t_str(doc, crypt_t_r56_cases[1].perms[:min(cut, 16)])) err := crypt_init(doc, transmute([]byte)string("pw")) #partial switch err { case .None, .Wrong_Password, .Unsupported_Encryption, .Bad_Encrypt_Dict: // The only outcomes crypt_init is allowed to have. case: testing.expectf(t, false, "V=%d R=%d L=%d CFM=%q: %v", v, r, l, cfm, err) } if err == .None { testing.expectf(t, doc.encrypt != nil, "V=%d R=%d L=%d CFM=%q: success without state", v, r, l, cfm) testing.expectf(t, len(doc.encrypt.key) >= 5 && len(doc.encrypt.key) <= 32, "V=%d R=%d L=%d CFM=%q: key length %d", v, r, l, cfm, len(doc.encrypt.key)) // Whatever it decided, decryption must stay in bounds. payload := crypt_t_id0[:] out := decrypt_string(doc, Ref{7, 0}, payload) testing.expectf(t, len(out) <= len(payload), "V=%d R=%d L=%d CFM=%q: %d bytes out of %d", v, r, l, cfm, len(out), len(payload)) } else { testing.expectf(t, doc.encrypt == nil, "V=%d R=%d L=%d CFM=%q: state left after %v", v, r, l, cfm, err) } } } } } } @(test) test_crypt_payload_sweep :: proc(t: ^testing.T) { // Arbitrary bytes fed to every filter at every length up to a few blocks. // None of it is valid ciphertext; none of it may crash, and none of it may // produce more bytes than it was given. payload: [80]byte for i in 0 ..< len(payload) { payload[i] = byte(i * 91 + 7) } indices := []int{1, 3, 4} // RC4 128-bit, RC4 via crypt filter, AES-128 for idx in indices { doc := crypt_t_new_doc(t) defer close(doc) kd := crypt_t_cases[idx] crypt_t_build(doc, kd) testing.expect(t, crypt_init(doc, transmute([]byte)kd.user_password) == .None) for n in 0 ..= len(payload) { for ref in ([]Ref{{1, 0}, {7, 0}, {0xFFFFFF, 0xFFFF}, {0, 0}}) { s := decrypt_string(doc, ref, payload[:n]) testing.expectf(t, len(s) <= n, "string %d bytes -> %d", n, len(s)) d := decrypt_stream(doc, ref, payload[:n]) testing.expectf(t, len(d) <= n, "stream %d bytes -> %d", n, len(d)) e := decrypt_embedded_file(doc, ref, payload[:n]) testing.expectf(t, len(e) <= n, "embedded %d bytes -> %d", n, len(e)) } } } // The same for AES-256, which takes a different path through the key code. doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build_r56(doc, crypt_t_r56_cases[1]) testing.expect(t, crypt_init(doc, nil) == .None) for n in 0 ..= len(payload) { got := decrypt_stream(doc, Ref{7, 0}, payload[:n]) testing.expectf(t, len(got) <= n, "aes-256 %d bytes -> %d", n, len(got)) } } @(test) test_crypt_u_check_length :: proc(t: ^testing.T) { // Algorithm 6 compares all 32 bytes of /U for R2, but only the first 16 for // R3 and later -- the rest is padding the producer chose freely, and files // do write different things there. scratch: [32]byte copy(scratch[:], crypt_t_cases[1].u) for i in 16 ..< 32 { scratch[i] = 0xA5 } doc := crypt_t_new_doc(t) defer close(doc) ed := crypt_t_build(doc, crypt_t_cases[1]) dict_set(ed, "U", crypt_t_str(doc, scratch[:])) testing.expect(t, crypt_init(doc, nil) == .None, "R3 should ignore the last 16 bytes of /U") copy(scratch[:], crypt_t_cases[0].u) scratch[31] ~= 0xFF doc2 := crypt_t_new_doc(t) defer close(doc2) ed2 := crypt_t_build(doc2, crypt_t_cases[0]) dict_set(ed2, "U", crypt_t_str(doc2, scratch[:])) testing.expect(t, crypt_init(doc2, nil) == .Wrong_Password, "R2 should compare all 32 bytes of /U") // A single flipped bit in the checked half fails at every revision. copy(scratch[:], crypt_t_cases[1].u) scratch[15] ~= 0x01 doc3 := crypt_t_new_doc(t) defer close(doc3) ed3 := crypt_t_build(doc3, crypt_t_cases[1]) dict_set(ed3, "U", crypt_t_str(doc3, scratch[:])) testing.expect(t, crypt_init(doc3, nil) == .Wrong_Password, "flipped bit in /U") } @(test) test_crypt_id_survives_init :: proc(t: ^testing.T) { // The /ID strings feed the key derivation and are never themselves // encrypted, so they must read back exactly as the file wrote them after // the handler has been set up. doc := crypt_t_new_doc(t) defer close(doc) crypt_t_build(doc, crypt_t_cases[1]) testing.expect(t, crypt_init(doc, nil) == .None) id, ok := to_bytes(array_get(doc, dict_array(doc, doc.trailer, "ID"), 0)) testing.expect(t, ok, "/ID is not a string") testing.expectf(t, slice.equal(id, crypt_t_id0[:]), "/ID = %x", id) // And the /O and /U in the /Encrypt dictionary likewise. ed := dict_dict(doc, doc.trailer, "Encrypt") testing.expect(t, slice.equal(dict_bytes(doc, ed, "O"), crypt_t_cases[1].o), "/O") testing.expect(t, slice.equal(dict_bytes(doc, ed, "U"), crypt_t_cases[1].u), "/U") } // --------------------------------------------------------------------------- // The cross-reference stream is never encrypted // --------------------------------------------------------------------------- // crypt_t_hex spells bytes as the body of a PDF hex string. @(private = "file") crypt_t_hex :: proc(b: []byte) -> string { out := strings.builder_make(0, len(b) * 2, context.temp_allocator) for v in b { fmt.sbprintf(&out, "%02x", v) } return strings.to_string(out) } // crypt_t_encrypted_xref_stream_file writes an encrypted document whose whole // structure is a /Type /XRef stream: catalog, page tree, page, the /Encrypt // dictionary as object 4, and the cross-reference stream as object 5. @(private = "file") crypt_t_encrypted_xref_stream_file :: proc(c: Kd_Case, rows_out: ^[]byte) -> Pdf_Builder { pb := pb_make("%PDF-1.5\n") pb_skeleton(&pb) pb_obj(&pb, 4, fmt.tprintf( "<< /Filter /Standard /V %d /R %d /Length %d /P %d /O <%s> /U <%s> >>", c.version, c.revision, c.length, c.permissions, crypt_t_hex(c.o[:]), crypt_t_hex(c.u[:]))) at := pb_here(&pb) rows := [6]Xref_Row{ {0, 0, 65535}, {1, u64(pb.offs[1]), 0}, {1, u64(pb.offs[2]), 0}, {1, u64(pb.offs[3]), 0}, {1, u64(pb.offs[4]), 0}, {1, u64(at), 0}, } rows_out^ = xref_rows_encode(rows[:], 1, 4, 2) pb_stream_obj(&pb, 5, fmt.tprintf( "/Type /XRef /Size 6 /W [1 4 2] /Root 1 0 R /Encrypt 4 0 R /ID [<%s> <%s>]", crypt_t_hex(crypt_t_id0[:]), crypt_t_hex(crypt_t_id0[:])), rows_out^) pb_startxref(&pb, at) return pb } @(test) test_crypt_xref_stream_is_never_decrypted :: proc(t: ^testing.T) { // ISO 32000-2, 7.5.8.2: "the cross-reference stream shall not be // encrypted". `xref_load` gets this right for free -- it runs before // `crypt_init`, so `doc.encrypt` is still nil while the table is read -- // but the same object read again through the public API arrives with the // key in place. Decrypting plaintext hands back noise, which `stream_data` // then caches on the Stream; `find_catalog`'s fallback scan walks every // object this way. rows: []byte pb := crypt_t_encrypted_xref_stream_file(crypt_t_cases[1], &rows) defer pb_destroy(&pb) defer delete(rows) doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) defer close(doc) testing.expectf(t, err == .None, "open: %s", error_string(err)) if doc == nil { return } testing.expect(t, doc.encrypt != nil, "the document did not come up encrypted") s := get_stream(doc, Ref{5, 0}) if !testing.expect(t, s != nil, "object 5 is not a stream") { return } data, derr := stream_data(doc, s) testing.expectf(t, derr == .None, "stream_data: %s", error_string(derr)) testing.expectf(t, slice.equal(data, rows), "the xref stream was decrypted on re-read: got %x, want %x", data, rows) // The other objects in the same file are encrypted, so the exemption has // to be the /Type and not "this document is in the clear". content := transmute([]byte)string("payload") testing.expect(t, !slice.equal(decrypt_stream(doc, Ref{5, 0}, content), content), "object 5's key is an identity transform, so this proves nothing") }