Tax day here in the US is Monday, April 18th this year. As an adult who also owns and runs a business, I let a professional handle my taxes each year. They do a better job thank I and—frankly—they seem to enjoy it far more than I ever will.
When my taxes are ready, they email me a password-protected PDF and ask me to sign it. I love that they are taking my security serious-ish. However, macOS' Preview app won't let me drop an optical signature on the PDF because they also set an owner password to keep me from mucking about with their carefully crafted figures.
Luckily, I figured out a way to use commonly-available open source tooling—vis-a-vis Ghostscript,—to strip these portable documents of their passwords and allow me to sign them.
Removing Passwords
To open the file protected.pdf
, protected with the password sekrit, and write an unprotected version to unprotected.pdf
, use this:
gs -q -dNOPAUSE -dBATCH \
-sDEVICE=pdfwrite -sPDFPassword=sekrit \
-sOutputFile=unprotected.pdf protected.pdf
The -q
silences some of the additional output. Drop it to see what you're missing!
The -dBATCH
and -dNOPAUSE
are pretty standard for command-line work. The former causes gs
to exit after the last file is processed (rather than awaiting additional commands from standard input). The latter keeps it from pausing after each page. The -sFOO=bar
flags set processing instructions; the first defines the password for reading the file. The second sets the path to write the unprotected PDF to.
If, like me, you hate typing things, you can combine three of the above flags into a single flag: -o
:
gs -q \
-sDEVICE=pdfwrite -sPDFPassword=sekrit \
-o unprotected.pdf protected.pdf
This handy little flag signals to Ghostscript that you are operating in non-interactive mode, so it tuns on the NOPAUSE
and BATCH
options for you, and uses the value you give to -o
as the OutputFile
processing parameter. That's what I call value.
Adding Passwords
You may find yourself starting with an unprotected document and want to add a password, either for the implicit encryption that gives you (hello, email!) or for more advanced content control:
gs -q -sDEVICE=pdfwrite \
-sOwnerPassword=open-sesame \
-sUserPassword=sekrit \
-o locked.pdf unprotected.pdf
This gives us a password-protected PDF, but once the user password has been supplied (and we can read the document) we find that we have the ability to modify it, print it, etc. If that's what you want, great! Otherwise, we can avail ourselves of the permissions system that Adobe built into PDF 1.3 and beyond.
Permissions are set using a 32-bit number, signed and in two's-complement format. The two least significant bits are required to be 0, and the highest 20 bits must be set to 1, as must bits 7 and 8.
1111 1111 1111 1111 1111 .... 11.. ..00
Those unset 8 bits encode permissions, where 1 means "granted" and 0 means "withheld." The full list can be found in the PDF 1.7 reference, in Table 3.20, pp123-124. For illustration, let's remove all permissions except "screen display":
1111 1111 1111 1111 1111 0000 1100 0000
This translates to the hexadecimal 32-bit integer 0xfffff0c0
—each block of 1111
is an f
, each 0000
is 0x0
, and 1100
is c
($8 + 4 = 12$). In decimal (unsigned integer), that's 4294963392
, but since this is a two's-complement signed integer (per Adobe, and they oughta know), it's actually -3904
.
gs -q -sDEVICE=pdfwrite \
-sOwnerPassword=open-sesame -sUserPassword=sekrit \
-dEncryptionR=3 -dPermissions=-3904 \
-o locked.pdf test.pdf
(Note: the reason we used -dPermissions
instead of -sPermissions
is because the permissions flag is numeric, and we did not want Ghostscript to set our permissions to the 5 UTF-8 octets -
, 3
, 9
, 0
, and 4
.)
Here's what locked.pdf
looks like when you open it up in Preview on a Mac:
After supplying the user password of sekrit
, we can see the contents of the PDF (a lovely mandala in the example document I was using):
As soon as we try to modify the document, for example, to draw on it with the pen:
At this point we must specify the owner password, since we're trying to modify the document in contravention with the permissions (i.e. no changes allowed) set in the document.
And finally, as the owner, we can make changes.
Neat, eh?
Looking for More?
If you're interested in diving a bit deeper, here are the resources I found useful while putting this essay together:
Section 3.5.2 of the PDF 1.7 reference covers the Standard Security Header, which defines the owner/user passwords, access permissions that can be restricted to one or both parties, and more. It starts on p120, about halfway down the page. Interestingly, it has this admonition against placing full faith in these "access permissions":
Note: Once the document has been opened and decrypted successfully, the application has access to the entire contents of the document. There is nothing inherent in PDF encryption that enforces the document permissions specified in the encryption dictionary. It is up to the implementors of PDF consumer applications to respect the intent of the document creator by restricting user access to an encrypted PDF file according to the permissions contained in the file.
I found this write-up (original here) on PDF 1.3+ and the Standard Security Handler. I found it enlightening.
While chasing down some printed reference on PDF (ironic, I know), I came across John Whitington, founder of Coherent Graphics in the UK. He wrote a piece of software that I have yet to explore, called Coherent PDF, and its manual is available online (in PDF of course). He also wrote A Machine Made This Book which also looks interesting.
For Ghostscript in particular, I leaned pretty heavily on the High Level Output Devices reference documentation, specifically section 6 for PDF files (i.e. PDFWRITE
). You can also find this document in the Ghostscript source distribution, should their docs site go missing in the next few decades.
If you'd like some simple PDF documents to play with permissions and password addition / removal on, here's an unprotected.pdf, and its protected.pdf counterpart. The owner and user passwords match the arguments used in the shell examples above: owner is open-sesame
and user is sekrit
.