Commit 37b80b40 authored by Gosia Ksionek's avatar Gosia Ksionek Committed by Mayra Cabrera

Create migration for token-iv pair

And new model

Add index to new table

Add saving token with iv

Add changelog entry

Add new column to new table

WIP

Change method to look for iv

Fix rubocop offences

Fix some rubocop offences

Fix more specs

Fix migration file and spec

Fix specs in migrations

WiP on specs

Add specs for model

WIP

Add spec for new method behauviour

Fix rubocop offences

Add cr remarks

Fix migration file

Add cr remark

Simplify one method

Add option for read only db

Add cr remarks

Add cr remarks

Add cr remarks
parent 78e313c0
...@@ -85,10 +85,18 @@ module TokenAuthenticatableStrategies ...@@ -85,10 +85,18 @@ module TokenAuthenticatableStrategies
end end
def find_by_encrypted_token(token, unscoped) def find_by_encrypted_token(token, unscoped)
encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce)
relation(unscoped).find_by(encrypted_field => encrypted_value) relation(unscoped).find_by(encrypted_field => encrypted_value)
end end
def find_hashed_iv(token)
token_record = TokenWithIv.find_by_plaintext_token(token)
token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
end
def insecure_strategy def insecure_strategy
@insecure_strategy ||= TokenAuthenticatableStrategies::Insecure @insecure_strategy ||= TokenAuthenticatableStrategies::Insecure
.new(klass, token_field, options) .new(klass, token_field, options)
......
# frozen_string_literal: true
# rubocop: todo Gitlab/NamespacedClass
class TokenWithIv < ApplicationRecord
validates :hashed_token, presence: true
validates :iv, presence: true
validates :hashed_plaintext_token, presence: true
def self.find_by_hashed_token(value)
find_by(hashed_token: ::Digest::SHA256.digest(value))
end
def self.find_by_plaintext_token(value)
find_by(hashed_plaintext_token: ::Digest::SHA256.digest(value))
end
def self.find_nonce_by_hashed_token(value)
return unless table_exists?
token_record = find_by_hashed_token(value)
token_record&.iv
end
end
---
title: Add token_with_iv table
merge_request:
author:
type: security
---
name: dynamic_nonce_creation
introduced_by_url:
rollout_issue_url:
milestone: '13.9'
type: development
group: group::manage
default_enabled: false
# frozen_string_literal: true
class CreateTokensWithIv < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :token_with_ivs do |t|
t.binary :hashed_token, null: false
t.binary :hashed_plaintext_token, null: false
t.binary :iv, null: false
t.index :hashed_token, name: 'index_token_with_ivs_on_hashed_token', unique: true, using: :btree
t.index :hashed_plaintext_token, name: 'index_token_with_ivs_on_hashed_plaintext_token', unique: true, using: :btree
end
end
end
...@@ -10,7 +10,7 @@ class EncryptFeatureFlagsClientsTokens < ActiveRecord::Migration[5.1] ...@@ -10,7 +10,7 @@ class EncryptFeatureFlagsClientsTokens < ActiveRecord::Migration[5.1]
def up def up
say_with_time("Encrypting tokens from operations_feature_flags_clients") do say_with_time("Encrypting tokens from operations_feature_flags_clients") do
FeatureFlagsClient.where('token_encrypted is NULL AND token IS NOT NULL').find_each do |feature_flags_client| FeatureFlagsClient.where('token_encrypted is NULL AND token IS NOT NULL').find_each do |feature_flags_client|
token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(feature_flags_client.token) token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(feature_flags_client.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
feature_flags_client.update!(token_encrypted: token_encrypted) feature_flags_client.update!(token_encrypted: token_encrypted)
end end
end end
......
...@@ -10,7 +10,7 @@ class EncryptDeployTokensTokens < ActiveRecord::Migration[5.1] ...@@ -10,7 +10,7 @@ class EncryptDeployTokensTokens < ActiveRecord::Migration[5.1]
def up def up
say_with_time("Encrypting tokens from deploy_tokens") do say_with_time("Encrypting tokens from deploy_tokens") do
DeploymentTokens.where('token_encrypted is NULL AND token IS NOT NULL').find_each(batch_size: 10000) do |deploy_token| DeploymentTokens.where('token_encrypted is NULL AND token IS NOT NULL').find_each(batch_size: 10000) do |deploy_token|
token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(deploy_token.token) token_encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(deploy_token.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
deploy_token.update!(token_encrypted: token_encrypted) deploy_token.update!(token_encrypted: token_encrypted)
end end
end end
......
dde424c434c78e22087123fa30eec75c07268a9079fea44339915747aae235e0
\ No newline at end of file
...@@ -17402,6 +17402,22 @@ CREATE SEQUENCE todos_id_seq ...@@ -17402,6 +17402,22 @@ CREATE SEQUENCE todos_id_seq
ALTER SEQUENCE todos_id_seq OWNED BY todos.id; ALTER SEQUENCE todos_id_seq OWNED BY todos.id;
CREATE TABLE token_with_ivs (
id bigint NOT NULL,
hashed_token bytea NOT NULL,
hashed_plaintext_token bytea NOT NULL,
iv bytea NOT NULL
);
CREATE SEQUENCE token_with_ivs_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE token_with_ivs_id_seq OWNED BY token_with_ivs.id;
CREATE TABLE trending_projects ( CREATE TABLE trending_projects (
id integer NOT NULL, id integer NOT NULL,
project_id integer NOT NULL project_id integer NOT NULL
...@@ -19119,6 +19135,8 @@ ALTER TABLE ONLY timelogs ALTER COLUMN id SET DEFAULT nextval('timelogs_id_seq': ...@@ -19119,6 +19135,8 @@ ALTER TABLE ONLY timelogs ALTER COLUMN id SET DEFAULT nextval('timelogs_id_seq':
ALTER TABLE ONLY todos ALTER COLUMN id SET DEFAULT nextval('todos_id_seq'::regclass); ALTER TABLE ONLY todos ALTER COLUMN id SET DEFAULT nextval('todos_id_seq'::regclass);
ALTER TABLE ONLY token_with_ivs ALTER COLUMN id SET DEFAULT nextval('token_with_ivs_id_seq'::regclass);
ALTER TABLE ONLY trending_projects ALTER COLUMN id SET DEFAULT nextval('trending_projects_id_seq'::regclass); ALTER TABLE ONLY trending_projects ALTER COLUMN id SET DEFAULT nextval('trending_projects_id_seq'::regclass);
ALTER TABLE ONLY u2f_registrations ALTER COLUMN id SET DEFAULT nextval('u2f_registrations_id_seq'::regclass); ALTER TABLE ONLY u2f_registrations ALTER COLUMN id SET DEFAULT nextval('u2f_registrations_id_seq'::regclass);
...@@ -20641,6 +20659,9 @@ ALTER TABLE ONLY timelogs ...@@ -20641,6 +20659,9 @@ ALTER TABLE ONLY timelogs
ALTER TABLE ONLY todos ALTER TABLE ONLY todos
ADD CONSTRAINT todos_pkey PRIMARY KEY (id); ADD CONSTRAINT todos_pkey PRIMARY KEY (id);
ALTER TABLE ONLY token_with_ivs
ADD CONSTRAINT token_with_ivs_pkey PRIMARY KEY (id);
ALTER TABLE ONLY trending_projects ALTER TABLE ONLY trending_projects
ADD CONSTRAINT trending_projects_pkey PRIMARY KEY (id); ADD CONSTRAINT trending_projects_pkey PRIMARY KEY (id);
...@@ -23165,6 +23186,10 @@ CREATE INDEX index_todos_on_user_id_and_id_done ON todos USING btree (user_id, i ...@@ -23165,6 +23186,10 @@ CREATE INDEX index_todos_on_user_id_and_id_done ON todos USING btree (user_id, i
CREATE INDEX index_todos_on_user_id_and_id_pending ON todos USING btree (user_id, id) WHERE ((state)::text = 'pending'::text); CREATE INDEX index_todos_on_user_id_and_id_pending ON todos USING btree (user_id, id) WHERE ((state)::text = 'pending'::text);
CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_plaintext_token ON token_with_ivs USING btree (hashed_plaintext_token);
CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_token ON token_with_ivs USING btree (hashed_token);
CREATE UNIQUE INDEX index_trending_projects_on_project_id ON trending_projects USING btree (project_id); CREATE UNIQUE INDEX index_trending_projects_on_project_id ON trending_projects USING btree (project_id);
CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING btree (key_handle); CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING btree (key_handle);
......
...@@ -23,7 +23,7 @@ RSpec.describe 'Geo read-only message', :geo do ...@@ -23,7 +23,7 @@ RSpec.describe 'Geo read-only message', :geo do
context 'when in maintenance mode' do context 'when in maintenance mode' do
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
end end
it_behaves_like 'Read-only instance', /This GitLab instance is undergoing maintenance and is operating in read\-only mode./ it_behaves_like 'Read-only instance', /This GitLab instance is undergoing maintenance and is operating in read\-only mode./
......
...@@ -22,7 +22,7 @@ RSpec.describe ApplicationHelper do ...@@ -22,7 +22,7 @@ RSpec.describe ApplicationHelper do
context 'maintenance mode' do context 'maintenance mode' do
context 'enabled' do context 'enabled' do
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
end end
it 'returns default message' do it 'returns default message' do
...@@ -48,7 +48,7 @@ RSpec.describe ApplicationHelper do ...@@ -48,7 +48,7 @@ RSpec.describe ApplicationHelper do
context 'disabled' do context 'disabled' do
it 'returns nil' do it 'returns nil' do
stub_application_setting(maintenance_mode: false) stub_maintenance_mode_setting(false)
expect(helper.read_only_message).to be_nil expect(helper.read_only_message).to be_nil
end end
...@@ -60,7 +60,7 @@ RSpec.describe ApplicationHelper do ...@@ -60,7 +60,7 @@ RSpec.describe ApplicationHelper do
context 'maintenance mode on' do context 'maintenance mode on' do
it 'returns messages for both' do it 'returns messages for both' do
expect(Gitlab::Geo).to receive(:secondary?).twice { true } expect(Gitlab::Geo).to receive(:secondary?).twice { true }
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
expect(helper.read_only_message).to match(/you must visit the primary site/) expect(helper.read_only_message).to match(/you must visit the primary site/)
expect(helper.read_only_message).to match(/#{default_maintenance_mode_message}/) expect(helper.read_only_message).to match(/#{default_maintenance_mode_message}/)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::CryptoHelper do
include ::EE::GeoHelpers
describe '.read_only?' do
context 'with Geo enabled' do
before do
allow(Gitlab::Geo).to receive(:enabled?) { true }
allow(Gitlab::Geo).to receive(:current_node) { geo_node }
end
context 'is Geo secondary node' do
let(:geo_node) { create(:geo_node) }
it 'returns true' do
expect(described_class.read_only?).to be_truthy
end
end
context 'is Geo primary node' do
let(:geo_node) { create(:geo_node, :primary) }
it 'returns false when is Geo primary node' do
expect(described_class.read_only?).to be_falsey
end
end
end
end
end
...@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database do ...@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database do
context 'in maintenance mode' do context 'in maintenance mode' do
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
end end
it 'returns true' do it 'returns true' do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Middleware::ReadOnly do RSpec.describe Gitlab::Middleware::ReadOnly do
context 'when maintenance mode is on' do context 'when maintenance mode is on' do
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
end end
it_behaves_like 'write access for a read-only GitLab (EE) instance in maintenance mode' it_behaves_like 'write access for a read-only GitLab (EE) instance in maintenance mode'
...@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Middleware::ReadOnly do ...@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Middleware::ReadOnly do
context 'when maintenance mode is not on' do context 'when maintenance mode is not on' do
before do before do
stub_application_setting(maintenance_mode: false) stub_maintenance_mode_setting(false)
end end
it_behaves_like 'write access for a read-only GitLab (EE) instance' it_behaves_like 'write access for a read-only GitLab (EE) instance'
......
...@@ -758,7 +758,7 @@ RSpec.describe Gitlab::GitAccess do ...@@ -758,7 +758,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when maintenance mode is enabled' do context 'when maintenance mode is enabled' do
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
end end
it 'blocks git push' do it 'blocks git push' do
...@@ -770,7 +770,7 @@ RSpec.describe Gitlab::GitAccess do ...@@ -770,7 +770,7 @@ RSpec.describe Gitlab::GitAccess do
context 'when maintenance mode is disabled' do context 'when maintenance mode is disabled' do
before do before do
stub_application_setting(maintenance_mode: false) stub_maintenance_mode_setting(false)
end end
it 'allows git push' do it 'allows git push' do
......
...@@ -12,8 +12,8 @@ RSpec.describe NullifyFeatureFlagPlaintextTokens do ...@@ -12,8 +12,8 @@ RSpec.describe NullifyFeatureFlagPlaintextTokens do
let!(:project1) { projects.create!(namespace_id: namespace.id, name: 'Project 1') } let!(:project1) { projects.create!(namespace_id: namespace.id, name: 'Project 1') }
let!(:project2) { projects.create!(namespace_id: namespace.id, name: 'Project 2') } let!(:project2) { projects.create!(namespace_id: namespace.id, name: 'Project 2') }
let(:secret1_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret1') } let(:secret1_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret1', nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
let(:secret2_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret2') } let(:secret2_encrypted) { Gitlab::CryptoHelper.aes256_gcm_encrypt('secret2', nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
before do before do
feature_flags_clients.create!(token: 'secret1', token_encrypted: secret1_encrypted, project_id: project1.id) feature_flags_clients.create!(token: 'secret1', token_encrypted: secret1_encrypted, project_id: project1.id)
......
...@@ -248,7 +248,7 @@ RSpec.describe API::Internal::Base do ...@@ -248,7 +248,7 @@ RSpec.describe API::Internal::Base do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
project.add_developer(user) project.add_developer(user)
end end
......
...@@ -19,7 +19,7 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do ...@@ -19,7 +19,7 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
end end
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
project.add_developer(current_user) project.add_developer(current_user)
end end
......
...@@ -7,7 +7,7 @@ RSpec.shared_examples 'write access for a read-only GitLab (EE) instance in main ...@@ -7,7 +7,7 @@ RSpec.shared_examples 'write access for a read-only GitLab (EE) instance in main
include_context 'with a mocked GitLab instance' include_context 'with a mocked GitLab instance'
before do before do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
end end
context 'normal requests to a read-only GitLab instance' do context 'normal requests to a read-only GitLab instance' do
......
...@@ -118,6 +118,7 @@ module Gitlab ...@@ -118,6 +118,7 @@ module Gitlab
def self.maintenance_mode? def self.maintenance_mode?
return false unless ::Feature.enabled?(:maintenance_mode) return false unless ::Feature.enabled?(:maintenance_mode)
return false unless ::Gitlab::CurrentSettings.current_application_settings?
::Gitlab::CurrentSettings.maintenance_mode ::Gitlab::CurrentSettings.maintenance_mode
end end
......
...@@ -6,25 +6,44 @@ module Gitlab ...@@ -6,25 +6,44 @@ module Gitlab
AES256_GCM_OPTIONS = { AES256_GCM_OPTIONS = {
algorithm: 'aes-256-gcm', algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32, key: Settings.attr_encrypted_db_key_base_32
iv: Settings.attr_encrypted_db_key_base_12
}.freeze }.freeze
AES256_GCM_IV_STATIC = Settings.attr_encrypted_db_key_base_12
def sha256(value) def sha256(value)
salt = Settings.attr_encrypted_db_key_base_truncated salt = Settings.attr_encrypted_db_key_base_truncated
::Digest::SHA256.base64digest("#{value}#{salt}") ::Digest::SHA256.base64digest("#{value}#{salt}")
end end
def aes256_gcm_encrypt(value) def aes256_gcm_encrypt(value, nonce: nil)
encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value)) aes256_gcm_encrypt_using_static_nonce(value)
Base64.strict_encode64(encrypted_token)
end end
def aes256_gcm_decrypt(value) def aes256_gcm_decrypt(value)
return unless value return unless value
nonce = Feature.enabled?(:dynamic_nonce_creation) ? dynamic_nonce(value) : AES256_GCM_IV_STATIC
encrypted_token = Base64.decode64(value) encrypted_token = Base64.decode64(value)
Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token)) decrypted_token = Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token, iv: nonce))
decrypted_token
end
def dynamic_nonce(value)
TokenWithIv.find_nonce_by_hashed_token(value) || AES256_GCM_IV_STATIC
end
def aes256_gcm_encrypt_using_static_nonce(value)
create_encrypted_token(value, AES256_GCM_IV_STATIC)
end
def read_only?
Gitlab::Database.read_only?
end
def create_encrypted_token(value, iv)
encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value, iv: iv))
Base64.strict_encode64(encrypted_token)
end end
end end
end end
...@@ -7,6 +7,10 @@ module Gitlab ...@@ -7,6 +7,10 @@ module Gitlab
Gitlab::SafeRequestStore.fetch(:current_application_settings) { ensure_application_settings! } Gitlab::SafeRequestStore.fetch(:current_application_settings) { ensure_application_settings! }
end end
def current_application_settings?
Gitlab::SafeRequestStore.exist?(:current_application_settings) || ::ApplicationSetting.current.present?
end
def expire_current_application_settings def expire_current_application_settings
::ApplicationSetting.expire ::ApplicationSetting.expire
Gitlab::SafeRequestStore.delete(:current_application_settings) Gitlab::SafeRequestStore.delete(:current_application_settings)
......
...@@ -27,7 +27,8 @@ RSpec.describe Admin::RunnersController do ...@@ -27,7 +27,8 @@ RSpec.describe Admin::RunnersController do
# There is still an N+1 query for `runner.builds.count` # There is still an N+1 query for `runner.builds.count`
# We also need to add 1 because it takes 2 queries to preload tags # We also need to add 1 because it takes 2 queries to preload tags
expect { get :index }.not_to exceed_query_limit(control_count + 6) # also looking for token nonce requires database queries
expect { get :index }.not_to exceed_query_limit(control_count + 16)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to have_content('tag1') expect(response.body).to have_content('tag1')
......
# frozen_string_literal: true
FactoryBot.define do
factory :token_with_iv do
hashed_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
iv { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
hashed_plaintext_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
end
end
...@@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do ...@@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do
expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z} expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z}
expect(encrypted).not_to include "\n" expect(encrypted).not_to include "\n"
end end
it 'does not save hashed token with iv value in database' do
expect { described_class.aes256_gcm_encrypt('some-value') }.not_to change { TokenWithIv.count }
end
it 'encrypts using static iv' do
expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: described_class::AES256_GCM_IV_STATIC)).and_return('hashed_value')
described_class.aes256_gcm_encrypt('some-value')
end
end end
describe '.aes256_gcm_decrypt' do describe '.aes256_gcm_decrypt' do
let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') } before do
stub_feature_flags(dynamic_nonce_creation: false)
end
context 'when token was encrypted using static nonce' do
let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) }
it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq 'some-value'
end
it 'decrypts a value when it ends with a new line character' do
decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
it 'correctly decrypts encrypted string' do expect(decrypted).to eq 'some-value'
decrypted = described_class.aes256_gcm_decrypt(encrypted) end
expect(decrypted).to eq 'some-value' it 'does not save hashed token with iv value in database' do
expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
end
context 'with feature flag switched on' do
before do
stub_feature_flags(dynamic_nonce_creation: true)
end
it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq 'some-value'
end
end
end end
it 'decrypts a value when it ends with a new line character' do context 'when token was encrypted using random nonce' do
decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n") let(:value) { 'random-value' }
# for compatibility with tokens encrypted using dynamic nonce
let!(:encrypted) do
iv = create_nonce
encrypted_token = described_class.create_encrypted_token(value, iv)
TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv)
encrypted_token
end
before do
stub_feature_flags(dynamic_nonce_creation: true)
end
expect(decrypted).to eq 'some-value' it 'correctly decrypts encrypted string' do
decrypted = described_class.aes256_gcm_decrypt(encrypted)
expect(decrypted).to eq value
end
it 'does not save hashed token with iv value in database' do
expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
end
end end
end end
def create_nonce
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt # Required before '#random_iv' can be called
cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used.
end
end end
...@@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do ...@@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do
end end
end end
end end
describe '#current_application_settings?', :use_clean_rails_memory_store_caching do
before do
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original
end
it 'returns true when settings exist' do
create(:application_setting,
home_page_url: 'http://mydomain.com',
signup_enabled: false)
expect(described_class.current_application_settings?).to eq(true)
end
it 'returns false when settings do not exist' do
expect(described_class.current_application_settings?).to eq(false)
end
context 'with cache', :request_store do
include_context 'with settings in cache'
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(described_class.current_application_settings?).to eq(true)
end
end
end
end end
...@@ -332,13 +332,13 @@ RSpec.describe Gitlab do ...@@ -332,13 +332,13 @@ RSpec.describe Gitlab do
describe '.maintenance_mode?' do describe '.maintenance_mode?' do
it 'returns true when maintenance mode is enabled' do it 'returns true when maintenance mode is enabled' do
stub_application_setting(maintenance_mode: true) stub_maintenance_mode_setting(true)
expect(described_class.maintenance_mode?).to eq(true) expect(described_class.maintenance_mode?).to eq(true)
end end
it 'returns false when maintenance mode is disabled' do it 'returns false when maintenance mode is disabled' do
stub_application_setting(maintenance_mode: false) stub_maintenance_mode_setting(false)
expect(described_class.maintenance_mode?).to eq(false) expect(described_class.maintenance_mode?).to eq(false)
end end
......
...@@ -8,7 +8,7 @@ RSpec.describe EncryptFeatureFlagsClientsTokens do ...@@ -8,7 +8,7 @@ RSpec.describe EncryptFeatureFlagsClientsTokens do
let(:feature_flags_clients) { table(:operations_feature_flags_clients) } let(:feature_flags_clients) { table(:operations_feature_flags_clients) }
let(:projects) { table(:projects) } let(:projects) { table(:projects) }
let(:plaintext) { "secret-token" } let(:plaintext) { "secret-token" }
let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext) } let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
describe '#up' do describe '#up' do
it 'keeps plaintext token the same and populates token_encrypted if not present' do it 'keeps plaintext token the same and populates token_encrypted if not present' do
......
...@@ -358,7 +358,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -358,7 +358,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
it 'calls .destroy_sessions' do it 'calls .destroy_sessions' do
expect(ActiveSession).to( expect(ActiveSession).to(
receive(:destroy_sessions) receive(:destroy_sessions)
.with(anything, user, [active_session.public_id, rack_session.public_id, rack_session.private_id])) .with(anything, user, [encrypted_active_session_id, rack_session.public_id, rack_session.private_id]))
subject subject
end end
......
...@@ -54,7 +54,7 @@ RSpec.describe ApplicationSetting, 'TokenAuthenticatable' do ...@@ -54,7 +54,7 @@ RSpec.describe ApplicationSetting, 'TokenAuthenticatable' do
it 'persists new token as an encrypted string' do it 'persists new token as an encrypted string' do
expect(subject).to eq settings.reload.runners_registration_token expect(subject).to eq settings.reload.runners_registration_token
expect(settings.read_attribute('runners_registration_token_encrypted')) expect(settings.read_attribute('runners_registration_token_encrypted'))
.to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject) .to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
expect(settings).to be_persisted expect(settings).to be_persisted
end end
...@@ -243,7 +243,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do ...@@ -243,7 +243,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
it 'persists new token as an encrypted string' do it 'persists new token as an encrypted string' do
build.ensure_token! build.ensure_token!
encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token) encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
expect(build.read_attribute('token_encrypted')).to eq encrypted expect(build.read_attribute('token_encrypted')).to eq encrypted
end end
......
...@@ -68,6 +68,10 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do ...@@ -68,6 +68,10 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
context 'when using optional strategy' do context 'when using optional strategy' do
let(:options) { { encrypted: :optional } } let(:options) { { encrypted: :optional } }
before do
stub_feature_flags(dynamic_nonce_creation: false)
end
it 'returns decrypted token when an encrypted token is present' do it 'returns decrypted token when an encrypted token is present' do
allow(instance).to receive(:read_attribute) allow(instance).to receive(:read_attribute)
.with('some_field_encrypted') .with('some_field_encrypted')
...@@ -124,7 +128,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do ...@@ -124,7 +128,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
it 'writes encrypted token and removes plaintext token and returns it' do it 'writes encrypted token and removes plaintext token and returns it' do
expect(instance).to receive(:[]=) expect(instance).to receive(:[]=)
.with('some_field_encrypted', encrypted) .with('some_field_encrypted', any_args)
expect(instance).to receive(:[]=) expect(instance).to receive(:[]=)
.with('some_field', nil) .with('some_field', nil)
...@@ -137,7 +141,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do ...@@ -137,7 +141,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
it 'writes encrypted token and writes plaintext token' do it 'writes encrypted token and writes plaintext token' do
expect(instance).to receive(:[]=) expect(instance).to receive(:[]=)
.with('some_field_encrypted', encrypted) .with('some_field_encrypted', any_args)
expect(instance).to receive(:[]=) expect(instance).to receive(:[]=)
.with('some_field', 'my-value') .with('some_field', 'my-value')
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TokenWithIv do
describe 'validations' do
it { is_expected.to validate_presence_of :hashed_token }
it { is_expected.to validate_presence_of :iv }
it { is_expected.to validate_presence_of :hashed_plaintext_token }
end
describe '.find_by_hashed_token' do
it 'only includes matching record' do
matching_record = create(:token_with_iv, hashed_token: ::Digest::SHA256.digest('hashed-token'))
create(:token_with_iv)
expect(described_class.find_by_hashed_token('hashed-token')).to eq(matching_record)
end
end
describe '.find_by_plaintext_token' do
it 'only includes matching record' do
matching_record = create(:token_with_iv, hashed_plaintext_token: ::Digest::SHA256.digest('hashed-token'))
create(:token_with_iv)
expect(described_class.find_by_plaintext_token('hashed-token')).to eq(matching_record)
end
end
end
...@@ -282,6 +282,8 @@ RSpec.configure do |config| ...@@ -282,6 +282,8 @@ RSpec.configure do |config|
current_user_mode.send(:user)&.admin? current_user_mode.send(:user)&.admin?
end end
end end
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(false)
end end
config.around(:example, :quarantine) do |example| config.around(:example, :quarantine) do |example|
......
...@@ -121,6 +121,12 @@ module StubConfiguration ...@@ -121,6 +121,12 @@ module StubConfiguration
allow(::Gitlab.config.packages).to receive_messages(to_settings(messages)) allow(::Gitlab.config.packages).to receive_messages(to_settings(messages))
end end
def stub_maintenance_mode_setting(value)
allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true)
stub_application_setting(maintenance_mode: value)
end
private private
# Modifies stubbed messages to also stub possible predicate versions # Modifies stubbed messages to also stub possible predicate versions
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment