-
-
Save swilgosz/cf9bd187e14933ca1d4d59f713ddfdd0 to your computer and use it in GitHub Desktop.
GDPR support for rails_event_store gem.
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
class EncryptedMapper | |
MissingEncryptionKey = Class.new(StandardError) | |
def event_to_serialized_record(domain_event) | |
metadata = {} | |
domain_event.metadata.each do |k, v| | |
metadata[k] = v | |
end | |
encryption_schema = domain_event.class.respond_to?(:encryption_schema) && domain_event.class.encryption_schema | |
encryption_metadata_ = encryption_metadata(domain_event.data, encryption_schema) | |
data = deep_dup(domain_event.data) | |
encrypt_data(data, encryption_metadata_) | |
RubyEventStore::SerializedRecord.new( | |
event_id: domain_event.event_id, | |
metadata: serializer.dump(metadata.merge(encryption: serializer.dump(encryption_metadata_))), | |
data: serializer.dump(data), | |
event_type: domain_event.class.to_s | |
) | |
end | |
def serialized_record_to_event(record) | |
metadata = serializer.load(record.metadata) | |
data = serializer.load(record.data) | |
encryption_metadata = serializer.load(metadata.delete(:encryption) || '') | |
Object.const_get(record.event_type).new( | |
event_id: record.event_id, | |
metadata: metadata, | |
data: deserialize_data(data, encryption_metadata) | |
) | |
end | |
private | |
attr_reader :key_repository, :serializer, :events_class_remapping | |
def initialize(key_repository, serializer: YAML, events_class_remapping: {}) | |
@key_repository = key_repository | |
@serializer = serializer | |
@events_class_remapping = events_class_remapping | |
end | |
def prepare_cipher(cipher_) | |
cipher = OpenSSL::Cipher.new(cipher_) | |
cipher.encrypt | |
cipher | |
end | |
def deep_dup(hash) | |
duplicate = hash.dup | |
duplicate.each do |k, v| | |
duplicate[k] = v.instance_of?(Hash) ? deep_dup(v) : v | |
end | |
duplicate | |
end | |
def encryption_metadata(data, schema) | |
return unless schema | |
schema.inject({}) do |acc, (key, value)| | |
key_identifier = value.call(data) | |
encryption_key = key_repository.key_of(key_identifier) | |
raise MissingEncryptionKey.new("Could not find encryption key for '#{key_identifier}'") unless encryption_key | |
cipher = prepare_cipher(encryption_key.cipher) | |
acc[key] = { cipher: encryption_key.cipher, iv: cipher.random_iv, identifier: key_identifier } | |
acc | |
end | |
end | |
def encrypt_data(data, metadata) | |
return unless metadata | |
metadata.each do |key, value| | |
encrypt_attribute(data, key, value) | |
end | |
end | |
def encrypt_attribute(data, attribute, meta) | |
encryption_key = key_repository.key_of(meta.fetch(:identifier)) | |
data[attribute] = encryption_key.encrypt( | |
serializer.dump(data.fetch(attribute)), | |
iv: Base64.encode64(meta.fetch(:iv)).encode('utf-8') | |
) | |
end | |
def deserialize_data(data, encryption_metadata) | |
if encryption_metadata | |
deep_dup(data).tap do |decrypted_data| | |
decrypt_data(decrypted_data, encryption_metadata) | |
end | |
else | |
data | |
end | |
end | |
def decrypt_data(data, metadata) | |
metadata.each do |key, value| | |
decrypt_attribute(data, key, value) | |
end | |
end | |
def decrypt_attribute(data, attribute, meta) | |
cryptogram = data[attribute] | |
return nil unless cryptogram | |
encryption_key = key_repository.key_of(meta.fetch(:identifier), cipher: meta.fetch(:cipher)) | |
data[attribute] = if encryption_key | |
decrypt_and_decode_value( | |
cryptogram, encryption_key, | |
Base64.encode64(meta.fetch(:iv)).encode('utf-8') | |
) | |
else | |
ForgottenData.new | |
end | |
end | |
def decrypt_and_decode_value(cryptogram, key, iv) | |
serializer.load(key.decrypt(cryptogram, iv: iv)) | |
rescue OpenSSL::Cipher::CipherError | |
ForgottenData.new | |
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
class EncryptionKey < ActiveRecord::Base | |
def encrypt(message, iv: nil) | |
crypto = OpenSSL::Cipher.new(cipher) | |
crypto.encrypt | |
crypto.iv = Base64.decode64((iv || self.iv).encode('ascii-8bit')) | |
crypto.key = Base64.decode64(key.encode('ascii-8bit')) | |
crypto.update(message) + crypto.final | |
end | |
def decrypt(message, iv: nil) | |
crypto = OpenSSL::Cipher.new(cipher) | |
crypto.decrypt | |
crypto.iv = Base64.decode64((iv || self.iv).encode('ascii-8bit')) | |
crypto.key = Base64.decode64(key.encode('ascii-8bit')) | |
(crypto.update(message) + crypto.final).force_encoding('UTF-8') | |
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
class EncryptionKeyRepository | |
DEFAULT_CIPHER = 'aes-256-cbc'.freeze | |
def key_of(identifier, cipher: DEFAULT_CIPHER) | |
EncryptionKey.where(identifier: identifier, cipher: cipher).take | |
end | |
def create(identifier, cipher: DEFAULT_CIPHER) | |
crypto = OpenSSL::Cipher.new(cipher) | |
crypto.encrypt | |
EncryptionKey.where( | |
identifier: identifier, | |
cipher: cipher | |
).first_or_create!( | |
iv: Base64.encode64(crypto.random_iv).encode('utf-8'), | |
key: Base64.encode64(crypto.random_key).encode('utf-8') | |
) | |
end | |
def forget(identifier) | |
EncryptionKey.where(identifier: identifier).destroy_all | |
end | |
def delete_all | |
EncryptionKey.destroy_all | |
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
class ForgottenData | |
include Enumerable | |
FORGOTTEN_DATA = 'FORGOTTEN_DATA'.freeze | |
def initialize(string = FORGOTTEN_DATA) | |
@string = string | |
end | |
def inspect | |
@string | |
end | |
alias to_s inspect | |
def ==(other) | |
@string == other | |
end | |
def to_a | |
[] | |
end | |
def to_h | |
{} | |
end | |
def to_i | |
0 | |
end | |
def to_f | |
0 | |
end | |
def each | |
if block_given? | |
self | |
else | |
enum_for(:each) | |
end | |
end | |
def empty? | |
true | |
end | |
def blank? | |
true | |
end | |
def present? | |
false | |
end | |
def size | |
0 | |
end | |
alias count size | |
def method_missing(m, *args, &blk) | |
self | |
end | |
def respond_to_missing?(method_name, include_private = false) | |
true | |
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
irb(main):001:0> user_uuid = 'uuid' | |
=> "uuid" | |
irb(main):002:0> encryption_repo = EncryptionKeyRepository.new | |
=> #<EncryptionKeyRepository:0x007f9896e5da18> | |
irb(main):003:0> encryption_repo.create(user_uuid) | |
=> #<EncryptionKey id: 4, cipher: "aes-256-cbc", iv: "\x9B\xA9:\x93\x03\x93c9-\xE4\x93$\x92\x02KW", key: "\x94\x92\xCC\x03;\x9Ax\xFB\xA9\xCDN\xBE\f_I\n\x00\xC4\xB6\x8E\x12m\xA1\xB1sU\xB9\vp]A\x1F", identifier: "uuid"> | |
irb(main):004:0> fact = Users::UserRegisteredFromFacebook.new(data: { uuid: user_uuid, fullname: 'fullname', email: 'email', facebook_id: 'facebook_id' }) | |
=> #<Users::UserRegisteredFromFacebook:0x007fc19363f360 @event_id="f5c3f6a3-e5dc-4da5-9400-6d3c63fc81fd", @metadata=#<RubyEventStore::Metadata:0x007fc19363e500 @h={}>, @data={:uuid=>"uuid", :fullname=>"fullname", :email=>"email", :facebook_id=>"facebook_id"}> | |
irb(main):005:0> mapper = EncryptedMapper.new(encryption_repo) | |
=> #<EncryptedMapper:0x007f989f226070 @key_repository=#<EncryptionKeyRepository:0x007f9896e5da18>, @serializer=Psych, @events_class_remapping={}> | |
irb(main):006:0> encrypted_fact = mapper.event_to_serialized_record(fact) | |
=> #<RubyEventStore::SerializedRecord:0x007fe841a5b1e0 @event_id="920c7bb4-82b8-42ea-9080-da6c24a8f4da", @data="---\n:uuid: uuid\n:fullname: !binary |-\n Og+K8hrYH2k/NpxQQ/oxRwRnduXF9mM8qy2KyblTsC0=\n:email: !binary |-\n AaOZ3ZJ2JOTlWeNyi1GHrQ==\n:facebook_id: !binary |-\n fAeM8LnnKsfVyzSII6IczoEbRwfBMolY81hdVWru254=\n", @metadata="---\n:encryption: |\n ---\n :fullname:\n :cipher: aes-256-cbc\n :iv: !binary |-\n q62jhFy6gaaSg2zgzjFzEA==\n :identifier: uuid\n :email:\n :cipher: aes-256-cbc\n :iv: !binary |-\n EalRURUZd4YXchcxuGaicQ==\n :identifier: uuid\n :facebook_id:\n :cipher: aes-256-cbc\n :iv: !binary |-\n 4OBVN6N7boHfBtpNhVrLWg==\n :identifier: uuid\n", @event_type="Users::UserRegisteredFromFacebook"> | |
irb(main):007:0> mapper.serialized_record_to_event(encrypted_fact) | |
=> #<Users::UserRegisteredFromFacebook:0x007fe84221d748 @event_id="920c7bb4-82b8-42ea-9080-da6c24a8f4da", @metadata=#<RubyEventStore::Metadata:0x007fe84221d630 @h={}>, @data={:uuid=>"uuid", :fullname=>"fullname", :email=>"email", :facebook_id=>"facebook_id"}> | |
irb(main):008:0> encryption_repo.delete_all | |
=> [#<EncryptionKey id: 4, cipher: "aes-256-cbc", iv: "\x9B\xA9:\x93\x03\x93c9-\xE4\x93$\x92\x02KW", key: "\x94\x92\xCC\x03;\x9Ax\xFB\xA9\xCDN\xBE\f_I\n\x00\xC4\xB6\x8E\x12m\xA1\xB1sU\xB9\vp]A\x1F", identifier: "uuid">] | |
irb(main):009:0> mapper.serialized_record_to_event(encrypted_fact) | |
=> #<Users::UserRegisteredFromFacebook:0x007fe1da4d0cd8 @event_id="41f82e76-2722-45ff-a6ef-e9b98c922f98", @metadata=#<RubyEventStore::Metadata:0x007fe1da4d0c88 @h={}>, @data={:uuid=>"uuid", :fullname=>FORGOTTEN_DATA, :email=>FORGOTTEN_DATA, :facebook_id=>FORGOTTEN_DATA}> |
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
Rails.configuration.event_store = RailsEventStore::Client.new( | |
mapper: EncryptedMapper.new(EncryptionKeyRepository.new) | |
) |
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
module Users | |
class UserRegisteredFromFacebook < RailsEventStore::Event | |
SCHEMA = { | |
uuid: String, | |
fullname: String, | |
email: [String, NilClass], | |
facebook_id: String | |
}.freeze | |
def self.strict(data:) | |
ClassyHash.validate(data, SCHEMA) | |
new(data: data) | |
end | |
def self.encryption_schema | |
{ | |
fullname: ->(data) { data.dig(:uuid) }, | |
email: ->(data) { data.dig(:uuid) }, | |
facebook_id: ->(data) { data.dig(:uuid) } | |
} | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment