diff --git a/README.md b/README.md index 4d3c0fb..ed2ffe6 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,13 @@ the **document structure**: - **Documents** — `Open`, object resolution with cycle and recursion guards, the trailer, the catalogue, and the page tree with the four attributes a page inherits from its ancestors. -- **Encryption** — the standard security handler in every revision: RC4 at 40 - and 128 bits, AESV2, and the AES-256 of `/R 5` and `/R 6`, with crypt - filters, `/Identity`, and `/EncryptMetadata`. A password is tried as the - user password and as the owner password, and `Open` uses the empty one, so - a file protected only against editing opens with no password at all. -- **Content streams** — a tokeniser that yields operators with their operands +- **Encryption** — the standard security handler in every revision, both ways: + reading RC4 at 40 and 128 bits, AESV2, and the AES-256 of `/R 5` and `/R 6`, + with crypt filters, `/Identity` and `/EncryptMetadata`; and writing AES-256 + or AES-128, with permissions, a user password and an owner password. + A password is tried as the user password and as the owner password, and + `Open` uses the empty one, so a file protected only against editing opens + with no password at all. and steps over rubbish rather than losing the operations around it, with inline images read whole. Where an inline image ends is the one genuinely ambiguous thing in a content stream, since its data may spell EI itself; diff --git a/encrypt.go b/encrypt.go new file mode 100644 index 0000000..a5fc672 --- /dev/null +++ b/encrypt.go @@ -0,0 +1,331 @@ +package reader + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "fmt" +) + +// Permissions say what a reader may do with a file it can open with the user +// password. They are advisory — nothing enforces them but the reader's own +// good manners — and a file opened with the owner password ignores them +// entirely. +type Permissions uint32 + +// The permissions a standard security handler can express. Anything not +// granted is refused; [AllPermissions] grants everything. +const ( + PermPrint Permissions = 1 << 2 + PermModify Permissions = 1 << 3 + PermCopy Permissions = 1 << 4 + PermAnnotate Permissions = 1 << 5 + PermFillForms Permissions = 1 << 8 + PermExtract Permissions = 1 << 9 // for accessibility + PermAssemble Permissions = 1 << 10 + PermPrintFaithful Permissions = 1 << 11 // at full resolution + + // AllPermissions grants every one of them. + AllPermissions = PermPrint | PermModify | PermCopy | PermAnnotate | + PermFillForms | PermExtract | PermAssemble | PermPrintFaithful +) + +// permissionBase is the pattern of reserved bits the specification requires +// around the permissions themselves. +const permissionBase uint32 = 0xFFFFF0C0 + +// An Encryption says how a file is to be protected. +// +// Two people can open it: whoever knows the user password, subject to the +// permissions, and whoever knows the owner password, subject to nothing. An +// empty user password means the file opens without one and the permissions are +// all it says. +type Encryption struct { + UserPassword string + OwnerPassword string + Permissions Permissions + + // AES128 asks for the older method, which readers before 2008 understand. + // The default is AES-256, which is what a file should use today. + AES128 bool +} + +// Encrypt protects everything written from here on. It must be called before +// anything is written, and the file it produces is not byte-for-byte +// reproducible: encryption needs randomness, by design. +func (w *Writer) Encrypt(e Encryption) { + if len(w.offsets) > 0 || len(w.pending) > 0 { + w.note(fmt.Errorf("reader: a file cannot be encrypted after it has been written to")) + return + } + enc, err := newEncrypter(e) + if err != nil { + w.note(err) + return + } + w.encrypt = enc +} + +// An encrypter holds the file key and the dictionary that describes it. +type encrypter struct { + key []byte + method cryptMethod + dict Dict + id []byte + err error + // number is the object the /Encrypt dictionary is written under, which is + // the one thing in the file that is never encrypted. + number int +} + +// newEncrypter derives a file key and builds the dictionary that lets a reader +// derive it again from a password. +func newEncrypter(e Encryption) (*encrypter, error) { + perm := permissionBase | uint32(e.Permissions) + id, err := randomBytes(16) + if err != nil { + return nil, err + } + if e.AES128 { + return newLegacyEncrypter(e, perm, id) + } + return newAES256Encrypter(e, perm, id) +} + +// newAES256Encrypter builds the revision 6 form: a random file key, wrapped +// twice, once for each password. +func newAES256Encrypter(e Encryption, perm uint32, id []byte) (*encrypter, error) { + key, err := randomBytes(32) + if err != nil { + return nil, err + } + // An owner password nobody set is the user's: leaving it empty would + // mean anything at all opens the file with every permission, which is + // the opposite of what asking for encryption means. + ownerPassword := e.OwnerPassword + if ownerPassword == "" { + ownerPassword = e.UserPassword + } + user, userE, err := wrapKey([]byte(e.UserPassword), key, nil) + if err != nil { + return nil, err + } + owner, ownerE, err := wrapKey([]byte(ownerPassword), key, user) + if err != nil { + return nil, err + } + perms, err := permsEntry(key, perm) + if err != nil { + return nil, err + } + return &encrypter{ + key: key, + method: cryptAESV3, + id: id, + dict: Dict{ + "Filter": Name("Standard"), + "V": Integer(5), + "R": Integer(6), + "Length": Integer(256), + "P": Integer(int32(perm)), + "U": String(user), + "UE": String(userE), + "O": String(owner), + "OE": String(ownerE), + "Perms": String(perms), + "CF": Dict{"StdCF": Dict{"CFM": Name("AESV3"), "Length": Integer(32)}}, + "StmF": Name("StdCF"), + "StrF": Name("StdCF"), + "EncryptMetadata": Bool(true), + }, + }, nil +} + +// wrapKey builds one 48-byte password entry and the 32 bytes that hold the +// file key wrapped for it. +func wrapKey(password, key, udata []byte) (entry, wrapped []byte, err error) { + salts, err := randomBytes(16) + if err != nil { + return nil, nil, err + } + validation, keySalt := salts[:8], salts[8:] + entry = append(append(hash2B(password, validation, udata, 6), validation...), keySalt...) + // hash2B always returns 32 bytes, so the cipher cannot refuse the key. + block, _ := aes.NewCipher(hash2B(password, keySalt, udata, 6)) + wrapped = make([]byte, 32) + cipher.NewCBCEncrypter(block, make([]byte, aes.BlockSize)).CryptBlocks(wrapped, key) + return entry, wrapped, nil +} + +// permsEntry is the block that lets a reader check the permissions have not +// been tampered with: they are encrypted with the file key itself. +func permsEntry(key []byte, perm uint32) ([]byte, error) { + tail, err := randomBytes(4) + if err != nil { + return nil, err + } + plain := []byte{ + byte(perm), byte(perm >> 8), byte(perm >> 16), byte(perm >> 24), + 0xFF, 0xFF, 0xFF, 0xFF, + 'T', // metadata is encrypted too + 'a', 'd', 'b', // the specification's own marker + tail[0], tail[1], tail[2], tail[3], + } + // The file key is 32 bytes by construction. + block, _ := aes.NewCipher(key) + out := make([]byte, 16) + // One block, with no chaining: this is the one place the format uses it. + block.Encrypt(out, plain) + return out, nil +} + +// newLegacyEncrypter builds the revision 4 form, whose key comes from the +// passwords rather than the other way round. +func newLegacyEncrypter(e Encryption, perm uint32, id []byte) (*encrypter, error) { + const n = 16 // 128 bits + owner := legacyOwnerValue([]byte(e.OwnerPassword), []byte(e.UserPassword), n) + key := legacyFileKey(padPassword([]byte(e.UserPassword)), owner, id, int32(perm), n, 4, true) + user := legacyUserValue(key, id) + return &encrypter{ + key: key, + method: cryptAESV2, + id: id, + dict: Dict{ + "Filter": Name("Standard"), + "V": Integer(4), + "R": Integer(4), + "Length": Integer(128), + "P": Integer(int32(perm)), + "O": String(owner), + "U": String(user), + "CF": Dict{"StdCF": Dict{"CFM": Name("AESV2"), "Length": Integer(16)}}, + "StmF": Name("StdCF"), + "StrF": Name("StdCF"), + "EncryptMetadata": Bool(true), + }, + }, nil +} + +// legacyOwnerValue is algorithm 3: the /O entry, which holds the user password +// encrypted under the owner's. +func legacyOwnerValue(ownerPassword, userPassword []byte, n int) []byte { + if len(ownerPassword) == 0 { + ownerPassword = userPassword + } + sum := md5Sum(padPassword(ownerPassword)) + key := sum + for i := 0; i < 50; i++ { + key = md5Sum(key) + } + key = key[:n] + x := padPassword(userPassword) + for i := 0; i <= 19; i++ { + x = rc4Bytes(xorKey(key, i), x) + } + return x +} + +// legacyUserValue is algorithm 5: the /U entry. +func legacyUserValue(key, id []byte) []byte { + x := rc4Bytes(key, md5Sum(append(append([]byte{}, pad...), id...))) + for i := 1; i <= 19; i++ { + x = rc4Bytes(xorKey(key, i), x) + } + return append(x, make([]byte, 16)...) +} + +// randomSource is where randomness comes from. It is a variable so a test +// can take it away and see that a file is refused rather than written +// without the protection it was asked for. +var randomSource = rand.Read + +// randomBytes is the only source of randomness here, and the reason an +// encrypted file is not reproducible. +func randomBytes(n int) ([]byte, error) { + out := make([]byte, n) + if _, err := randomSource(out); err != nil { + return nil, fmt.Errorf("reader: no randomness available: %w", err) + } + return out, nil +} + +// apply encrypts one object's strings and, for a stream, its data. +func (e *encrypter) apply(num, gen int, o Object) Object { + switch v := o.(type) { + case String: + return String(e.encryptBytes(num, gen, v)) + case Array: + out := make(Array, len(v)) + for i, x := range v { + out[i] = e.apply(num, gen, x) + } + return out + case Dict: + out := Dict{} + for k, x := range v { + out[k] = e.apply(num, gen, x) + } + return out + case *Stream: + out := &Stream{Dict: Dict{}, Raw: e.encryptBytes(num, gen, v.Raw)} + for k, x := range v.Dict { + out.Dict[k] = e.apply(num, gen, x) + } + return out + } + return o +} + +// encryptBytes protects one object's bytes. +func (e *encrypter) encryptBytes(num, gen int, data []byte) []byte { + dec := &decryptor{key: e.key} + key := dec.objectKey(num, gen, e.method) + iv, err := randomBytes(aes.BlockSize) + if err != nil { + // Writing the bytes as they are would hand out in the clear what was + // asked to be protected, so the failure is remembered and the whole + // file is refused. + e.note(err) + return data + } + // The file key is 32 bytes by construction. + block, _ := aes.NewCipher(key) + // The padding CBC calls for, always at least one whole block of it. + n := aes.BlockSize - len(data)%aes.BlockSize + padded := append(append([]byte{}, data...), bytes.Repeat([]byte{byte(n)}, n)...) + out := make([]byte, len(padded)) + cipher.NewCBCEncrypter(block, iv).CryptBlocks(out, padded) + return append(iv, out...) +} + +// md5Sum is the hash the pre-2.0 handlers are built on. +func md5Sum(b []byte) []byte { + sum := md5.Sum(b) + return sum[:] +} + +// writeEncryptDict writes the /Encrypt dictionary and returns the trailer +// entries a reader needs to find and use it. The dictionary is the one object +// in the file that is never encrypted, since it says how to decrypt the rest. +func (w *Writer) writeEncryptDict() Dict { + if w.encrypt == nil { + return nil + } + ref := w.Reserve() + w.encrypt.number = ref.Num + w.writeInlineRaw(ref, w.encrypt.dict) + // The identifier takes part in the older key derivation and has to be in + // the trailer for a reader to reach it. + id := String(w.encrypt.id) + return Dict{"Encrypt": ref, "ID": Array{id, id}} +} + +// note keeps the first thing that went wrong while encrypting, so a file that +// could not be protected is refused rather than written in the clear. +func (e *encrypter) note(err error) { + if e.err == nil { + e.err = err + } +} diff --git a/encrypt_test.go b/encrypt_test.go new file mode 100644 index 0000000..8a2010f --- /dev/null +++ b/encrypt_test.go @@ -0,0 +1,365 @@ +package reader + +import ( + "bytes" + "crypto/aes" + "errors" + "fmt" + "testing" +) + +// protectedFile builds a small document behind a password. +func protectedFile(t *testing.T, packed bool, e Encryption) []byte { + t.Helper() + w := NewWriter("1.7") + if packed { + w = NewPackedWriter("1.7") + } + w.Encrypt(e) + pagesRef := w.Reserve() + content := w.Add(&Stream{Dict: Dict{}, Raw: []byte("BT (hello) Tj ET")}) + page := w.Add(Dict{"Type": Name("Page"), "Parent": pagesRef, "Contents": content}) + w.Put(pagesRef, Dict{"Type": Name("Pages"), "Kids": Array{page}, "Count": Integer(1), + "MediaBox": Array{Integer(0), Integer(0), Integer(100), Integer(100)}}) + root := w.Add(Dict{"Type": Name("Catalog"), "Pages": pagesRef}) + info := w.Add(Dict{"Title": String("a secret title")}) + out, err := w.Finish(Dict{"Root": root, "Info": info}) + if err != nil { + t.Fatal(err) + } + return out +} + +// readsBack asserts that a password opens the file and everything is intact. +func readsBack(t *testing.T, b []byte, password string) { + t.Helper() + d, err := OpenWithPassword(b, password) + if err != nil { + t.Fatalf("%q: %v", password, err) + } + if !d.Encrypted() { + t.Errorf("%q: the file does not report itself encrypted", password) + } + data, err := d.PageContent(1) + if err != nil || string(data) != "BT (hello) Tj ET" { + t.Errorf("%q: content = %q, %v", password, data, err) + } + info, _ := d.Resolve(d.Trailer().Get("Info")) + dict, _ := ToDict(info) + title, _ := ToString(mustResolveObject(d, dict.Get("Title"))) + if string(title) != "a secret title" { + t.Errorf("%q: title = %q", password, title) + } +} + +// mustResolveObject is a test helper that ignores the error a document that +// opened cannot produce. +func mustResolveObject(d *Document, o Object) Object { + out, _ := d.Resolve(o) + return out +} + +func TestEncryptAndRead(t *testing.T) { + for _, packed := range []bool{false, true} { + for _, aes128 := range []bool{false, true} { + b := protectedFile(t, packed, Encryption{ + UserPassword: "hunter2", + OwnerPassword: "letmein", + Permissions: PermPrint, + AES128: aes128, + }) + readsBack(t, b, "hunter2") + readsBack(t, b, "letmein") + if _, err := Open(b); err != ErrWrongPassword { + t.Errorf("packed=%v aes128=%v: no password gave %v", packed, aes128, err) + } + if _, err := OpenWithPassword(b, "nope"); err != ErrWrongPassword { + t.Errorf("packed=%v aes128=%v: a wrong password gave %v", packed, aes128, err) + } + // Nothing readable is left lying about. + if bytes.Contains(b, []byte("a secret title")) { + t.Errorf("packed=%v aes128=%v: the title is in the clear", packed, aes128) + } + if bytes.Contains(b, []byte("hello")) { + t.Errorf("packed=%v aes128=%v: the content is in the clear", packed, aes128) + } + } + } +} + +func TestAnEmptyOwnerPasswordIsTheUsers(t *testing.T) { + // Leaving the owner password out must not mean anything at all opens the + // file with every permission. + for _, aes128 := range []bool{false, true} { + b := protectedFile(t, false, Encryption{UserPassword: "hunter2", AES128: aes128}) + readsBack(t, b, "hunter2") + if _, err := Open(b); err != ErrWrongPassword { + t.Errorf("aes128=%v: the empty password opened it", aes128) + } + if _, err := OpenWithPassword(b, "nope"); err != ErrWrongPassword { + t.Errorf("aes128=%v: a wrong password opened it", aes128) + } + } +} + +func TestAnEmptyUserPasswordOpensWithoutOne(t *testing.T) { + // A file protected only against editing opens with no password at all. + b := protectedFile(t, false, Encryption{OwnerPassword: "letmein", Permissions: PermPrint}) + readsBack(t, b, "") + readsBack(t, b, "letmein") +} + +func TestPermissionsAreWrittenDown(t *testing.T) { + b := protectedFile(t, false, Encryption{UserPassword: "x", Permissions: PermPrint | PermCopy}) + d, err := OpenWithPassword(b, "x") + if err != nil { + t.Fatal(err) + } + enc, err := d.Resolve(d.Trailer().Get("Encrypt")) + if err != nil { + t.Fatal(err) + } + dict, ok := ToDict(enc) + if !ok { + t.Fatalf("/Encrypt is a %s", enc.Kind()) + } + p, ok := ToInt(dict.Get("P")) + if !ok { + t.Fatalf("/P = %v", dict.Get("P")) + } + got := Permissions(uint32(int32(p)) &^ permissionBase) + if got != PermPrint|PermCopy { + t.Errorf("permissions = %b, want %b", got, PermPrint|PermCopy) + } + // And the file says which handler wrote it. + if v, _ := ToInt(dict.Get("R")); v != 6 { + t.Errorf("/R = %v", dict.Get("R")) + } +} + +func TestEncryptRefusesAfterWritingHasBegun(t *testing.T) { + w := NewWriter("1.7") + w.Add(Integer(1)) + w.Encrypt(Encryption{UserPassword: "x"}) + if w.Err() == nil { + t.Error("want an error") + } + // And on the packed side, where the first object is only pending. + w = NewPackedWriter("1.7") + w.Add(Integer(1)) + w.Encrypt(Encryption{UserPassword: "x"}) + if w.Err() == nil { + t.Error("want an error") + } +} + +func TestTheEncryptDictionaryIsNotItselfEncrypted(t *testing.T) { + b := protectedFile(t, false, Encryption{UserPassword: "x"}) + // The handler's name is a name, not a string, so it is never encrypted; + // what matters is that a reader with no key at all can still read the + // dictionary that tells it how to get one. + if !bytes.Contains(b, []byte("/Standard")) { + t.Error("the security handler cannot be identified without a key") + } + d := &Document{buf: b, xref: map[int]xrefEntry{}, cache: map[int]Object{}, + loading: map[int]bool{}, objStms: map[int]map[int]Object{}} + if err := d.loadXref(); err != nil { + t.Fatal(err) + } + enc, err := d.Resolve(d.Trailer().Get("Encrypt")) + if err != nil { + t.Fatal(err) + } + dict, ok := ToDict(enc) + if !ok { + t.Fatalf("/Encrypt is a %s", enc.Kind()) + } + u, _ := ToString(dict.Get("U")) + if len(u) != 48 { + t.Errorf("/U is %d bytes, so it was encrypted along with everything else", len(u)) + } +} + +func TestTheIdentifierIsInTheTrailer(t *testing.T) { + b := protectedFile(t, true, Encryption{UserPassword: "x", AES128: true}) + d, err := OpenWithPassword(b, "x") + if err != nil { + t.Fatal(err) + } + id, ok := ToArray(d.Trailer().Get("ID")) + if !ok || len(id) != 2 { + t.Fatalf("/ID = %v", d.Trailer().Get("ID")) + } + first, _ := ToString(id[0]) + if len(first) != 16 { + t.Errorf("/ID[0] is %d bytes", len(first)) + } +} + +func TestPermsEntryCarriesThePermissions(t *testing.T) { + key := bytes.Repeat([]byte{7}, 32) + got, err := permsEntry(key, permissionBase|uint32(PermPrint)) + if err != nil { + t.Fatal(err) + } + if len(got) != 16 { + t.Fatalf("/Perms is %d bytes", len(got)) + } + // It is one block encrypted with the file key and nothing else, so it + // decrypts back to what went in. + plain := decryptOneBlock(t, key, got) + if string(plain[9:12]) != "adb" { + t.Errorf("the marker is %q", plain[9:12]) + } + if plain[8] != 'T' { + t.Errorf("the metadata flag is %q", plain[8]) + } +} + +func TestLegacyOwnerValueUsesTheUserPasswordWhenThereIsNone(t *testing.T) { + withOwner := legacyOwnerValue([]byte("owner"), []byte("user"), 16) + without := legacyOwnerValue(nil, []byte("user"), 16) + sameAsUser := legacyOwnerValue([]byte("user"), []byte("user"), 16) + if bytes.Equal(withOwner, without) { + t.Error("an owner password made no difference") + } + if !bytes.Equal(without, sameAsUser) { + t.Error("an absent owner password is not the user's") + } +} + +func TestRandomBytes(t *testing.T) { + a, err := randomBytes(16) + if err != nil { + t.Fatal(err) + } + b, err := randomBytes(16) + if err != nil { + t.Fatal(err) + } + if bytes.Equal(a, b) { + t.Error("two draws came out the same") + } + if len(a) != 16 { + t.Errorf("got %d bytes", len(a)) + } +} + +func TestEncryptedFilesAreNotReproducible(t *testing.T) { + // Encryption needs randomness, so two writings of the same document differ + // — which is the one place this writer is deliberately not a function. + e := Encryption{UserPassword: "x"} + if bytes.Equal(protectedFile(t, false, e), protectedFile(t, false, e)) { + t.Error("two encrypted writings came out identical") + } +} + +func TestPermissionNames(t *testing.T) { + // The values are the bit positions the specification gives them, counting + // from one; nothing here should drift. + for perm, bit := range map[Permissions]int{ + PermPrint: 3, PermModify: 4, PermCopy: 5, PermAnnotate: 6, + PermFillForms: 9, PermExtract: 10, PermAssemble: 11, PermPrintFaithful: 12, + } { + if perm != 1<<(bit-1) { + t.Errorf("%b is not bit %d", perm, bit) + } + } + if AllPermissions == 0 { + t.Error("AllPermissions grants nothing") + } +} + +// decryptOneBlock undoes a single AES block, which is the only place the +// format uses the cipher without chaining. +func decryptOneBlock(t *testing.T, key, data []byte) []byte { + t.Helper() + block, err := aes.NewCipher(key) + if err != nil { + t.Fatal(err) + } + out := make([]byte, 16) + block.Decrypt(out, data) + return out +} + +// noRandomness makes every draw fail, so the paths that give up rather than +// write an unprotected file can be reached. +func noRandomness(t *testing.T) { + t.Helper() + old := randomSource + randomSource = func([]byte) (int, error) { return 0, errNoRandomness } + t.Cleanup(func() { randomSource = old }) +} + +var errNoRandomness = errors.New("no randomness for the test") + +func TestWithoutRandomnessNothingIsWritten(t *testing.T) { + noRandomness(t) + for _, aes128 := range []bool{false, true} { + w := NewWriter("1.7") + w.Encrypt(Encryption{UserPassword: "x", AES128: aes128}) + if w.Err() == nil { + t.Errorf("aes128=%v: a file was set up to be encrypted with no randomness", aes128) + } + if _, err := w.Finish(Dict{}); err == nil { + t.Errorf("aes128=%v: it was written anyway", aes128) + } + } +} + +func TestRandomnessLostAfterSettingUp(t *testing.T) { + // The key is derived, and only then does randomness run out. Nothing may + // be handed back: some of the file would be in the clear. + for _, packed := range []bool{false, true} { + t.Run(map[bool]string{false: "plain", true: "packed"}[packed], func(t *testing.T) { + w := NewWriter("1.7") + if packed { + w = NewPackedWriter("1.7") + } + w.Encrypt(Encryption{UserPassword: "x"}) + if w.Err() != nil { + t.Fatal(w.Err()) + } + noRandomness(t) + root := w.Add(Dict{"Type": Name("Catalog"), "Title": String("a secret")}) + if _, err := w.Finish(Dict{"Root": root}); err == nil { + t.Error("the file was written anyway") + } + }) + } +} + +// budgetedRandomness lets exactly n draws succeed and fails every one after. +func budgetedRandomness(t *testing.T, n int) { + t.Helper() + old := randomSource + left := n + randomSource = func(b []byte) (int, error) { + if left == 0 { + return 0, errNoRandomness + } + left-- + return old(b) + } + t.Cleanup(func() { randomSource = old }) +} + +func TestRandomnessRunningOutAtEachStep(t *testing.T) { + // Setting up AES-256 draws five times: the identifier, the file key, the + // salts for each of the two passwords, and the tail of the permissions + // block. Whichever draw fails, no file comes out. + for n := 0; n < 5; n++ { + t.Run(fmt.Sprintf("after %d draws", n), func(t *testing.T) { + budgetedRandomness(t, n) + w := NewWriter("1.7") + w.Encrypt(Encryption{UserPassword: "x"}) + if w.Err() == nil { + t.Fatal("the file was set up anyway") + } + if _, err := w.Finish(Dict{}); err == nil { + t.Error("it was written anyway") + } + }) + } +} diff --git a/objstm.go b/objstm.go index 78b060f..726d04a 100644 --- a/objstm.go +++ b/objstm.go @@ -73,6 +73,8 @@ func flateCompress(data []byte) []byte { // rather than a table, which is the only form that can name an object inside // an object stream. func (w *Writer) finishWithXrefStream(trailer Dict) ([]byte, error) { + // Finish has already written the /Encrypt dictionary and put what a + // reader needs into the trailer. w.packObjects() xref := w.Reserve() // The cross-reference stream is reserved after everything else — the @@ -105,8 +107,11 @@ func (w *Writer) finishWithXrefStream(trailer Dict) ([]byte, error) { for k, v := range trailer { dict[k] = v } - w.writeInline(xref, &Stream{Dict: dict, Raw: rows}) + w.writeInlineRaw(xref, &Stream{Dict: dict, Raw: rows}) fmt.Fprintf(&w.buf, "startxref\n%d\n%%%%EOF\n", start) + if w.encrypt != nil && w.encrypt.err != nil { + w.note(w.encrypt.err) + } if w.err != nil { return nil, w.err } diff --git a/writer.go b/writer.go index 665e7a8..616cd34 100644 --- a/writer.go +++ b/writer.go @@ -196,6 +196,7 @@ type Writer struct { copied map[*Document]map[int]Ref err error pack bool + encrypt *encrypter } // NewWriter starts a file with the given version in its header, "1.7" when the @@ -259,15 +260,38 @@ func (w *Writer) Put(ref Ref, o Object) { w.writeInline(ref, o) } -// writeInline writes an object where it stands in the file. +// writeInline writes an object where it stands in the file, compressed and +// then encrypted — in that order, since encrypted bytes do not compress. func (w *Writer) writeInline(ref Ref, o Object) { - if s, ok := o.(*Stream); ok && w.pack && s.Dict.Get("Filter").Kind() == KindNull { - // Nothing this package generated arrives compressed, and a packed - // file is being written to be small. - s = &Stream{Dict: cloneDict(s.Dict), Raw: flateCompress(s.Raw)} - s.Dict["Filter"] = Name("FlateDecode") - o = s + o = w.compressStream(o) + if w.encrypt != nil { + o = w.encrypt.apply(ref.Num, ref.Gen, o) } + w.emit(ref, o) +} + +// writeInlineRaw writes an object that must not be encrypted: the /Encrypt +// dictionary itself, and the cross-reference stream, neither of which a +// reader could decrypt, since it needs them to work out how. +func (w *Writer) writeInlineRaw(ref Ref, o Object) { + w.emit(ref, w.compressStream(o)) +} + +// compressStream deflates a stream that arrives with no filter of its own, +// when the file is being written to be small. +func (w *Writer) compressStream(o Object) Object { + s, ok := o.(*Stream) + if !ok || !w.pack || s.Dict.Get("Filter").Kind() != KindNull { + return o + } + // Nothing this package generated arrives compressed. + out := &Stream{Dict: cloneDict(s.Dict), Raw: flateCompress(s.Raw)} + out.Dict["Filter"] = Name("FlateDecode") + return out +} + +// emit writes an object and remembers where it went. +func (w *Writer) emit(ref Ref, o Object) { w.offsets[ref.Num] = w.buf.Len() fmt.Fprintf(&w.buf, "%d %d obj\n", ref.Num, ref.Gen) w.buf.Write(AppendObject(nil, o)) @@ -304,6 +328,12 @@ func (w *Writer) note(err error) { // file. /Size is filled in; the caller supplies /Root and whatever else the // trailer needs. func (w *Writer) Finish(trailer Dict) ([]byte, error) { + // The caller's dictionary is left as it was; what a file needs to say + // about its own encryption is added to a copy. + trailer = cloneDict(trailer) + for k, v := range w.writeEncryptDict() { + trailer[k] = v + } if w.pack { return w.finishWithXrefStream(trailer) } @@ -331,6 +361,9 @@ func (w *Writer) Finish(trailer Dict) ([]byte, error) { w.buf.WriteString("trailer\n") w.buf.Write(AppendObject(nil, out)) fmt.Fprintf(&w.buf, "\nstartxref\n%d\n%%%%EOF\n", start) + if w.encrypt != nil && w.encrypt.err != nil { + w.note(w.encrypt.err) + } if w.err != nil { return nil, w.err }