require 'openssl' require 'json' require 'base64' require 'pp' require_relative '../../cli/scripting' # see some ramblings here: http://distributed-frostbite.blogspot.com/2010/06/file-encryption-in-ruby-with-openssl.html def run! # this will run only if called from command line (not when require'd nor load'd) include Eco::CLI::Scripting include Eco::Data::Crypto # script arguments keygen = get_arg("-keygen") filename = get_arg("-keygen", with_param: true) ssh = get_arg("-ssh") length = get_arg("-length", with_param: true) check_rsa_pairs = get_arg("-keypairs") key_filename = get_arg("-keypairs", with_param: true) # methods case true when keygen puts "++ keygen RSA ++" #pp ::OpenSSL.constants.sort cr = Eco::Data::Crypto::OpenSSL.new case true when ssh puts "think about installing this library: https://rubygems.org/gems/sshkey" puts "GitHub: https://github.com/bensie/sshkey" puts "or add this Ruby solution to convert to ssh-rsa: https://stackoverflow.com/a/3162593/4352306" exit(1) cr.rsa_keygen_ssh(filename: filename) else cr.rsa_keygen(filename: filename) end when check_rsa_pairs puts "++ checking file pairs ++" begin key_filename = "rsa" if !key_filename priv = File.read(key_filename + '.pem') pub = File.read(key_filename + '.pub') rescue puts "could not find files with basename: #{key_filename}" exit(1) end cr = Eco::Data::Crypto::OpenSSL.new equals = cr.rsa_keypairs?(priv, pub) if equals puts "the pair #{key_filename} seem to be for one to another!" else puts "those files with base name #{key_filename} are not a rsa pair" end end end module Eco module Data #TODO study how to encrypt uniquely the api key in the configuration file #TODO study how RSA could boost team-shared Drive folder (updates, and people files should be encrypted via public key) module Crypto # file conf definitions THIS_FILENAME = __FILE__ MY_NAME = File.basename(THIS_FILENAME, File.extname(THIS_FILENAME)) # ".rb" PATH = File.dirname(THIS_FILENAME) WORKING_DIR = PATH + "/" + MY_NAME PARSER_FILE = WORKING_DIR + "/json_parser.json" # .dll class OpenSSL DEFAULT_KEY_LENGTH = 256 DEFAULT_RSA_LENGTH = 2048 SALT_LENGTH = 128 SALT_SEED = "$%!@_halt9000" BLOCK_OCTETS = 512 attr_accessor :salt_seed attr_reader :digest, :pkcs5, :kdf, :cipher, :rsa attr_reader :salt def initialize(init = {}) @digest = ::OpenSSL::Digest @pkcs5 = ::OpenSSL::PKCS5 #@kdf = ::OpenSSL::KDF @cipher = ::OpenSSL::Cipher self.salt_seed = SALT_SEED @rsa = ::OpenSSL::PKey::RSA end def salt_seed=(value) @salt_seed = value @salt = self.pbkdf2(@salt_seed, salt: @salt_seed, length: SALT_LENGTH) end def sha256 (str = nil) sha = @digest.SHA256.new digest = sha.digest(str) unless !str sha.reset digest end def sha512 (str = nil) sha = @digest.SHA512.new digest = sha.digest(str) unless !str sha.reset digest end # default hmac hash to "sha256" -> https://github.com/hueniverse/iron/issues/55 def pbkdf2 (pass, salt: @salt || "salt", iterations: 1000, length: DEFAULT_KEY_LENGTH, hash: "sha256") octets = length.div(8) #puts "this has been called" #puts "pass: #{pass}" #puts "salt: #{salt}" #puts "iterations: #{iterations}" #puts "length: #{length}" #puts "hash: #{hash}" @pkcs5.pbkdf2_hmac(pass, salt, iterations, octets, hash) end def pbkdf2_sha1 (pass, salt: @salt, iterations: 1000, length: DEFAULT_KEY_LENGTH) #self.pbkdf2(pass, salt: salt, iterations: iternations, length: length, hash: "sha1") @pkcs5.pbkdf2_hmac(pass, salt, iterations, octets, "sha1") end =begin def scrypt (pass, salt: @salt, cost: 14, length: DEFAULT_KEY_LENGTH) block_size = 8 parallelization = 1 octets = length.div(8) @kdf.scrypt(pass, salt: salt, N: 2**cost, r: block_size, p: parallelization, length: octets) end =end def aes256_pass(pass) self.pbkdf2(pass, length: 256) end def aes256_encrypt(data, key: nil, iv: nil, block_octets: BLOCK_OCTETS) block_bits = block_bits * 8 cipher = @cipher.new('aes-256-cbc') cipher.encrypt if key ; cipher.key = key else ; key = cipher.random_key end if iv ; cipher.iv = iv else ; iv = cipher.random_iv end str_c = "" while str.length > 0 str_c += cipher.update(str.slice!(0, block_bits)) end str_c += cipher.final return str_c #EncryptedData.new({content: str_c, key: key, iv: iv}) end def aes256_decrypt(data, key: , iv: , block_octets: BLOCK_OCTETS) block_bits = block_bits * 8 #validation = encrypted_data && encrypted_data.key && encrypted_data.iv # return nil unless validation cipher = @cipher.new('aes-256-cbc') cipher.decrypt cipher.key = key cipher.iv = iv #cipher.key = encrypted_data.key #cipher.iv = encrypted_data.iv #str_c = encrypted_data.content str_c = data str = "" while str_c.length > 0 str += cipher.update(str_c.slice!(0, block_bits)) #puts str[-50..-1] || str end str += cipher.final return str end def rsa_keygen(length = DEFAULT_RSA_LENGTH, filename: "rsa") filename = "rsa" if !filename key = @rsa.new(length) File.open('./' + filename + '.pem',"w") {|fd| fd << key.to_pem } File.open('./' + filename + '.pub',"w") {|fd| fd << key.public_key.to_pem } end def rsa_keygen_ssh(length = DEFAULT_RSA_LENGTH, filename: "rsa") # to do conersion pem to ssh-rsa: https://stackoverflow.com/a/3162593/4352306 # conversion explained here: http://blog.oddbit.com/2011/05/08/converting-openssh-public-keys/ # much simpler developed in php here: https://stackoverflow.com/a/5524323/4352306 filename = "rsa" if !filename key = @rsa.new(length) File.open('./' + filename + '.der',"w") {|fd| fd << key.ssh_type } File.open('./' + filename + '.pub',"w") {|fd| fd << key.public_key.ssh_type } end def rsa_keypairs?(private_key, public_key) str_original = "See, my mule don’t like people laughing" pub_key = @rsa.new(public_key) str_original.force_encoding(Encoding::UTF_8) encrypted_data = Base64.encode64(pub_key.public_encrypt(str_original)) priv_key = @rsa.new(private_key) begin # see here: https://stackoverflow.com/a/13251160/4352306 (padding error when false pub key) decrypted_data = priv_key.private_decrypt(Base64.decode64(encrypted_data)) # OpenSSL core Ruby library is written in C which uses different encoding (ASCII-8BIT) # that's why we force UTF-8 encoding (before and after encryption) # see this answer: https://stackoverflow.com/a/27326915/4352306 decrypted_data.force_encoding(Encoding::UTF_8) rescue return false end puts "encrypted string: #{str_original}" puts "decrypted string: #{decrypted_data}" return (decrypted_data == str_original) end end class EncryptedData attr_reader :key, :iv, :content def initialize(init = {}) @key = init[:key] @iv = init[:iv] @content = init[:content] @sha = sha256(@content) load() if !@key || !@iv end def load (file = nil) parser = get_parser(file) sha64 = Base64.encode64(@sha) if @sha if parser.key?(sha64) #puts "found key!" meta = parser[sha64] key64, iv64 = meta.values_at("key", "iv") @key = Base64.decode64(key64) if key64 @iv = Base64.decode64(iv64) if iv64 else #puts "key not found! #{sha64}" end end def save (file = nil) parser = get_parser(file) sha64 = Base64.encode64(@sha) if @sha #puts "save key: #{sha64}" if parser.key?(sha64) meta = parser[sha64] else meta = {key: nil, iv: nil} parser[sha64] = meta end meta[:key] = Base64.encode64(@key) if @key meta[:iv] = Base64.encode64(@iv) if @iv set_parser(file, parser) end private def get_parser (file = nil) file = PARSER_FILE if !file # create the directory unless it exists Dir.mkdir(WORKING_DIR) unless Dir.exist?(WORKING_DIR) parser = {} if File.file?(file) fd = File.open(file,"r") parser = JSON.load(fd) fd.close end return parser end def set_parser (file = nil, parser = {}) file = PARSER_FILE if !file # create the directory unless it exists Dir.mkdir(WORKING_DIR) unless Dir.exist?(WORKING_DIR) fd = File.open(file,"w") fd << parser.to_json fd.close end def sha256 (str = nil) sha = OpenSSL::Digest::SHA256.new digest = sha.digest(str) unless !str sha.reset digest end end def self.encrypt (str) cipher = OpenSSL::Cipher.new('aes-256-cbc') cipher.encrypt key = cipher.random_key iv = cipher.random_iv str_c = "" while str.length > 0 str_c += cipher.update(str.slice!(0, 4096)) end str_c += cipher.final EncryptedData.new({content: str_c, key: key, iv: iv}) end def self.decrypt (encrypted_data) unless encrypted_data && encrypted_data.key && encrypted_data.iv return nil end cipher = OpenSSL::Cipher.new('aes-256-cbc') cipher.decrypt cipher.key = encrypted_data.key cipher.iv = encrypted_data.iv str_c = encrypted_data.content str = "" while str_c.length > 0 str += cipher.update(str_c.slice!(0, 4096)) #puts str[-50..-1] || str end str += cipher.final return str end end =begin class Includer include Crypto end CRYPTO = Includer.new =end end end run! if __FILE__==$0