diff --git a/crypt.go b/crypt.go index b7cea0a..993cfe4 100644 --- a/crypt.go +++ b/crypt.go @@ -44,10 +44,58 @@ type decryptor struct { strings cryptMethod streams cryptMethod revision int + perm Permissions + owner bool // the password given was the owner's, not the user's skipObj int // the /Encrypt dictionary's own object number, never decrypted skipKnown bool // whether skipObj is meaningful } +// A Protection is what a file's security handler says about it: how it is +// encrypted, and what a reader that opened it with the user password may do. +type Protection struct { + // Method names the algorithm the way a person would say it: "RC4-40", + // "RC4-128", "AES-128", "AES-256", or "none" for a file that declares an + // /Encrypt dictionary and then encrypts nothing with it. + Method string + // Revision is the standard security handler's revision: 2, 3, 4, 5 or 6. + Revision int + // Permissions is what the file grants whoever opened it with the user + // password. + Permissions Permissions + // Owner is true when the password the file was opened with was the + // owner's, in which case the permissions do not apply to this reader. + Owner bool +} + +// Protection reports how the file is protected, and false when it is not +// protected at all. A document that opened has already been decrypted; this +// says what it said about itself on the way. +func (d *Document) Protection() (Protection, bool) { + if d.decrypt == nil { + return Protection{}, false + } + return Protection{ + Method: d.decrypt.methodName(), + Revision: d.decrypt.revision, + Permissions: d.decrypt.perm, + Owner: d.decrypt.owner, + }, true +} + +// methodName says which algorithm protects the streams, which is the one that +// matters: it is where the content is. +func (dec *decryptor) methodName() string { + switch dec.streams { + case cryptAESV3: + return "AES-256" + case cryptAESV2: + return "AES-128" + case cryptRC4: + return fmt.Sprintf("RC4-%d", len(dec.key)*8) + } + return "none" +} + // Encrypted reports whether the file declares an /Encrypt dictionary. func (d *Document) Encrypted() bool { return d.trailer != nil && d.trailer.Get("Encrypt").Kind() != KindNull @@ -120,23 +168,24 @@ func newDecryptor(enc Dict, id []byte, password string, r Resolver) (*decryptor, metadata = b } + dec.perm = Permissions(uint32(perm)) & AllPermissions if rev >= 5 { - key, err := deriveKeyR5(enc, password, r) + key, asOwner, err := deriveKeyR5(enc, password, r) if err != nil { return nil, err } - dec.key = key + dec.key, dec.owner = key, asOwner return dec, nil } n := length / 8 if rev == 2 { n = 5 } - key, err := deriveKeyLegacy(password, owner, user, id, perm, n, rev, metadata) + key, asOwner, err := deriveKeyLegacy(password, owner, user, id, perm, n, rev, metadata) if err != nil { return nil, err } - dec.key = key + dec.key, dec.owner = key, asOwner return dec, nil } @@ -181,25 +230,32 @@ func (dec *decryptor) readMethods(enc Dict, v int, r Resolver) error { // deriveKeyLegacy is the pre-2.0 key derivation, trying the password as the // user password and then as the owner password. -func deriveKeyLegacy(password string, owner, user, id []byte, perm int32, n, rev int, metadata bool) ([]byte, error) { +func deriveKeyLegacy(password string, owner, user, id []byte, perm int32, n, rev int, metadata bool) (key []byte, asOwner bool, err error) { for _, candidate := range legacyCandidates(password, owner, n, rev) { - key := legacyFileKey(candidate, owner, id, perm, n, rev, metadata) - if legacyUserKeyMatches(key, user, id, rev) { - return key, nil + k := legacyFileKey(candidate.padded, owner, id, perm, n, rev, metadata) + if legacyUserKeyMatches(k, user, id, rev) { + return k, candidate.owner, nil } } - return nil, ErrWrongPassword + return nil, false, ErrWrongPassword +} + +// A legacyCandidate is one padded password to try, and whether reaching it +// meant knowing the owner's rather than the user's. +type legacyCandidate struct { + padded []byte + owner bool } // legacyCandidates lists the padded passwords worth trying: the one given, the // empty one, and the user password the owner password unlocks. -func legacyCandidates(password string, owner []byte, n, rev int) [][]byte { - out := [][]byte{padPassword([]byte(password))} +func legacyCandidates(password string, owner []byte, n, rev int) []legacyCandidate { + out := []legacyCandidate{{padded: padPassword([]byte(password))}} if password != "" { - out = append(out, padPassword(nil)) + out = append(out, legacyCandidate{padded: padPassword(nil)}) } if u := userFromOwner([]byte(password), owner, n, rev); u != nil { - out = append(out, u) + out = append(out, legacyCandidate{padded: u, owner: true}) } return out } @@ -284,14 +340,14 @@ func xorKey(key []byte, v int) []byte { // deriveKeyR5 is the PDF 2.0 derivation, /R 5 and /R 6: the password is // validated against a salted hash and then unwraps the file key. -func deriveKeyR5(enc Dict, password string, r Resolver) ([]byte, error) { +func deriveKeyR5(enc Dict, password string, r Resolver) (key []byte, asOwner bool, err error) { user, _ := ToString(resolved(enc, "U", r)) userE, _ := ToString(resolved(enc, "UE", r)) owner, _ := ToString(resolved(enc, "O", r)) ownerE, _ := ToString(resolved(enc, "OE", r)) rev := int(intOf(resolved(enc, "R", r), 6)) if len(user) < 48 { - return nil, fmt.Errorf("reader: /U is %d bytes, not the 48 this revision needs", len(user)) + return nil, false, fmt.Errorf("reader: /U is %d bytes, not the 48 this revision needs", len(user)) } pw := []byte(password) for _, candidate := range [][]byte{pw, nil} { @@ -299,15 +355,15 @@ func deriveKeyR5(enc Dict, password string, r Resolver) ([]byte, error) { break } if key := unlockR5(candidate, user, userE, nil, rev); key != nil { - return key, nil + return key, false, nil } if len(owner) >= 48 { if key := unlockR5(candidate, owner, ownerE, user[:48], rev); key != nil { - return key, nil + return key, true, nil } } } - return nil, ErrWrongPassword + return nil, false, ErrWrongPassword } // unlockR5 checks one password against a 48-byte /U or /O entry and, when it diff --git a/crypt_test.go b/crypt_test.go index b8ce166..56b8141 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -370,3 +370,37 @@ func TestDecryptAfterAnOffsetRepair(t *testing.T) { t.Errorf("/Producer = %q, want %q", s, encryptedProducer) } } + +func TestTheMethodAFileIsProtectedWithIsNamedInWords(t *testing.T) { + // Every method the reader understands, including the older ones no + // writer here produces, and a file that declares a handler and then + // protects nothing with it. + cases := []struct { + name string + opts encOptions + want string + }{ + {"40-bit RC4", encOptions{v: 1, r: 2, length: 40}, "RC4-40"}, + {"128-bit RC4", encOptions{v: 2, r: 3, length: 128}, "RC4-128"}, + {"RC4 through a crypt filter", encOptions{v: 4, r: 4, length: 128, method: cryptRC4}, "RC4-128"}, + {"AES-128", encOptions{v: 4, r: 4, length: 128, method: cryptAESV2}, "AES-128"}, + {"AES-256", encOptions{v: 5, r: 6, length: 256, method: cryptAESV3}, "AES-256"}, + {"a handler that protects nothing", encOptions{v: 4, r: 4, length: 128, method: cryptNone}, "none"}, + } + for _, c := range cases { + d, err := Open(encryptedFile(t, c.opts)) + if err != nil { + t.Fatalf("%s: %v", c.name, err) + } + p, ok := d.Protection() + if !ok { + t.Fatalf("%s: reported as unprotected", c.name) + } + if p.Method != c.want { + t.Errorf("%s: method %q, want %q", c.name, p.Method, c.want) + } + if p.Revision != c.opts.r { + t.Errorf("%s: revision %d, want %d", c.name, p.Revision, c.opts.r) + } + } +} diff --git a/cryptbuild_test.go b/cryptbuild_test.go index d0168a2..0ad1f2e 100644 --- a/cryptbuild_test.go +++ b/cryptbuild_test.go @@ -197,8 +197,14 @@ func encryptedFile(t *testing.T, opt encOptions) []byte { meta = "false" } cfm := "V2" - if enc.method == cryptAESV2 { + switch enc.method { + case cryptAESV2: cfm = "AESV2" + case cryptNone: + // A file that declares a security handler and then protects + // nothing with it: rare, legal, and something a person asking + // what a file is protected with deserves to be told. + cfm = "None" } if opt.v >= 4 { encDict = fmt.Sprintf( diff --git a/encrypt.go b/encrypt.go index a5fc672..613550d 100644 --- a/encrypt.go +++ b/encrypt.go @@ -7,6 +7,7 @@ import ( "crypto/md5" "crypto/rand" "fmt" + "strings" ) // Permissions say what a reader may do with a file it can open with the user @@ -32,6 +33,39 @@ const ( PermFillForms | PermExtract | PermAssemble | PermPrintFaithful ) +// permissionNames pairs each permission with what it lets a reader do, in the +// order a person would want to read them. +var permissionNames = []struct { + bit Permissions + name string +}{ + {PermPrint, "print"}, + {PermPrintFaithful, "print at full resolution"}, + {PermModify, "modify"}, + {PermAssemble, "assemble"}, + {PermCopy, "copy"}, + {PermExtract, "extract for accessibility"}, + {PermAnnotate, "annotate"}, + {PermFillForms, "fill in forms"}, +} + +// Allows reports whether every one of the given permissions is granted. +func (p Permissions) Allows(want Permissions) bool { return p&want == want } + +// String lists what is granted, in words. +func (p Permissions) String() string { + var out []string + for _, e := range permissionNames { + if p.Allows(e.bit) { + out = append(out, e.name) + } + } + if len(out) == 0 { + return "nothing" + } + return strings.Join(out, ", ") +} + // permissionBase is the pattern of reserved bits the specification requires // around the permissions themselves. const permissionBase uint32 = 0xFFFFF0C0 diff --git a/encrypt_test.go b/encrypt_test.go index 8a2010f..609e3bb 100644 --- a/encrypt_test.go +++ b/encrypt_test.go @@ -363,3 +363,93 @@ func TestRandomnessRunningOutAtEachStep(t *testing.T) { }) } } + +func TestWhatAProtectedFileSaysAboutItself(t *testing.T) { + // A file that opened has already been decrypted; what it said about + // itself on the way is what a person needs to be told. + cases := []struct { + name string + enc Encryption + open string + wantMethod string + wantRev int + wantOwner bool + }{ + {"AES-256 opened by the user", Encryption{ + UserPassword: "u", OwnerPassword: "o", Permissions: PermPrint, + }, "u", "AES-256", 6, false}, + {"AES-256 opened by the owner", Encryption{ + UserPassword: "u", OwnerPassword: "o", Permissions: PermPrint, + }, "o", "AES-256", 6, true}, + {"AES-128 opened by the user", Encryption{ + UserPassword: "u", OwnerPassword: "o", Permissions: PermPrint | PermCopy, AES128: true, + }, "u", "AES-128", 4, false}, + {"AES-128 opened by the owner", Encryption{ + UserPassword: "u", OwnerPassword: "o", Permissions: PermPrint | PermCopy, AES128: true, + }, "o", "AES-128", 4, true}, + } + for _, c := range cases { + out := protectedFile(t, false, c.enc) + d, err := OpenWithPassword(out, c.open) + if err != nil { + t.Fatalf("%s: %v", c.name, err) + } + p, ok := d.Protection() + if !ok { + t.Fatalf("%s: an encrypted file says it is not protected", c.name) + } + if p.Method != c.wantMethod { + t.Errorf("%s: method %q, want %q", c.name, p.Method, c.wantMethod) + } + if p.Revision != c.wantRev { + t.Errorf("%s: revision %d, want %d", c.name, p.Revision, c.wantRev) + } + if p.Owner != c.wantOwner { + t.Errorf("%s: opened as owner = %v, want %v", c.name, p.Owner, c.wantOwner) + } + if p.Permissions != c.enc.Permissions { + t.Errorf("%s: permissions %v, want %v", c.name, p.Permissions, c.enc.Permissions) + } + } + + // A file with nothing to hide says so. + w := NewWriter("1.7") + pagesRef := w.Reserve() + page := w.Add(Dict{"Type": Name("Page"), "Parent": pagesRef}) + w.Put(pagesRef, Dict{"Type": Name("Pages"), "Kids": Array{page}, "Count": Integer(1)}) + root := w.Add(Dict{"Type": Name("Catalog"), "Pages": pagesRef}) + plain, err := w.Finish(Dict{"Root": root}) + if err != nil { + t.Fatal(err) + } + d, err := Open(plain) + if err != nil { + t.Fatal(err) + } + if p, ok := d.Protection(); ok { + t.Errorf("an unencrypted file reported %+v", p) + } +} + +func TestPermissionsInWords(t *testing.T) { + cases := []struct { + perm Permissions + want string + }{ + {0, "nothing"}, + {PermPrint, "print"}, + {PermPrint | PermCopy, "print, copy"}, + {AllPermissions, "print, print at full resolution, modify, assemble, copy, extract for accessibility, annotate, fill in forms"}, + } + for _, c := range cases { + if got := c.perm.String(); got != c.want { + t.Errorf("%d: %q, want %q", uint32(c.perm), got, c.want) + } + } + if !AllPermissions.Allows(PermPrint | PermCopy) { + t.Error("everything does not allow printing and copying") + } + if (PermPrint).Allows(PermPrint | PermCopy) { + t.Error("printing alone allowed copying") + } +}