Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 74 additions & 18 deletions crypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -284,30 +340,30 @@ 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} {
if candidate == nil && password == "" {
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
Expand Down
34 changes: 34 additions & 0 deletions crypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
8 changes: 7 additions & 1 deletion cryptbuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
34 changes: 34 additions & 0 deletions encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
90 changes: 90 additions & 0 deletions encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Loading