1538 lines
67 KiB
Odin
1538 lines
67 KiB
Odin
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")
|
|
}
|