Skip to content

Instantly share code, notes, and snippets.

@mariuszkapcia
Last active February 1, 2019 09:57
Show Gist options
  • Save mariuszkapcia/19fa2164b22c700bd4e86e4134894b96 to your computer and use it in GitHub Desktop.
Save mariuszkapcia/19fa2164b22c700bd4e86e4134894b96 to your computer and use it in GitHub Desktop.
GDPR support for rails_event_store gem.
MissingEncryptionKey = Class.new(StandardError)
class EncryptedMapper
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: meta.fetch(:iv))
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, meta.fetch(:iv))
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
class EncryptionKey < ActiveRecord::Base
def encrypt(message, iv: nil)
crypto = OpenSSL::Cipher.new(cipher)
crypto.encrypt
crypto.iv = iv || self.iv
crypto.key = key
crypto.update(message) + crypto.final
end
def decrypt(message, iv: nil)
crypto = OpenSSL::Cipher.new(cipher)
crypto.decrypt
crypto.iv = iv || self.iv
crypto.key = key
(crypto.update(message) + crypto.final).force_encoding('UTF-8')
end
end
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: crypto.random_iv,
key: crypto.random_key
)
end
def forget(identifier)
EncryptionKey.where(identifier: identifier).destroy_all
end
def delete_all
EncryptionKey.destroy_all
end
end
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
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}>
Rails.configuration.event_store = RailsEventStore::Client.new(
mapper: EncryptedMapper.new(EncryptionKeyRepository.new)
)
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