Created
January 5, 2024 16:29
-
-
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
This file contains hidden or 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
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