Files

1538 lines
67 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
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")
}