Class: Maquina::Credentials

Inherits:
Object
  • Object
show all
Defined in:
lib/maquina/credentials.rb,
lib/maquina/credentials/cli.rb,
lib/maquina_credentials/version.rb

Defined Under Namespace

Classes: CLI, DecryptionFailed, MasterKeyMissing

Constant Summary collapse

DEFAULT_CREDENTIALS_PATH =
"credentials.yml.enc"
ENV_KEY =
"MAQUINA_MASTER_KEY"
FILE_ENV_KEY =
"MAQUINA_CREDENTIALS_FILE"
CIPHER =
"aes-256-gcm"
KEY_LENGTH =
32
IV_LENGTH =
12
AUTH_TAG_LENGTH =
16
HKDF_INFO =
"maquina-credentials-v1"
VERSION =
"0.1.0"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(credentials_path: nil) ⇒ Credentials

Returns a new instance of Credentials.



119
120
121
122
123
# File 'lib/maquina/credentials.rb', line 119

def initialize(credentials_path: nil)
  @credentials_path = self.class.resolve_credentials_path(credentials_path)
  @credentials = nil
  @loaded = false
end

Class Method Details

.decrypt(encrypted, raw_key) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/maquina/credentials.rb', line 48

def self.decrypt(encrypted, raw_key)
  decoded = strict_base64_decode(encrypted)
  raise DecryptionFailed if decoded.bytesize < IV_LENGTH + AUTH_TAG_LENGTH

  iv = decoded.byteslice(0, IV_LENGTH)
  auth_tag = decoded.byteslice(-AUTH_TAG_LENGTH, AUTH_TAG_LENGTH)
  ciphertext = decoded.byteslice(IV_LENGTH, decoded.bytesize - IV_LENGTH - AUTH_TAG_LENGTH)

  cipher = OpenSSL::Cipher.new(CIPHER)
  cipher.decrypt
  cipher.key = derive_key(raw_key)
  cipher.iv = iv
  cipher.auth_tag = auth_tag
  cipher.auth_data = ""
  cipher.update(ciphertext) + cipher.final
rescue ArgumentError, OpenSSL::Cipher::CipherError
  raise DecryptionFailed
end

.deep_stringify(obj) ⇒ Object



108
109
110
111
112
113
114
115
116
117
# File 'lib/maquina/credentials.rb', line 108

def self.deep_stringify(obj)
  case obj
  when Hash
    obj.transform_keys(&:to_s).transform_values { |value| deep_stringify(value) }
  when Array
    obj.map { |value| deep_stringify(value) }
  else
    obj
  end
end

.derive_key(raw_key) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/maquina/credentials.rb', line 79

def self.derive_key(raw_key)
  raw_key = raw_key.b
  return raw_key[0, KEY_LENGTH] if raw_key.bytesize == KEY_LENGTH

  OpenSSL::KDF.hkdf(
    raw_key,
    salt: "",
    info: HKDF_INFO,
    length: KEY_LENGTH,
    hash: "SHA256"
  )
end

.encrypt(payload, raw_key) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
# File 'lib/maquina/credentials.rb', line 36

def self.encrypt(payload, raw_key)
  cipher = OpenSSL::Cipher.new(CIPHER)
  cipher.encrypt
  cipher.key = derive_key(raw_key)
  iv = cipher.random_iv
  cipher.iv = iv
  cipher.auth_data = ""

  ciphertext = cipher.update(payload) + cipher.final
  strict_base64_encode(iv + ciphertext + cipher.auth_tag)
end

.master_keyObject

Raises:



92
93
94
95
96
97
# File 'lib/maquina/credentials.rb', line 92

def self.master_key
  key = ENV[ENV_KEY]
  raise MasterKeyMissing if key.nil? || key.empty?

  key
end

.resolve_credentials_path(credentials_path = nil) ⇒ Object



99
100
101
102
103
104
105
106
# File 'lib/maquina/credentials.rb', line 99

def self.resolve_credentials_path(credentials_path = nil)
  return credentials_path unless credentials_path.nil? || credentials_path.empty?

  env_path = ENV[FILE_ENV_KEY]
  return env_path unless env_path.nil? || env_path.empty?

  File.join(Dir.pwd, DEFAULT_CREDENTIALS_PATH)
end

.strict_base64_decode(encoded) ⇒ Object



71
72
73
74
75
76
77
# File 'lib/maquina/credentials.rb', line 71

def self.strict_base64_decode(encoded)
  unless encoded.match?(/\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?\z/)
    raise ArgumentError
  end

  encoded.unpack1("m0")
end

.strict_base64_encode(bytes) ⇒ Object



67
68
69
# File 'lib/maquina/credentials.rb', line 67

def self.strict_base64_encode(bytes)
  [bytes].pack("m0")
end

.write(hash, credentials_path: nil) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/maquina/credentials.rb', line 22

def self.write(hash, credentials_path: nil)
  credentials_path = resolve_credentials_path(credentials_path)
  payload = YAML.dump(deep_stringify(hash))
  encrypted = encrypt(payload, master_key)
  tmp_path = "#{credentials_path}.tmp.#{Process.pid}"

  FileUtils.mkdir_p(File.dirname(credentials_path))
  File.write(tmp_path, encrypted)
  File.rename(tmp_path, credentials_path)
  File.chmod(0o600, credentials_path)
ensure
  File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
end

Instance Method Details

#read(path) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/maquina/credentials.rb', line 125

def read(path)
  return "" if path.nil? || path.empty?

  value = path.split(".").reduce(credentials) do |current, key|
    break unless current.is_a?(Hash)

    current[key]
  end

  value.nil? ? "" : value.to_s
end