Class: SecretKeys::Encryptor
- Inherits:
-
Object
- Object
- SecretKeys::Encryptor
- Defined in:
- lib/secret_keys/encryptor.rb
Overview
Encyption helper for encrypting and decrypting values using AES-256-GCM and returning as Base64 encoded strings. The encrypted values also include a prefix that can be used to detect if a string is an encrypted value.
Defined Under Namespace
Classes: CipherParams
Constant Summary collapse
- ENCODING_FORMAT =
format: <nonce:12>, <auth_tag:16>, <data:*>
"a12 a16 a*"- ENCRYPTED_PREFIX =
"$AES$:"- CIPHER =
"aes-256-gcm"- KDF_ITERATIONS =
20_000- HASH_FUNC =
"sha256"- KEY_LENGTH =
32- SALT_MATCHER =
Valid salts are hexencoded strings
/\A(\h\h)+\z/.freeze
Class Method Summary collapse
-
.derive_key(password, salt:, length: KEY_LENGTH, iterations: KDF_ITERATIONS, hash: HASH_FUNC) ⇒ Object
Derive a key of given length from a password and salt value.
-
.encrypted?(value) ⇒ Boolean
Detect of the value is a string that was encrypted by this library.
-
.from_password(password, salt) ⇒ SecretKeys::Encryptor
Create an Encryptor from a password and salt.
-
.random_salt ⇒ String
Hex encoded random bytes.
Instance Method Summary collapse
-
#decrypt(encrypted_str) ⇒ String
Decrypt a string with the encryption key.
-
#encrypt(str) ⇒ String
Encrypt a string with the encryption key.
- #encrypted?(value) ⇒ Boolean
-
#initialize(raw_key) ⇒ Encryptor
constructor
A new instance of Encryptor.
- #inspect ⇒ Object
Constructor Details
#initialize(raw_key) ⇒ Encryptor
Returns a new instance of Encryptor.
59 60 61 62 |
# File 'lib/secret_keys/encryptor.rb', line 59 def initialize(raw_key) raise ArgumentError, "key must be #{KEY_LENGTH} bytes long" unless raw_key.bytesize == KEY_LENGTH @derived_key = raw_key end |
Class Method Details
.derive_key(password, salt:, length: KEY_LENGTH, iterations: KDF_ITERATIONS, hash: HASH_FUNC) ⇒ Object
Derive a key of given length from a password and salt value.
43 44 45 46 47 48 49 50 |
# File 'lib/secret_keys/encryptor.rb', line 43 def derive_key(password, salt:, length: KEY_LENGTH, iterations: KDF_ITERATIONS, hash: HASH_FUNC) if defined?(OpenSSL::KDF) OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: iterations, length: length, hash: hash) else # Ruby 2.4 compatibility OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, length, hash) end end |
.encrypted?(value) ⇒ Boolean
Detect of the value is a string that was encrypted by this library.
38 39 40 |
# File 'lib/secret_keys/encryptor.rb', line 38 def encrypted?(value) value.is_a?(String) && value.start_with?(ENCRYPTED_PREFIX) end |
.from_password(password, salt) ⇒ SecretKeys::Encryptor
Create an Encryptor from a password and salt. This is a shortcut for generating an Encryptor with a 32 byte encryption key. The key will be derived from the password and salt.
27 28 29 30 31 32 33 34 35 |
# File 'lib/secret_keys/encryptor.rb', line 27 def from_password(password, salt) raise ArgumentError, "Password must be present" if password.nil? || password.empty? raise ArgumentError, "Salt must be a hex encoded value" if salt.nil? || !SALT_MATCHER.match?(salt) # Convert the salt to raw byte string salt_bytes = [salt].pack("H*") derived_key = derive_key(password, salt: salt_bytes) new(derived_key) end |
.random_salt ⇒ String
Returns hex encoded random bytes.
53 54 55 |
# File 'lib/secret_keys/encryptor.rb', line 53 def random_salt SecureRandom.hex(16) end |
Instance Method Details
#decrypt(encrypted_str) ⇒ String
Decrypt a string with the encryption key. If the value is not a string or it was not encrypted with the encryption key, the value itself will be returned.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/secret_keys/encryptor.rb', line 102 def decrypt(encrypted_str) return encrypted_str unless self.class.encrypted?(encrypted_str) decrypt_str = encrypted_str[ENCRYPTED_PREFIX.length..-1] params = decode_aes(decrypt_str) cipher = OpenSSL::Cipher.new(CIPHER).decrypt cipher.key = @derived_key cipher.iv = params.nonce cipher.auth_tag = params.auth_tag cipher.auth_data = "" decoded_str = cipher.update(params.data) + cipher.final # force to utf-8 encoding. We already ensured this when we encoded in the first place decoded_str.force_encoding(Encoding::UTF_8) end |
#encrypt(str) ⇒ String
Encrypt a string with the encryption key. Encrypted values are also salted so calling this function multiple times will result in different values. Only strings can be encrypted. Any other object type will be return the value passed in.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/secret_keys/encryptor.rb', line 70 def encrypt(str) return str unless str.is_a?(String) return "" if str == "" cipher = OpenSSL::Cipher.new(CIPHER).encrypt # Technically, this is a "bad" way to do things since we could theoretically # get a repeat nonce, compromising the algorithm. That said, it should be safe # from repeats as long as we don't use this key for more than 2^32 encryptions # so... rotate your keys/salt ever 4 billion encryption calls nonce = cipher.random_iv cipher.key = @derived_key cipher.auth_data = "" # Make sure the string is encoded as UTF-8. JSON/YAML only support string types # anyways, so if you passed in binary data, it was gonna fail anyways. This ensures # that we can easily decode the string later. If you have UTF-16 or something, deal with it. utf8_str = str.encode(Encoding::UTF_8) encrypted_data = cipher.update(utf8_str) + cipher.final auth_tag = cipher.auth_tag params = CipherParams.new(nonce, auth_tag, encrypted_data) encode_aes(params).prepend(ENCRYPTED_PREFIX) end |
#encrypted?(value) ⇒ Boolean
121 122 123 |
# File 'lib/secret_keys/encryptor.rb', line 121 def encrypted?(value) self.class.encrypted?(value) end |
#inspect ⇒ Object
125 126 127 128 |
# File 'lib/secret_keys/encryptor.rb', line 125 def inspect obj_id = object_id.to_s(16).rjust(16, "0") "#<#{self.class.name}:0x#{obj_id}>" end |