P5a plan: PDF documents — seven Ornith tasks with acceptance tests first
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>
This commit is contained in:
+572
@@ -0,0 +1,572 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user