docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables, 04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI, skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted fixtures. Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard; TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and writeTimeout 120→240 s for the PDF path's worst case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
573 lines
17 KiB
Go
573 lines
17 KiB
Go
package reader
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/md5"
|
|
"crypto/rc4"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
)
|
|
|
|
// ErrWrongPassword is returned when a file is encrypted and neither the
|
|
// password given nor the empty one opens it.
|
|
var ErrWrongPassword = errors.New("reader: the password does not open this file")
|
|
|
|
// ErrUnsupportedEncryption is returned for a security handler this package
|
|
// does not implement — anything other than the standard one.
|
|
var ErrUnsupportedEncryption = errors.New("reader: unsupported security handler")
|
|
|
|
// pad is the 32-byte string every pre-2.0 password is padded with.
|
|
var pad = []byte{
|
|
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56,
|
|
0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
|
|
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
|
|
}
|
|
|
|
// cryptMethod is how one class of data — strings or streams — is protected.
|
|
type cryptMethod uint8
|
|
|
|
const (
|
|
cryptNone cryptMethod = iota // /Identity: not encrypted at all
|
|
cryptRC4
|
|
cryptAESV2 // 128-bit AES, per-object key
|
|
cryptAESV3 // 256-bit AES, the file key used directly
|
|
)
|
|
|
|
// A decryptor holds the file encryption key and how to apply it.
|
|
type decryptor struct {
|
|
key []byte
|
|
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
|
|
}
|
|
|
|
// setUpDecryption reads /Encrypt and derives the file key. It is called once,
|
|
// before any string or stream is handed out.
|
|
func (d *Document) setUpDecryption(password string) error {
|
|
if !d.Encrypted() {
|
|
return nil
|
|
}
|
|
if ref, ok := d.trailer.Get("Encrypt").(Ref); ok {
|
|
d.encryptNum, d.encryptKnown = ref.Num, true
|
|
}
|
|
o, err := d.Resolve(d.trailer.Get("Encrypt"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
enc, ok := ToDict(o)
|
|
if !ok {
|
|
return fmt.Errorf("reader: /Encrypt is a %s, not a dictionary", o.Kind())
|
|
}
|
|
id := d.firstID()
|
|
dec, err := newDecryptor(enc, id, password, d.Get)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dec.skipObj, dec.skipKnown = d.encryptNum, d.encryptKnown
|
|
d.decrypt = dec
|
|
return nil
|
|
}
|
|
|
|
// firstID returns the first element of the trailer's /ID, which takes part in
|
|
// the pre-2.0 key derivation. A file without one contributes nothing.
|
|
func (d *Document) firstID() []byte {
|
|
arr, ok := ToArray(d.trailer.Get("ID"))
|
|
if !ok || len(arr) == 0 {
|
|
return nil
|
|
}
|
|
s, ok := ToString(arr[0])
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return s
|
|
}
|
|
|
|
// newDecryptor derives the file encryption key from a password.
|
|
func newDecryptor(enc Dict, id []byte, password string, r Resolver) (*decryptor, error) {
|
|
if f, ok := ToName(resolved(enc, "Filter", r)); ok && f != "Standard" {
|
|
return nil, ErrUnsupportedEncryption
|
|
}
|
|
v := int(intOf(resolved(enc, "V", r), 0))
|
|
rev := int(intOf(resolved(enc, "R", r), 0))
|
|
if v == 0 || rev == 0 {
|
|
return nil, fmt.Errorf("reader: /Encrypt has no usable /V and /R")
|
|
}
|
|
length := int(intOf(resolved(enc, "Length", r), 40))
|
|
if length < 40 || length > 256 || length%8 != 0 {
|
|
length = 40
|
|
}
|
|
dec := &decryptor{revision: rev}
|
|
if err := dec.readMethods(enc, v, r); err != nil {
|
|
return nil, err
|
|
}
|
|
owner, _ := ToString(resolved(enc, "O", r))
|
|
user, _ := ToString(resolved(enc, "U", r))
|
|
perm := int32(intOf(resolved(enc, "P", r), 0))
|
|
metadata := true
|
|
if b, ok := ToBool(resolved(enc, "EncryptMetadata", r)); ok {
|
|
metadata = b
|
|
}
|
|
|
|
dec.perm = Permissions(uint32(perm)) & AllPermissions
|
|
if dec.streams == cryptNone && dec.strings == cryptNone {
|
|
// /StmF and /StrF both name the identity crypt filter, so nothing in
|
|
// the document body is encrypted and no key is needed to read it. A
|
|
// file like that is opened without validating the password: refusing
|
|
// it would refuse a document whose every byte is already plain, which
|
|
// is what every other reader shows. Only /EFF — the embedded files —
|
|
// would need the key, and this reader does not hand those out.
|
|
//
|
|
// Nothing was authenticated, so dec.owner stays false and Protection
|
|
// reports the method as "none": a caller can see exactly what it got.
|
|
return dec, nil
|
|
}
|
|
if rev >= 5 {
|
|
key, asOwner, err := deriveKeyR5(enc, password, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dec.key, dec.owner = key, asOwner
|
|
return dec, nil
|
|
}
|
|
n := length / 8
|
|
if rev == 2 {
|
|
n = 5
|
|
}
|
|
key, asOwner, err := deriveKeyLegacy(password, owner, user, id, perm, n, rev, metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dec.key, dec.owner = key, asOwner
|
|
return dec, nil
|
|
}
|
|
|
|
// readMethods works out how strings and streams are protected. Before /V 4
|
|
// there is one method for everything; from /V 4 the crypt filters in /CF are
|
|
// named by /StmF and /StrF.
|
|
func (dec *decryptor) readMethods(enc Dict, v int, r Resolver) error {
|
|
if v < 4 {
|
|
dec.strings, dec.streams = cryptRC4, cryptRC4
|
|
return nil
|
|
}
|
|
cf, _ := ToDict(resolved(enc, "CF", r))
|
|
pick := func(key Name) (cryptMethod, error) {
|
|
name, ok := ToName(resolved(enc, key, r))
|
|
if !ok || name == "Identity" {
|
|
return cryptNone, nil
|
|
}
|
|
f, ok := ToDict(resolved(cf, name, r))
|
|
if !ok {
|
|
return cryptNone, nil
|
|
}
|
|
switch m, _ := ToName(resolved(f, "CFM", r)); m {
|
|
case "V2":
|
|
return cryptRC4, nil
|
|
case "AESV2":
|
|
return cryptAESV2, nil
|
|
case "AESV3":
|
|
return cryptAESV3, nil
|
|
case "None":
|
|
return cryptNone, nil
|
|
default:
|
|
return cryptNone, fmt.Errorf("reader: unsupported crypt filter method /%s", m)
|
|
}
|
|
}
|
|
var err error
|
|
if dec.streams, err = pick("StmF"); err != nil {
|
|
return err
|
|
}
|
|
dec.strings, err = pick("StrF")
|
|
return err
|
|
}
|
|
|
|
// 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) (key []byte, asOwner bool, err error) {
|
|
for _, candidate := range legacyCandidates(password, owner, n, rev) {
|
|
k := legacyFileKey(candidate.padded, owner, id, perm, n, rev, metadata)
|
|
if legacyUserKeyMatches(k, user, id, rev) {
|
|
return k, candidate.owner, nil
|
|
}
|
|
}
|
|
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) []legacyCandidate {
|
|
out := []legacyCandidate{{padded: padPassword([]byte(password))}}
|
|
if password != "" {
|
|
out = append(out, legacyCandidate{padded: padPassword(nil)})
|
|
}
|
|
if u := userFromOwner([]byte(password), owner, n, rev); u != nil {
|
|
out = append(out, legacyCandidate{padded: u, owner: true})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// padPassword truncates or pads a password to the 32 bytes the algorithm wants.
|
|
func padPassword(p []byte) []byte {
|
|
out := make([]byte, 32)
|
|
k := copy(out, p)
|
|
copy(out[k:], pad)
|
|
return out
|
|
}
|
|
|
|
// legacyFileKey is algorithm 2: the file encryption key from a padded password.
|
|
func legacyFileKey(padded, owner, id []byte, perm int32, n, rev int, metadata bool) []byte {
|
|
h := md5.New()
|
|
h.Write(padded)
|
|
h.Write(owner)
|
|
h.Write([]byte{byte(perm), byte(perm >> 8), byte(perm >> 16), byte(perm >> 24)})
|
|
h.Write(id)
|
|
if rev >= 4 && !metadata {
|
|
h.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF})
|
|
}
|
|
key := h.Sum(nil)
|
|
if rev >= 3 {
|
|
for i := 0; i < 50; i++ {
|
|
sum := md5.Sum(key[:n])
|
|
key = sum[:]
|
|
}
|
|
}
|
|
return key[:n]
|
|
}
|
|
|
|
// legacyUserKeyMatches is algorithm 6: does this key reproduce /U?
|
|
func legacyUserKeyMatches(key, user, id []byte, rev int) bool {
|
|
if rev == 2 {
|
|
return bytes.Equal(rc4Bytes(key, pad), user)
|
|
}
|
|
h := md5.New()
|
|
h.Write(pad)
|
|
h.Write(id)
|
|
x := rc4Bytes(key, h.Sum(nil))
|
|
for i := 1; i <= 19; i++ {
|
|
x = rc4Bytes(xorKey(key, i), x)
|
|
}
|
|
return len(user) >= 16 && bytes.Equal(x, user[:16])
|
|
}
|
|
|
|
// userFromOwner is algorithm 7: recover the padded user password from /O using
|
|
// the owner password.
|
|
func userFromOwner(password, owner []byte, n, rev int) []byte {
|
|
if len(owner) < 32 {
|
|
return nil
|
|
}
|
|
sum := md5.Sum(padPassword(password))
|
|
key := sum[:]
|
|
if rev >= 3 {
|
|
for i := 0; i < 50; i++ {
|
|
s := md5.Sum(key)
|
|
key = s[:]
|
|
}
|
|
}
|
|
key = key[:n]
|
|
if rev == 2 {
|
|
return rc4Bytes(key, owner[:32])
|
|
}
|
|
x := owner[:32]
|
|
for i := 19; i >= 0; i-- {
|
|
x = rc4Bytes(xorKey(key, i), x)
|
|
}
|
|
return x
|
|
}
|
|
|
|
// xorKey returns key with every byte exclusive-ored with v, which is how both
|
|
// password algorithms walk their nineteen rounds.
|
|
func xorKey(key []byte, v int) []byte {
|
|
out := make([]byte, len(key))
|
|
for i := range key {
|
|
out[i] = key[i] ^ byte(v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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) (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, 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, false, nil
|
|
}
|
|
if len(owner) >= 48 {
|
|
if key := unlockR5(candidate, owner, ownerE, user[:48], rev); key != nil {
|
|
return key, true, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, false, ErrWrongPassword
|
|
}
|
|
|
|
// unlockR5 checks one password against a 48-byte /U or /O entry and, when it
|
|
// matches, unwraps the file key from the matching /UE or /OE.
|
|
func unlockR5(password, entry, wrapped, udata []byte, rev int) []byte {
|
|
valSalt, keySalt := entry[32:40], entry[40:48]
|
|
if !bytes.Equal(hash2B(password, valSalt, udata, rev), entry[:32]) {
|
|
return nil
|
|
}
|
|
if len(wrapped) < 32 {
|
|
return nil
|
|
}
|
|
inter := hash2B(password, keySalt, udata, rev)
|
|
// inter is always the 32 bytes hash2B returns, so the cipher cannot refuse it.
|
|
block, _ := aes.NewCipher(inter)
|
|
out := make([]byte, 32)
|
|
cipher.NewCBCDecrypter(block, make([]byte, aes.BlockSize)).CryptBlocks(out, wrapped[:32])
|
|
return out
|
|
}
|
|
|
|
// hash2B is algorithm 2.B: SHA-256 for revision 5, and for revision 6 the
|
|
// hardening loop that repeatedly encrypts and re-hashes.
|
|
func hash2B(password, salt, udata []byte, rev int) []byte {
|
|
h := sha256.New()
|
|
h.Write(password)
|
|
h.Write(salt)
|
|
h.Write(udata)
|
|
k := h.Sum(nil)
|
|
if rev < 6 {
|
|
return k
|
|
}
|
|
for round := 0; ; round++ {
|
|
var k1 []byte
|
|
one := append(append(append([]byte{}, password...), k...), udata...)
|
|
for i := 0; i < 64; i++ {
|
|
k1 = append(k1, one...)
|
|
}
|
|
// k is always 32 bytes, so a 16-byte key is always well formed.
|
|
block, _ := aes.NewCipher(k[:16])
|
|
e := make([]byte, len(k1)-len(k1)%aes.BlockSize)
|
|
cipher.NewCBCEncrypter(block, k[16:32]).CryptBlocks(e, k1[:len(e)])
|
|
sum := 0
|
|
for _, b := range e[:16] {
|
|
sum += int(b)
|
|
}
|
|
var next hash.Hash
|
|
switch sum % 3 {
|
|
case 0:
|
|
next = sha256.New()
|
|
case 1:
|
|
next = sha512.New384()
|
|
default:
|
|
next = sha512.New()
|
|
}
|
|
next.Write(e)
|
|
k = next.Sum(nil)
|
|
if round >= 63 && int(e[len(e)-1]) <= round-31 {
|
|
break
|
|
}
|
|
}
|
|
return k[:32]
|
|
}
|
|
|
|
// objectKey is the per-object key the pre-2.0 methods use; AESV3 uses the file
|
|
// key unchanged.
|
|
func (dec *decryptor) objectKey(num, gen int, method cryptMethod) []byte {
|
|
if method == cryptAESV3 {
|
|
return dec.key
|
|
}
|
|
h := md5.New()
|
|
h.Write(dec.key)
|
|
h.Write([]byte{byte(num), byte(num >> 8), byte(num >> 16), byte(gen), byte(gen >> 8)})
|
|
if method == cryptAESV2 {
|
|
h.Write([]byte{0x73, 0x41, 0x6C, 0x54}) // "sAlT"
|
|
}
|
|
k := h.Sum(nil)
|
|
if n := len(dec.key) + 5; n < 16 {
|
|
return k[:n]
|
|
}
|
|
return k
|
|
}
|
|
|
|
// decryptBytes undoes one method over one object's data.
|
|
func (dec *decryptor) decryptBytes(num, gen int, method cryptMethod, data []byte) []byte {
|
|
switch method {
|
|
case cryptNone:
|
|
return data
|
|
case cryptRC4:
|
|
return rc4Bytes(dec.objectKey(num, gen, method), data)
|
|
default:
|
|
return aesDecrypt(dec.objectKey(num, gen, method), data)
|
|
}
|
|
}
|
|
|
|
// rc4Bytes applies RC4, which is its own inverse. A key length the cipher
|
|
// refuses yields nil, which every caller reads as a password that does not
|
|
// fit — the derivations here never produce one.
|
|
func rc4Bytes(key, data []byte) []byte {
|
|
c, err := rc4.NewCipher(key)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
out := make([]byte, len(data))
|
|
c.XORKeyStream(out, data)
|
|
return out
|
|
}
|
|
|
|
// aesDecrypt undoes AES in CBC mode, the initialisation vector being the first
|
|
// block of the data and the padding the one CBC calls for.
|
|
func aesDecrypt(key, data []byte) []byte {
|
|
if len(data) < 2*aes.BlockSize || len(data)%aes.BlockSize != 0 {
|
|
return nil
|
|
}
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
out := make([]byte, len(data)-aes.BlockSize)
|
|
cipher.NewCBCDecrypter(block, data[:aes.BlockSize]).CryptBlocks(out, data[aes.BlockSize:])
|
|
n := int(out[len(out)-1])
|
|
if n < 1 || n > aes.BlockSize || n > len(out) {
|
|
return out
|
|
}
|
|
return out[:len(out)-n]
|
|
}
|
|
|
|
// decryptObject walks an object, decrypting every string in it and, for a
|
|
// stream, its data. The /Encrypt dictionary itself, and any cross-reference
|
|
// stream, are left alone: neither is encrypted.
|
|
func (dec *decryptor) decryptObject(num, gen int, o Object) Object {
|
|
if dec.skipKnown && num == dec.skipObj {
|
|
return o
|
|
}
|
|
return dec.walk(num, gen, o)
|
|
}
|
|
|
|
// walk is decryptObject's recursion.
|
|
func (dec *decryptor) walk(num, gen int, o Object) Object {
|
|
switch v := o.(type) {
|
|
case String:
|
|
return String(dec.decryptBytes(num, gen, dec.strings, v))
|
|
case Array:
|
|
for i, e := range v {
|
|
v[i] = dec.walk(num, gen, e)
|
|
}
|
|
return v
|
|
case Dict:
|
|
for k, e := range v {
|
|
v[k] = dec.walk(num, gen, e)
|
|
}
|
|
return v
|
|
case *Stream:
|
|
if t, ok := ToName(v.Dict.Get("Type")); ok && t == "XRef" {
|
|
return v
|
|
}
|
|
v.Dict, _ = ToDict(dec.walk(num, gen, v.Dict))
|
|
if streamIsPlain(v.Dict) {
|
|
return v
|
|
}
|
|
v.Raw = dec.decryptBytes(num, gen, dec.streams, v.Raw)
|
|
return v
|
|
}
|
|
return o
|
|
}
|
|
|
|
// streamIsPlain reports whether a stream's own filter chain says its bytes were
|
|
// left unencrypted: a leading /Crypt filter naming /Identity, which is what
|
|
// /Crypt with no /Name means too. Producers use it for the metadata stream of a
|
|
// file whose /EncryptMetadata is false, and decrypting such a stream turns
|
|
// readable XML into noise.
|
|
func streamIsPlain(d Dict) bool {
|
|
if first, ok := firstFilter(d); !ok || first != "Crypt" {
|
|
return false
|
|
}
|
|
name, ok := ToName(firstDecodeParms(d).Get("Name"))
|
|
return !ok || name == "Identity"
|
|
}
|
|
|
|
// resolved is a helper for reading an /Encrypt entry that may be indirect.
|
|
func resolved(d Dict, key Name, r Resolver) Object {
|
|
o, err := Resolve(d.Get(key), r)
|
|
if err != nil {
|
|
return Null{}
|
|
}
|
|
return o
|
|
}
|
|
|
|
// intOf reads an integer with a default.
|
|
func intOf(o Object, def int64) int64 {
|
|
if n, ok := ToInt(o); ok {
|
|
return n
|
|
}
|
|
return def
|
|
}
|