module EncryptedStrings # Indicates no public key was found class NoPublicKeyError < StandardError end # Indicates no private key was found class NoPrivateKeyError < StandardError end # Encryption in which the keys used to encrypt/decrypt come in pairs. Also # known as public key encryption. Anything that's encrypted using the # public key can only be decrypted with the same algorithm and a matching # private key. Any message that is encrypted with the private key can only # be decrypted with the matching public key. # # Source: http://support.microsoft.com/kb/246071 # # == Encrypting # # To encrypt a string using an asymmetric cipher, the location of the # public key file must be specified. You can define the default for this # value like so: # # EncryptedStrings::AsymmetricCipher.default_public_key_file = './public.key' # # If these configuration options are not passed in to #encrypt, then the # default values will be used. You can override the default values like so: # # password = 'shhhh' # password.encrypt(:asymmetric, :public_key_file => './encrypted_public.key') # => "INy95irZ8AlHmvc6ZAF/ARsTpbqPIB/4bEAKKOebjsayB7NYWtIzpswvzxqf\nNJ5yyuvxfMODrcg7RimEMFkFlg==\n" # # An exception will be raised if either the public key file could not be # found or the key could not decrypt the public key file. # # == Decrypting # # To decrypt a string using an asymmetric cipher, the location of the # private key file must be specified. If this file is itself encrypted, you # must also specify the algorithm and password used to seed the symmetric # algorithm that will decrypt the plublic key file. You can define defaults # for these values like so: # # EncryptedStrings::AsymmetricCipher.default_private_key_file = './private.key' # EncryptedStrings::SymmetricCipher.default_algorithm = 'DES-EDE3-CBC' # EncryptedStrings::SymmetricCipher.default_password = 'secret' # # If these configuration options are not passed in to #decrypt, then the # default values will be used. You can override the default values like so: # # password = "INy95irZ8AlHmvc6ZAF/ARsTpbqPIB/4bEAKKOebjsayB7NYWtIzpswvzxqf\nNJ5yyuvxfMODrcg7RimEMFkFlg==\n" # password.decrypt(:asymmetric, :public_key_file => './encrypted_public.key', :password => 'secret') # => "shhhh" # # An exception will be raised if either the private key file could not be # found or the password could not decrypt the private key file. class AsymmetricCipher < Cipher class << self # The default private key to use during encryption. Default is nil. attr_accessor :default_private_key_file # The default public key to use during encryption. Default is nil. attr_accessor :default_public_key_file end # Private key used for decrypting data attr_reader :private_key_file # Public key used for encrypting data attr_reader :public_key_file # The algorithm to use if the key files are encrypted themselves attr_accessor :algorithm # The password used during symmetric decryption of the key files attr_accessor :password # Creates a new cipher that uses an asymmetric encryption strategy. # # Configuration options: # * :private_key_file - Encrypted private key file # * :public_key_file - Public key file # * :password - The password to use in the symmetric cipher # * :algorithm - Algorithm to use symmetrically encrypted strings def initialize(options = {}) invalid_options = options.keys - [:private_key_file, :public_key_file, :algorithm, :password] raise ArgumentError, "Unknown key(s): #{invalid_options.join(", ")}" unless invalid_options.empty? options = { :private_key_file => AsymmetricCipher.default_private_key_file, :public_key_file => AsymmetricCipher.default_public_key_file }.merge(options) @public_key = @private_key = nil self.private_key_file = options[:private_key_file] self.public_key_file = options[:public_key_file] raise ArgumentError, 'At least one key file must be specified (:private_key_file or :public_key_file)' unless private_key_file || public_key_file self.algorithm = options[:algorithm] self.password = options[:password] super() end # Encrypts the given data. If no public key file has been specified, then # a NoPublicKeyError will be raised. def encrypt(data) raise NoPublicKeyError, "Public key file: #{public_key_file}" unless public? encrypted_data = public_rsa.public_encrypt(data) [encrypted_data].pack('m') end # Decrypts the given data. If no private key file has been specified, then # a NoPrivateKeyError will be raised. def decrypt(data) raise NoPrivateKeyError, "Private key file: #{private_key_file}" unless private? decrypted_data = data.unpack('m')[0] private_rsa.private_decrypt(decrypted_data) end # Sets the location of the private key and loads it def private_key_file=(file) @private_key_file = file and load_private_key end # Sets the location of the public key and loads it def public_key_file=(file) @public_key_file = file and load_public_key end # Does this cipher have a public key available? def public? return true if @public_key load_public_key !@public_key.nil? end # Does this cipher have a private key available? def private? return true if @private_key load_private_key !@private_key.nil? end private # Loads the private key from the configured file def load_private_key @private_rsa = nil if private_key_file && File.file?(private_key_file) @private_key = File.read(private_key_file) end end # Loads the public key from the configured file def load_public_key @public_rsa = nil if public_key_file && File.file?(public_key_file) @public_key = File.read(public_key_file) end end # Retrieves the private RSA from the private key def private_rsa if password options = {:password => password} options[:algorithm] = algorithm if algorithm private_key = @private_key.decrypt(:symmetric, options) OpenSSL::PKey::RSA.new(private_key) else @private_rsa ||= OpenSSL::PKey::RSA.new(@private_key) end end # Retrieves the public RSA def public_rsa @public_rsa ||= OpenSSL::PKey::RSA.new(@public_key) end end end