# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2019 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'securerandom' require 'hexapdf/error' module HexaPDF module Encryption # Common interface for AES algorithms # # This module defines the common interface that is used by the security handlers to encrypt or # decrypt data with AES. It has to be *prepended* by any AES algorithm class. # # See the ClassMethods module for available class level methods of AES algorithms. # # == Implementing an AES Class # # An AES class needs to define at least the following methods: # # initialize(key, iv, mode):: # Initializes the AES algorithm with the given key and initialization vector. The mode # determines how the AES algorithm object works: If the mode is :encrypt, the object # encrypts the data, if the mode is :decrypt, the object decrypts the data. # # process(data):: # Processes the data and returns the encrypted/decrypted data. The method can assume that # the passed in data always has a length that is a multiple of BLOCK_SIZE. module AES # Valid AES key lengths VALID_KEY_LENGTH = [16, 24, 32].freeze # The AES block size BLOCK_SIZE = 16 # Convenience methods for decryption and encryption that operate according to the PDF # specification. # # These methods will be available on the class object that prepends the AES module. module ClassMethods # Encrypts the given +data+ using the +key+ and a randomly generated initialization # vector. # # The data is padded using the PKCS#5 padding scheme and the initialization vector is # prepended to the encrypted data, # # See: PDF1.7 s7.6.2. def encrypt(key, data) iv = random_bytes(BLOCK_SIZE) iv << new(key, iv, :encrypt).process(pad(data)) end # Returns a Fiber object that encrypts the data from the given source fiber with the # +key+. # # Padding and the initialization vector are handled like in #encrypt. def encryption_fiber(key, source) Fiber.new do data = random_bytes(BLOCK_SIZE) algorithm = new(key, data, :encrypt) Fiber.yield(data) data = ''.b while source.alive? && (new_data = source.resume) data << new_data next if data.length < BLOCK_SIZE new_data = data.slice!(0, data.length - data.length % BLOCK_SIZE) Fiber.yield(algorithm.process(new_data)) end algorithm.process(pad(data)) end end # Decrypts the given +data+ using the +key+. # # It is assumed that the initialization vector is included in the first BLOCK_SIZE bytes # of the data. After the decryption the PKCS#5 padding is removed. # # See: PDF1.7 s7.6.2. def decrypt(key, data) if data.length % BLOCK_SIZE != 0 || data.length < 2 * BLOCK_SIZE raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes" end unpad(new(key, data.slice!(0, BLOCK_SIZE), :decrypt).process(data)) end # Returns a Fiber object that decrypts the data from the given source fiber with the # +key+. # # Padding and the initialization vector are handled like in #decrypt. def decryption_fiber(key, source) Fiber.new do data = ''.b while data.length < BLOCK_SIZE && source.alive? && (new_data = source.resume) data << new_data end algorithm = new(key, data.slice!(0, BLOCK_SIZE), :decrypt) while source.alive? && (new_data = source.resume) data << new_data next if data.length < 2 * BLOCK_SIZE new_data = data.slice!(0, data.length - BLOCK_SIZE - data.length % BLOCK_SIZE) Fiber.yield(algorithm.process(new_data)) end if data.length < BLOCK_SIZE || data.length % BLOCK_SIZE != 0 raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes" end unpad(algorithm.process(data)) end end # Returns a string of n random bytes. # # The specific AES algorithm class can override this class method to provide another # method for generating random bytes. def random_bytes(n) SecureRandom.random_bytes(n) end private # Pads the data to a muliple of BLOCK_SIZE using the PKCS#5 padding scheme and returns the # result. # # See: PDF1.7 s7.6.2 def pad(data) padding_length = BLOCK_SIZE - data.size % BLOCK_SIZE data + padding_length.chr * padding_length end # Removes the padding from the data according to the PKCS#5 padding scheme and returns the # result. # # In case the padding is not correct as per the specification, it is assumed that there is # no padding and the input is returned as is. # # See: PDF1.7 s7.6.2 def unpad(data) padding_length = data.getbyte(-1) if padding_length > BLOCK_SIZE || padding_length == 0 || data[-padding_length, padding_length].each_byte.any? {|byte| byte != padding_length } data else data[0...-padding_length] end end end # Automatically extends the klass with the necessary class level methods. def self.prepended(klass) # :nodoc: klass.extend(ClassMethods) end # Creates a new AES object using the given encryption key and initialization vector. # # The mode must either be :encrypt or :decrypt. # # Classes prepending this module have to have their own initialization method as this method # just performs basic checks. def initialize(key, iv, mode) unless VALID_KEY_LENGTH.include?(key.length) raise HexaPDF::EncryptionError, "AES key length must be 128, 192 or 256 bit" end unless iv.length == BLOCK_SIZE raise HexaPDF::EncryptionError, "AES initialization vector length must be 128 bit" end mode = mode.intern super end end end end