module Prawn::Document::Security
Implements PDF
encryption (password protection and permissions) as specified in the PDF
Reference, version 1.3, section 3.5 “Encryption”.
Constants
- FULL_PERMISSIONS
- PASSWORD_PADDING
- PERMISSIONS_BITS
Flags in the permissions word, numbered as LSB = 1
Public Class Methods
Encrypts the given string under the given key, also requiring the object ID and generation number of the reference. See Algorithm 3.1.
# File lib/prawn/security.rb, line 110 def self.encrypt_string(str, key, id, gen) # Convert ID and Gen number into little-endian truncated byte strings id = [id].pack('V')[0, 3] gen = [gen].pack('V')[0, 2] extended_key = "#{key}#{id}#{gen}" # Compute the RC4 key from the extended key and perform the encryption rc4_key = Digest::MD5.digest(extended_key)[0, 10] Arcfour.new(rc4_key).encrypt(str) end
Public Instance Methods
Encrypts the document, to protect confidential data or control modifications to the document. The encryption algorithm used is detailed in the PDF
Reference 1.3, section 3.5 “Encryption”, and it is implemented by all major PDF
readers.
options
can contain the following:
:user_password
-
Password required to open the document. If this is omitted or empty, no password will be required. The document will still be encrypted, but anyone can read it.
:owner_password
-
Password required to make modifications to the document or change or override its permissions. If this is set to
:random
, a random password will be used; this can be useful if you never want users to be able to override the document permissions. :permissions
-
A hash mapping permission symbols (see below) to
true
orfalse
. True means “permitted”, and false means “not permitted”. All permissions default totrue
.
The following permissions can be specified:
:print_document
-
Print document.
:modify_contents
-
Modify contents of document (other than text annotations and interactive form fields).
:copy_contents
-
Copy text and graphics from document.
:modify_annotations
-
Add or modify text annotations and interactive form fields.
Examples¶ ↑
Deny printing to everyone, but allow anyone to open without a password:
encrypt_document :permissions => { :print_document => false }, :owner_password => :random
Set a user and owner password on the document, with full permissions for both the user and the owner:
encrypt_document :user_password => 'foo', :owner_password => 'bar'
Set no passwords, grant all permissions (This is useful because the default in some readers, if no permissions are specified, is “deny”):
encrypt_document
Caveats¶ ↑
-
The encryption used is weak; the key is password-derived and is limited to 40 bits, due to US export controls in effect at the time the
PDF
standard was written. -
There is nothing technologically requiring
PDF
readers to respect the permissions embedded in a document. ManyPDF
readers do not. -
In short, you have no security at all against a moderately motivated person. Don’t use this for anything super-serious. This is not a limitation of
Prawn
, but is rather a built-in limitation of thePDF
format.
# File lib/prawn/security.rb, line 88 def encrypt_document(options = {}) Prawn.verify_options %i[user_password owner_password permissions], options @user_password = options.delete(:user_password) || '' @owner_password = options.delete(:owner_password) || @user_password if @owner_password == :random # Generate a completely ridiculous password @owner_password = (1..32).map { rand(256) }.pack('c*') end self.permissions = options.delete(:permissions) || {} # Shove the necessary entries in the trailer and enable encryption. state.trailer[:Encrypt] = encryption_dictionary state.encrypt = true state.encryption_key = user_encryption_key end
Private Instance Methods
Provides the values for the trailer encryption dictionary.
# File lib/prawn/security.rb, line 124 def encryption_dictionary { Filter: :Standard, # default PDF security handler V: 1, # "Algorithm 3.1", PDF reference 1.3 R: 2, # Revision 2 of the algorithm O: PDF::Core::ByteString.new(owner_password_hash), U: PDF::Core::ByteString.new(user_password_hash), P: permissions_value } end
The O (owner) value in the encryption dictionary. Algorithm 3.3.
# File lib/prawn/security.rb, line 195 def owner_password_hash @owner_password_hash ||= begin key = Digest::MD5.digest(pad_password(@owner_password))[0, 5] Arcfour.new(key).encrypt(pad_password(@user_password)) end end
Pads or truncates a password to 32 bytes as per Alg 3.2.
# File lib/prawn/security.rb, line 178 def pad_password(password) password = password[0, 32] password + PASSWORD_PADDING[0, 32 - password.length] end
# File lib/prawn/security.rb, line 147 def permissions=(perms = {}) @permissions ||= FULL_PERMISSIONS perms.each do |key, value| unless PERMISSIONS_BITS[key] raise( ArgumentError, "Unknown permission :#{key}. Valid options: " + PERMISSIONS_BITS.keys.map(&:inspect).join(', ') ) end # 0-based bit number, from LSB bit_position = PERMISSIONS_BITS[key] - 1 if value # set bit @permissions |= (1 << bit_position) else # clear bit @permissions &= ~(1 << bit_position) end end end
# File lib/prawn/security.rb, line 169 def permissions_value @permissions || FULL_PERMISSIONS end
# File lib/prawn/security.rb, line 183 def user_encryption_key @user_encryption_key ||= begin md5 = Digest::MD5.new md5 << pad_password(@user_password) md5 << owner_password_hash md5 << [permissions_value].pack('V') md5.digest[0, 5] end end
The U (user) value in the encryption dictionary. Algorithm 3.4.
# File lib/prawn/security.rb, line 204 def user_password_hash Arcfour.new(user_encryption_key).encrypt(PASSWORD_PADDING) end