v2.2.0
Breaking changes
PdfDictionary.Set,TryGetandGetnow throwArgumentNullExceptionfor anullkey.
PreviouslyTryGet(null, out _)returnedfalse,Get(null)returnednull, and
Set(null, value)appended an entry that only failed later — with aNullReferenceExceptionout
ofWriteTo, once the dictionary was serialised. All three are Stable API, which is why this is
recorded here rather than under Security, where the rest of this fix lives: without the guard, a
nullkey would behave differently depending on which side of the internal indexing threshold a
dictionary sits — returningfalsebelow it, throwing above it — exactly the property that
threshold is supposed to be free to move without changing what callers observe. (#208)
Security
-
OwnerPassword = ""beside a realUserPasswordproduced a file with no real password
protection at all, in every release from v1.0.0 through v2.1.0.??treats an empty string as
a value, not as "unset", so that combination sealed/Oand/OE(ISO 32000-2 §7.6.4.4.6,
Algorithm 9) under the empty password instead of falling back to the user password. At/R6 an
empty password fails the/Ucheck and satisfies the/Ocheck, so any conforming reader lands
on owner access when no password is supplied. That is a property of the file itself, not of the
order a particular reader tries/Uand/Oin. Verified concretely: a file written by v2.1.0
withUserPassword = "hunter2", OwnerPassword = ""opens with no password supplied at all,
and the catalog, hence the whole object graph, decrypts. The document's confidentiality is gone,
not merely its permission flags: nothing enforcedPermissions, but nothing enforced
UserPasswordeither.OwnerPassword = ""beside a non-emptyUserPasswordnow throws instead
of producing a file. Both passwords empty is unchanged, since an unprotected document is
legitimate and ISO 32000-2 permits an empty owner password.OwnerPassword = nullis unchanged
too: that is the documented fallback to the user password as owner, and it does not reproduce
this defect (/Ois sealed under the real user password, not the empty string) — though anyone
who can open the document still holds owner access under it, soPermissionsstill binds nobody.
The guard sits inStandardSecurityHandler's constructor, which is Stable API and callable
directly, and inPdfDocument.Encrypt, so the failure surfaces at the call site rather than at
Save();VellumPdf.Layout.Document.Encryptinherits it by delegation. (EncryptionSetup.TryAuthenticate,
this library's own authentication order, is why our reader reports such a file's access level as
owner rather than user — that explains what we report, not why the exposure exists.)A document already written with this shape cannot be fixed in place: correcting it means
re-deriving/Oand/OE, which needs a real owner password behind them from the start. Affected
documents must be re-encrypted from the original plaintext, with a distinctOwnerPasswordthis
time. (#211) -
PdfDictionarylookup is no longer quadratic in the key count.SetandTryGetnow build a
hash index once a dictionary passes 16 entries, rather than scanning the whole entry list on every
call. The/Encryptdictionary is parsed, and copied again byEncryptionSetup.DereferenceValues,
before any password is checked, on a file anyone can send, so a hostile document naming tens of
thousands of keys there previously cost time quadratic in that count with nothing to show for it:
opening a fixture with an 80,000-key/Encryptdictionary took about 27 seconds before this fix and
well under a second after.EncryptionSetup's/CFcap (MaxCryptFilters, still 64) keeps its
comment but loses the reason it used to give: every term that touches/Encryptis linear now, so
the cap no longer bears any of the weight of bounding this cost. It stays because a real document
names one or two crypt filters, not sixty-four. (#208)