From 17b659539fa47ffa0ff728b7dc9d9128c1ee3ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Vantomme?= Date: Thu, 10 Jun 2021 16:52:51 +0200 Subject: [PATCH 1/3] Feat (API Particulier): new encryption service --- app/services/encryption_service.rb | 17 ++++++++++ config/env.example | 3 ++ config/secrets.yml | 2 ++ spec/services/encryption_service_spec.rb | 42 ++++++++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 app/services/encryption_service.rb create mode 100644 spec/services/encryption_service_spec.rb diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb new file mode 100644 index 000000000..448ddcc7a --- /dev/null +++ b/app/services/encryption_service.rb @@ -0,0 +1,17 @@ +class EncryptionService + def initialize + len = ActiveSupport::MessageEncryptor.key_len + salt = Rails.application.secrets.encryption_service_salt + password = Rails.application.secrets.secret_key_base + key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len) + @encryptor = ActiveSupport::MessageEncryptor.new(key) + end + + def encrypt(value) + value.blank? ? nil : @encryptor.encrypt_and_sign(value) + end + + def decrypt(value) + value.blank? ? nil : @encryptor.decrypt_and_verify(value) + end +end diff --git a/config/env.example b/config/env.example index 17e3d081d..48266c857 100644 --- a/config/env.example +++ b/config/env.example @@ -112,3 +112,6 @@ API_EDUCATION_URL="https://data.education.gouv.fr/api/records/1.0" # Modifier le nb de tentatives de relance de job si echec # MAX_ATTEMPTS_JOBS=25 # MAX_ATTEMPTS_API_ENTREPRISE_JOBS=5 + +# Clé de chriffrement des données sensibles en base +ENCRYPTION_SERVICE_SALT="" diff --git a/config/secrets.yml b/config/secrets.yml index 2440c6d6c..1199d404a 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -11,6 +11,7 @@ # if you're sharing your code publicly. defaults: &defaults secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> + encryption_service_salt: <%= ENV["ENCRYPTION_SERVICE_SALT"] %> signing_key: <%= ENV["SIGNING_KEY"] %> otp_secret_key: <%= ENV["OTP_SECRET_KEY"] %> basic_auth: @@ -75,6 +76,7 @@ development: test: <<: *defaults secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a + encryption_service_salt: QUDyMoXyw2YXU8pHnpts3w9MyMpsMQ6BgP62obgCf7PQv signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017 otp_secret_key: 78ddda3679dc0ba2c99f50bcff04f49d862358dbeb7ead50368fdd6de14392be884ee10a204a0375b4b382e1a842fafe40d7858b7ab4796ec3a67c518d31112b api_entreprise: diff --git a/spec/services/encryption_service_spec.rb b/spec/services/encryption_service_spec.rb new file mode 100644 index 000000000..dc13b8fd2 --- /dev/null +++ b/spec/services/encryption_service_spec.rb @@ -0,0 +1,42 @@ +describe EncryptionService do + describe "#encrypt" do + subject { EncryptionService.new.encrypt(value) } + + context "with a nil value" do + let(:value) { nil } + + it { expect(subject).to be_nil } + end + + context "with a string value" do + let(:value) { "The quick brown fox jumps over the lazy dog" } + + it { expect(subject).to be_instance_of(String) } + it { expect(subject).to be_present } + it { expect(subject).not_to eq(value) } + end + end + + describe "#decrypt" do + subject { EncryptionService.new.decrypt(encrypted_value) } + + context "with a nil value" do + let(:encrypted_value) { nil } + + it { expect(subject).to be_nil } + end + + context "with a string value" do + let (:value) { "The quick brown fox jumps over the lazy dog" } + let(:encrypted_value) { EncryptionService.new.encrypt(value) } + + it { expect(subject).to eq(value) } + end + + context "with an invalid value" do + let(:encrypted_value) { "Gur dhvpx oebja sbk whzcf bire gur ynml qbt" } + + it { expect { subject }.to raise_exception StandardError } + end + end +end From 66c35fdffecff4711ca418549037536c3ad6640b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 16 Jul 2021 16:59:09 +0200 Subject: [PATCH 2/3] add encryptable_concern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François VANTOMME --- app/models/concerns/encryptable_concern.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/models/concerns/encryptable_concern.rb diff --git a/app/models/concerns/encryptable_concern.rb b/app/models/concerns/encryptable_concern.rb new file mode 100644 index 000000000..ebb9ff09c --- /dev/null +++ b/app/models/concerns/encryptable_concern.rb @@ -0,0 +1,21 @@ +module EncryptableConcern + extend ActiveSupport::Concern + + class_methods do + def attr_encrypted(*attributes) + attributes.each do |attribute| + define_method("#{attribute}=".to_sym) do |value| + self.public_send( + "encrypted_#{attribute}=".to_sym, + EncryptionService.new.encrypt(value) + ) + end + + define_method(attribute) do + value = self.public_send("encrypted_#{attribute}".to_sym) + EncryptionService.new.decrypt(value) if value.present? + end + end + end + end +end From b29bae47076575c89c8f1de0e88cda23ca53404c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Fri, 16 Jul 2021 17:03:58 +0200 Subject: [PATCH 3/3] a procedure has an encrypted api_particulier_token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François VANTOMME --- app/models/procedure.rb | 7 ++++++ ...ted_api_particulier_token_to_procedures.rb | 5 ++++ db/schema.rb | 1 + spec/models/procedure_spec.rb | 23 ++++++++++++------- 4 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20210317094648_add_encrypted_api_particulier_token_to_procedures.rb diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 823badc02..2e67d599f 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -18,6 +18,7 @@ # duree_conservation_dossiers_dans_ds :integer # duree_conservation_dossiers_hors_ds :integer # durees_conservation_required :boolean default(TRUE) +# encrypted_api_particulier_token :string # euro_flag :boolean default(FALSE) # experts_require_administrateur_invitation :boolean default(FALSE) # for_individual :boolean default(FALSE) @@ -47,6 +48,7 @@ class Procedure < ApplicationRecord include ProcedureStatsConcern + include EncryptableConcern include Discard::Model self.discard_column = :hidden_at @@ -56,6 +58,9 @@ class Procedure < ApplicationRecord MAX_DUREE_CONSERVATION_EXPORT = 3.hours MIN_WEIGHT = 350000 + + attr_encrypted :api_particulier_token + has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure belongs_to :draft_revision, class_name: 'ProcedureRevision', optional: false belongs_to :published_revision, class_name: 'ProcedureRevision', optional: true @@ -262,6 +267,7 @@ class Procedure < ApplicationRecord if: -> { new_record? || created_at > Date.new(2020, 11, 13) } validates :api_entreprise_token, jwt_token: true, allow_blank: true + validates :api_particulier_token, format: { with: /\A[A-Za-z0-9\-_=.]{15,}\z/, message: "n'est pas un jeton valide" }, allow_blank: true before_save :update_juridique_required after_initialize :ensure_path_exists @@ -440,6 +446,7 @@ class Procedure < ApplicationRecord if is_different_admin procedure.administrateurs = [admin] procedure.api_entreprise_token = nil + procedure.encrypted_api_particulier_token = nil else procedure.administrateurs = administrateurs end diff --git a/db/migrate/20210317094648_add_encrypted_api_particulier_token_to_procedures.rb b/db/migrate/20210317094648_add_encrypted_api_particulier_token_to_procedures.rb new file mode 100644 index 000000000..f9f8ac6ca --- /dev/null +++ b/db/migrate/20210317094648_add_encrypted_api_particulier_token_to_procedures.rb @@ -0,0 +1,5 @@ +class AddEncryptedAPIParticulierTokenToProcedures < ActiveRecord::Migration[6.0] + def change + add_column :procedures, :encrypted_api_particulier_token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 34629685e..e3c349b3e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -599,6 +599,7 @@ ActiveRecord::Schema.define(version: 2021_07_27_172504) do t.bigint "draft_revision_id" t.bigint "published_revision_id" t.boolean "allow_expert_review", default: true, null: false + t.string "encrypted_api_particulier_token" t.boolean "experts_require_administrateur_invitation", default: false t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state" t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id" diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index a24ad648a..dcba7b2a2 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -206,18 +206,25 @@ describe Procedure do end end - context 'api_entreprise_token' do - let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } - let(:invalid_token) { 'plouf' } - it { is_expected.to allow_value(valid_token).for(:api_entreprise_token) } - it { is_expected.not_to allow_value(invalid_token).for(:api_entreprise_token) } + context 'when juridique_required is false' do + let(:procedure) { build(:procedure, juridique_required: false, cadre_juridique: nil) } + + it { expect(procedure.valid?).to eq(true) } end end - context 'when juridique_required is false' do - let(:procedure) { build(:procedure, juridique_required: false, cadre_juridique: nil) } + context 'api_entreprise_token' do + let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } + let(:invalid_token) { 'plouf' } + it { is_expected.to allow_value(valid_token).for(:api_entreprise_token) } + it { is_expected.not_to allow_value(invalid_token).for(:api_entreprise_token) } + end - it { expect(procedure.valid?).to eq(true) } + context 'api_particulier_token' do + let(:valid_token) { "3841b13fa8032ed3c31d160d3437a76a" } + let(:invalid_token) { 'jet0n 1nvalide' } + it { is_expected.to allow_value(valid_token).for(:api_particulier_token) } + it { is_expected.not_to allow_value(invalid_token).for(:api_particulier_token) } end context 'monavis' do