You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

57 lines
2.4 KiB

require "openssl"
require "base64"
require "json"
# Portable authenticated encryption, identical in both applications.
# The file contains no password or key. Never accept client-selected algorithms/KDF costs.
class ProfessionalTransferEnvelope
FORMAT = "pcm-professional-transfer-encrypted-v1".freeze
AAD = "pcm-professional-transfer:v1".freeze
ITERATIONS = 600_000
MAX_FILE_BYTES = 4 * 1024 * 1024
class Invalid < StandardError; end
def self.encrypt(payload, password)
check_password!(password)
salt = OpenSSL::Random.random_bytes(16)
iv = OpenSSL::Random.random_bytes(12)
cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt
cipher.key = key(password, salt)
cipher.iv = iv
cipher.auth_data = AAD
ciphertext = cipher.update(JSON.generate(payload)) + cipher.final
envelope = { format: FORMAT, salt: Base64.strict_encode64(salt), iv: Base64.strict_encode64(iv),
tag: Base64.strict_encode64(cipher.auth_tag), ciphertext: Base64.strict_encode64(ciphertext) }
output = JSON.generate(envelope)
raise Invalid, "too_large" if output.bytesize > MAX_FILE_BYTES
output
end
def self.decrypt(file, password)
check_password!(password)
raise Invalid, "too_large" if file.bytesize > MAX_FILE_BYTES
envelope = JSON.parse(file, max_nesting: 5)
raise Invalid, "invalid_file" unless envelope.is_a?(Hash) && envelope.keys.sort == %w[ciphertext format iv salt tag] && envelope["format"] == FORMAT
salt, iv, tag, ciphertext = %w[salt iv tag ciphertext].map { |name| Base64.strict_decode64(envelope.fetch(name)) }
raise Invalid, "invalid_file" unless salt.bytesize == 16 && iv.bytesize == 12 && tag.bytesize == 16 && ciphertext.bytesize.positive?
cipher = OpenSSL::Cipher.new("aes-256-gcm").decrypt
cipher.key = key(password, salt)
cipher.iv = iv
cipher.auth_tag = tag
cipher.auth_data = AAD
JSON.parse(cipher.update(ciphertext) + cipher.final, max_nesting: 10)
rescue Invalid
raise
rescue JSON::ParserError, ArgumentError, TypeError, KeyError, OpenSSL::Cipher::CipherError
raise Invalid, "invalid_file"
end
def self.check_password!(password)
raise Invalid, "password" unless password.is_a?(String) && password.length >= 12 && password.bytesize <= 256
end
def self.key(password, salt)
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, ITERATIONS, 32, "sha256")
end
private_class_method :key, :check_password!
end