Skip to content

Instantly share code, notes, and snippets.

@lacostej
Created January 5, 2024 16:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lacostej/cc33868553e0d57f27681dc887865caa to your computer and use it in GitHub Desktop.
Save lacostej/cc33868553e0d57f27681dc887865caa to your computer and use it in GitHub Desktop.
Experiment with AES-256-GCM and proposed migration to fastlane's match encryption
require 'openssl'
require 'securerandom'
require 'base64'
class OldEncryption
ALGORITHM = 'aes-256-cbc'
def encrypt(data, password, salt)
cipher = ::OpenSSL::Cipher::Cipher.new(ALGORITHM)
cipher.encrypt
keyivgen(cipher, password, salt)
encrypted_data = cipher.update(data)
encrypted_data << cipher.final
end
def decrypt(encrypted_data, password, salt)
cipher = OpenSSL::Cipher::Cipher.new(ALGORITHM)
cipher.decrypt
keyivgen(cipher, password, salt)
data = cipher.update(encrypted_data)
data << cipher.final
end
private
def keyivgen(cipher, password, salt)
cipher.pkcs5_keyivgen(password, salt, 1, "MD5")
end
end
class Encryption
ALGORITHM = 'AES-256-GCM'
def encrypt(data, password, salt)
cipher = ::OpenSSL::Cipher::Cipher.new(ALGORITHM)
cipher.encrypt
gen = keyivgen(cipher, password, salt)
encrypted_data = cipher.update(data)
encrypted_data << cipher.final
auth_tag = cipher.auth_tag
{encrypted_data: encrypted_data, auth_tag: auth_tag}
end
def decrypt(encrypted_data, password, salt, auth_tag)
cipher = ::OpenSSL::Cipher::Cipher.new(ALGORITHM)
cipher.decrypt
gen = keyivgen(cipher, password, salt)
cipher.auth_tag = auth_tag
data = cipher.update(encrypted_data)
data << cipher.final
end
private
def keyivgen(cipher, password, salt)
keyIv = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10000, length: 32+12+24, hash: "sha256")
key = keyIv[0..31]
iv = keyIv[32..43]
auth_data = keyIv[44..-1]
cipher.key = key
cipher.iv = iv
cipher.auth_data = auth_data
end
end
class MatchDataEncryption
V1_PREFIX = "Salted__"
V2_PREFIX = "2_Salted__"
def encrypt(data, password, salt, version=2)
if version==2
e = Encryption.new
encryption = e.encrypt(data, password, salt)
encrypted_data = V2_PREFIX + salt + encryption[:auth_tag] + encryption[:encrypted_data]
else
e = OldEncryption.new
encrypted_data = V1_PREFIX + salt + e.encrypt(data, password, salt)
end
data = Base64.encode64(encrypted_data)
end
def decrypt(base64encoded_encrypted, password, expected_salt)
stored_data = Base64.decode64(base64encoded_encrypted)
if stored_data.start_with?(V2_PREFIX)
salt = stored_data[10..17]
raise "ERROR SALT v2 #{salt}, #{expected_salt}" unless salt == expected_salt
auth_tag = stored_data[18..33]
data_to_decrypt = stored_data[34..-1]
e = Encryption.new
e.decrypt(data_to_decrypt, password, salt, auth_tag)
else
salt = stored_data[8..15]
raise "ERROR SALT v1 #{salt}, #{expected_salt}" unless salt == expected_salt
data_to_decrypt = stored_data[16..-1]
e = OldEncryption.new
e.decrypt(data_to_decrypt, password, salt)
end
end
end
class MatchFileEncryption
def encrypt(file_path, password, salt)
data_to_encrypt = File.binread(file_path)
e = MatchDataEncryption.new
data = e.encrypt(data_to_encrypt, password, salt)
File.write(file_path, data)
end
def decrypt(file_path, password, expected_salt)
content = File.read(file_path)
e = MatchDataEncryption.new
decrypted_data = e.decrypt(content, password, expected_salt)
File.binwrite(file_path, decrypted_data)
end
end
text='a text file with multiple lines'
DATA = 10.times.map{|i| text}.join("\n")
datafile = "testdata.txt"
def test_0(data)
me = MatchDataEncryption.new
password='2"QAHg@v(Qp{=*n^'
wrong_password="#{password}x" # too short
salt = SecureRandom.random_bytes(8)
# encrypt the old way
encrypted_data = me.encrypt(data, password, salt, 1)
# decryption works
decrypted_data = me.decrypt(encrypted_data, password, salt)
raise "UNMATCH" unless data == decrypted_data
# old way may not find that decryption failed
begin
decrypted_data = me.decrypt(encrypted_data, wrong_password, salt)
puts "ERROR: v1 salt #{salt.unpack("C*")} not detected"
rescue OpenSSL::Cipher::CipherError => e
# expected "bad decrypt"
raise "ERROR: #{e} v1 salt #{salt.unpack("C*")}" unless e.to_s == "bad decrypt"
end
# encrypt the new way
encrypted_data = me.encrypt(data, password, salt)
begin
decrypted_data = me.decrypt(encrypted_data, wrong_password, salt)
raise "ERROR: v2 salt #{salt.unpack("C*")} HI"
rescue OpenSSL::Cipher::CipherError => e
# expected "bad decrypt"
raise "ERROR: #{e} v2 salt #{salt.unpack("C*")}" unless e.to_s == ""
end
end
def test_1(data)
datafile = "test.data"
File.open(datafile, "w:UTF-8") do |f| f.write(data) end
password='2"QAHg@v(Qp{=*n^'
wrong_password="invalid" # too short
salt = SecureRandom.random_bytes(8)
# test with a specific known broken salt with the given small wrong password
# salt = [85, 199, 9, 3, 14, 29, 62, 66].pack("C*")
e = MatchFileEncryption.new
e.encrypt(datafile, password, salt)
#e.decrypt(datafile, password, salt)
#roundtrip_data = File.read(datafile)
#raise "Wrong data" unless roundtrip_data == data
begin
e.decrypt(datafile, wrong_password, salt)
data = File.read(datafile)
raise "ERROR: salt #{salt.unpack("C*")}"
rescue OpenSSL::Cipher::CipherError => e
raise e unless e.to_s == ""
end
end
RUNS=1000
puts "Running test #0"
RUNS.times { |i|
begin
test_0(DATA)
rescue => e
puts "#{i}: #{e}"
end
}
puts "Running test #1"
RUNS.times { |i|
begin
test_1(DATA)
rescue => e
puts "#{i}: #{e}"
puts e.backtrace
end
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment