Class: Gitlab::OtpKeyRotator

Inherits:
Object
  • Object
show all
Defined in:
lib/gitlab/otp_key_rotator.rb

Overview

The otp_key_base param is used to encrypt the User#otp_secret attribute.

When otp_key_base is changed, it invalidates the current encrypted values of User#otp_secret. This class can be used to decrypt all the values with the old key, encrypt them with the new key, and and update the database with the new values.

For persistence between runs, a CSV file is used with the following columns:

user_id, old_value, new_value

Only the encrypted values are stored in this file.

As users may have their 2FA settings changed at any time, this is only guaranteed to be safe if run offline.

Constant Summary collapse

HEADERS =
%w[user_id old_value new_value].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename) ⇒ OtpKeyRotator

Create a new rotator. filename is used to store values by calculate!, and to update the database with new and old values in apply! and rollback!, respectively.



27
28
29
# File 'lib/gitlab/otp_key_rotator.rb', line 27

def initialize(filename)
  @filename = filename
end

Instance Attribute Details

#filenameObject (readonly)

Returns the value of attribute filename.



22
23
24
# File 'lib/gitlab/otp_key_rotator.rb', line 22

def filename
  @filename
end

Instance Method Details

#rollback!Object

rubocop: disable CodeReuse/ActiveRecord



56
57
58
59
60
61
62
# File 'lib/gitlab/otp_key_rotator.rb', line 56

def rollback!
  User.transaction do
    CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row|
      User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value'])
    end
  end
end

#rotate!(old_key:, new_key:) ⇒ Object

rubocop: disable CodeReuse/ActiveRecord

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/gitlab/otp_key_rotator.rb', line 32

def rotate!(old_key:, new_key:)
  old_key ||= Gitlab::Application.secrets.otp_key_base

  raise ArgumentError, "Old key is the same as the new key" if old_key == new_key
  raise ArgumentError, "New key is too short! Must be 256 bits" if new_key.size < 64

  write_csv do |csv|
    User.transaction do
      User.with_two_factor.in_batches do |relation| # rubocop: disable Cop/InBatches
        rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
        rows.each do |row|
          user = %i[id ciphertext iv salt].zip(row).to_h
          new_value = reencrypt(user, old_key, new_key)

          User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value)
          csv << [user[:id], user[:ciphertext], new_value]
        end
      end
    end
  end
end