-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
848 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
require_relative './core_extensions/string/binary_hex' | ||
|
||
module CoreExtensions | ||
def self.load | ||
::String.include CoreExtensions::String::BinaryHex | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
module CoreExtensions | ||
module String | ||
module BinaryHex | ||
def bth | ||
unpack('H*').first | ||
end | ||
|
||
def htb | ||
Array(self).pack('H*') | ||
end | ||
|
||
def force_binary | ||
return htb if match(/^[0-9A-F]+$/i).is_a? MatchData | ||
return self if bth.match(/^[0-9A-F]+$/i).is_a? MatchData | ||
raise ArgumentError, 'Invalid encoding, hex or binary' | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
require_relative '../core_extensions.rb' | ||
CoreExtensions.load | ||
|
||
module Mifiel | ||
module Crypto | ||
autoload :PBE, 'mifiel/crypto/pbe' | ||
autoload :Response, 'mifiel/crypto/response' | ||
autoload :AES, 'mifiel/crypto/aes' | ||
autoload :ECIES, 'mifiel/crypto/ecies' | ||
autoload :PKCS5, 'mifiel/crypto/pkcs5' | ||
|
||
def self.decrypt(asn1, pass) | ||
pkcs5 = Mifiel::Crypto::PKCS5.parse(asn1.force_binary) | ||
params = pkcs5.values | ||
params[:data] = params[:cipher_text] | ||
params[:key] = | ||
Mifiel::Crypto::PBE.derive_key({ password: pass }.merge(params.slice(:salt, :iterations, :key_size))) | ||
Mifiel::Crypto::AES.decrypt(params.slice(:key, :data, :iv, :cipher)) | ||
end | ||
|
||
def self.encrypt(document, password) | ||
params = { | ||
salt: Mifiel::Crypto::PBE.random_salt, | ||
iterations: Mifiel::Crypto::PBE::ITERATIONS, | ||
password: password | ||
} | ||
params[:key] = Mifiel::Crypto::PBE.derive_key(params) | ||
params[:iv] = Mifiel::Crypto::AES.random_iv | ||
params[:data] = document | ||
params[:cipher_text] = Mifiel::Crypto::AES.encrypt(params.slice(:key, :iv, :data)) | ||
Mifiel::Crypto::PKCS5.new(params.slice(:salt, :iv, :iterations, :cipher_text)) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
require 'openssl' | ||
module Mifiel | ||
module Crypto | ||
class AES | ||
CIPHER = 256 | ||
CIPHERS = { 'AES-128-CBC' => 128, 'AES-192-CBC' => 192, 'AES-256-CBC' => 256 }.freeze | ||
|
||
def self.random_iv(size = 16) | ||
OpenSSL::Random.random_bytes(size) | ||
end | ||
|
||
def self.encrypt(cipher: CIPHER, key: nil, iv: nil, data: nil) | ||
aes = Mifiel::Crypto::AES.new(cipher) | ||
aes.encrypt(key: key, iv: iv, data: data) | ||
end | ||
|
||
def self.decrypt(cipher: CIPHER, key: nil, iv: nil, data: nil) | ||
aes = Mifiel::Crypto::AES.new(cipher) | ||
aes.decrypt(key: key, iv: iv, data: data) | ||
end | ||
|
||
def self.build_cipher(cipher) | ||
return OpenSSL::Cipher.new(cipher) if cipher.is_a? String | ||
OpenSSL::Cipher::AES.new(cipher, :CBC) | ||
rescue | ||
raise Mifiel::AESError, 'Cipher not supported' | ||
end | ||
|
||
attr_accessor :cipher | ||
|
||
def initialize(cipher_id = CIPHER) | ||
@cipher = Mifiel::Crypto::AES.build_cipher(cipher_id) | ||
end | ||
|
||
def encrypt(key: nil, iv: nil, data: nil) | ||
iv ||= Mifiel::Crypto::AES.random_iv(size) | ||
cipher_final(key, iv, data, action: :encrypt) | ||
end | ||
|
||
def decrypt(key: nil, iv: nil, data: nil) | ||
cipher_final(key, iv, data, action: :decrypt) | ||
end | ||
|
||
def cipher_final(key, iv, message, action: :encrypt) | ||
@cipher.send(action) | ||
@cipher.iv = iv | ||
@cipher.key = key | ||
@cipher.update(message) + @cipher.final | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# This class was based on https://github.com/jamoes/ecies | ||
module Mifiel | ||
module Crypto | ||
class ECIES | ||
# The allowed digest algorithms for ECIES. | ||
DIGESTS = %w(SHA224 SHA256 SHA384 SHA512).freeze | ||
|
||
# The allowed cipher algorithms for ECIES. | ||
CIPHERS = %w(AES-128-CBC AES-192-CBC AES-256-CBC).freeze | ||
IV_SIZE = 16 | ||
|
||
def initialize(cipher: 'AES-256-CBC', kdf_digest: 'SHA512', mac_digest: 'SHA256') | ||
raise Mifiel::ECError, "Cipher must be one of #{CIPHERS}" unless CIPHERS.include?(cipher) | ||
raise Mifiel::ECError, "Cipher must be one of #{DIGESTS}" unless DIGESTS.include?(mac_digest) | ||
raise Mifiel::ECError, "Cipher must be one of #{DIGESTS}" unless DIGESTS.include?(kdf_digest) | ||
|
||
@cipher = OpenSSL::Cipher.new(cipher) | ||
@mac_digest = OpenSSL::Digest.new(mac_digest) | ||
@kdf_digest = OpenSSL::Digest.new(kdf_digest) | ||
@mac_length = @mac_digest.digest_length | ||
end | ||
|
||
def cipher_final(key, iv, message, action: :encrypt) | ||
@cipher.reset | ||
@cipher.send(action) | ||
@cipher.iv = iv | ||
@cipher.key = key | ||
@cipher.update(message) + @cipher.final | ||
end | ||
|
||
def generate_keys(shared_secret) | ||
key_pair = @kdf_digest.digest(shared_secret) | ||
cipher_key = key_pair.byteslice(0, @cipher.key_len) | ||
hmac_key = key_pair.byteslice(-@mac_length, @mac_length) | ||
{ cipher_key: cipher_key, hmac_key: hmac_key } | ||
end | ||
|
||
def compute_mac(hmac_key, ephemeral_public_key_octet, ciphertext, iv) | ||
OpenSSL::HMAC.digest(@mac_digest, hmac_key, iv + ephemeral_public_key_octet + ciphertext) | ||
end | ||
|
||
# Encrypts a message to a public key using ECIES. | ||
# @param key [OpenSSL::EC:PKey] The public key. | ||
# @param message [String] The plain-text message. | ||
# @return [String] The octet string of the encrypted message. | ||
def encrypt(key, message, iv: nil) # rubocop:disable Metrics/AbcSize | ||
iv ||= OpenSSL::Random.random_bytes(IV_SIZE) | ||
ephemeral_key = OpenSSL::PKey::EC.new(key.group).generate_key | ||
ephemeral_public_key_octet = ephemeral_key.public_key.to_bn.to_s(2) | ||
keys = generate_keys(ephemeral_key.dh_compute_key(key.public_key)) | ||
|
||
ciphertext = cipher_final(keys[:cipher_key], iv, message) | ||
mac = compute_mac(keys[:hmac_key], ephemeral_public_key_octet, ciphertext, iv) | ||
iv + ephemeral_public_key_octet + ciphertext + mac | ||
end | ||
|
||
# Decrypts a message with a private key using ECIES. | ||
# @param key [OpenSSL::EC:PKey] The private key. | ||
# @param encrypted_message [String] Octet string of the encrypted message. | ||
# @return [String] The plain-text message. | ||
def decrypt(key, encrypted_message) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength | ||
iv = encrypted_message.byteslice(0, IV_SIZE) | ||
group_copy = OpenSSL::PKey::EC::Group.new(key.group) | ||
ephemeral_public_key_length = group_copy.generator.to_bn.to_s(2).bytesize | ||
ciphertext_length = encrypted_message.bytesize - ephemeral_public_key_length - @mac_length - IV_SIZE | ||
raise Mifiel::ECError, 'Encrypted message too short' unless ciphertext_length > 0 | ||
|
||
ephemeral_public_key_octet = encrypted_message.byteslice(iv.bytesize, ephemeral_public_key_length) | ||
ciphertext = encrypted_message.byteslice((ephemeral_public_key_length + iv.bytesize), ciphertext_length) | ||
ephemeral_public_key = OpenSSL::PKey::EC::Point.new(group_copy, OpenSSL::BN.new(ephemeral_public_key_octet, 2)) | ||
keys = generate_keys(key.dh_compute_key(ephemeral_public_key)) | ||
|
||
mac = encrypted_message.byteslice(-@mac_length, @mac_length) | ||
computed_mac = compute_mac(keys[:hmac_key], ephemeral_public_key_octet, ciphertext, iv) | ||
raise Mifiel::ECError, 'Invalid mac' unless computed_mac == mac | ||
cipher_final(keys[:cipher_key], iv, ciphertext, action: :decrypt) | ||
end | ||
|
||
# Converts a hex-encoded public key to an `OpenSSL::PKey::EC`. | ||
# | ||
# @param hex_string [String] The hex-encoded public key. | ||
# @param ec_group [OpenSSL::PKey::EC::Group,String] The elliptical curve | ||
# group for this public key. | ||
# @return [OpenSSL::PKey::EC] The public key. | ||
# @raise [OpenSSL::PKey::EC::Point::Error] If the public key is invalid. | ||
def self.public_from_hex(hex_string, ec_group = 'secp256k1') | ||
ec_group = OpenSSL::PKey::EC::Group.new(ec_group) if ec_group.is_a?(String) | ||
key = OpenSSL::PKey::EC.new(ec_group) | ||
key.public_key = OpenSSL::PKey::EC::Point.new(ec_group, OpenSSL::BN.new(hex_string, 16)) | ||
key | ||
end | ||
|
||
# Converts a hex-encoded private key to an `OpenSSL::PKey::EC`. | ||
# | ||
# @param hex_string [String] The hex-encoded private key. | ||
# @param ec_group [OpenSSL::PKey::EC::Group,String] The elliptical curve | ||
# group for this private key. | ||
# @return [OpenSSL::PKey::EC] The private key. | ||
# @note The returned key only contains the private component. In order to | ||
# populate the public component of the key, you must compute it as | ||
# follows: `key.public_key = key.group.generator.mul(key.private_key)`. | ||
# @raise [::ECError] If the private key is invalid. | ||
def self.private_from_hex(hex_string, ec_group = 'secp256k1') | ||
ec_group = OpenSSL::PKey::EC::Group.new(ec_group) if ec_group.is_a?(String) | ||
key = OpenSSL::PKey::EC.new(ec_group) | ||
key.private_key = OpenSSL::BN.new(hex_string, 16) | ||
raise Mifiel::ECError, 'Private key greater than group order' unless key.private_key < ec_group.order | ||
raise Mifiel::ECError, 'Private key too small' unless key.private_key > 1 | ||
key | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
require 'openssl' | ||
require 'securerandom' | ||
|
||
module Mifiel | ||
module Crypto | ||
class PBE | ||
ALPHA_NUM = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a | ||
SPECIALS = ['-', '_', '+', '=', '#', '&', '*', '.'].freeze | ||
CHARS = ALPHA_NUM + SPECIALS | ||
ITERATIONS = 1000 | ||
|
||
def self.random_password(length = 32) | ||
CHARS.sort_by { SecureRandom.random_number }.join[0...length] | ||
end | ||
|
||
def self.random_salt(size = 16) | ||
SecureRandom.random_bytes(size) | ||
end | ||
|
||
def self.derive_key(password:, salt:, key_size: 32, iterations: ITERATIONS) | ||
args = { | ||
password: password, | ||
salt: salt, | ||
iterations: iterations, | ||
key_size: key_size, | ||
digest: OpenSSL::Digest::SHA256.new | ||
} | ||
OpenSSL::PKCS5.pbkdf2_hmac(*args.values) | ||
rescue => e | ||
raise Mifiel::PBError, e.message || 'Unable to derive key.' | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.