Class: Robocap::SDK::KeyVault

Inherits:
Object
  • Object
show all
Defined in:
lib/robocap/sdk/key_vault.rb

Constant Summary collapse

RSA_VERSION_DIR_RE =
/\Av(\d+)\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sdk_root) ⇒ KeyVault

Returns a new instance of KeyVault.



19
20
21
22
# File 'lib/robocap/sdk/key_vault.rb', line 19

def initialize(sdk_root)
  @sdk_root  = Pathname(sdk_root)
  @keys_root = Config.keys_vault_root(@sdk_root)
end

Instance Attribute Details

#sdk_rootObject (readonly)

Returns the value of attribute sdk_root.



24
25
26
# File 'lib/robocap/sdk/key_vault.rb', line 24

def sdk_root
  @sdk_root
end

Class Method Details

.private_pem_path_for_dir(key_dir) ⇒ Object



51
52
53
# File 'lib/robocap/sdk/key_vault.rb', line 51

def self.private_pem_path_for_dir(key_dir)
  Pathname(key_dir).join('private.pem')
end

.public_pem_path_for_dir(key_dir) ⇒ Object



47
48
49
# File 'lib/robocap/sdk/key_vault.rb', line 47

def self.public_pem_path_for_dir(key_dir)
  Pathname(key_dir).join('public.pem')
end

Instance Method Details

#customer_root(customer_id) ⇒ Object



26
27
28
29
# File 'lib/robocap/sdk/key_vault.rb', line 26

def customer_root(customer_id)
  Config.validate_customer_id!(customer_id)
  @keys_root.join(customer_id)
end

#delete_rsa_key_dir(customer_id, key_dir) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/robocap/sdk/key_vault.rb', line 205

def delete_rsa_key_dir(customer_id, key_dir)
  Config.validate_customer_id!(customer_id)
  unless exists_customer?(customer_id)
    raise Error.new(
      code: ErrorCode::ERR_CUSTOMER_NOT_FOUND,
      message: "Customer not found: #{customer_id}",
    )
  end
  resolved_rsa_dir = Pathname(rsa_dir(customer_id).realpath)
  resolved = Pathname(key_dir).expand_path
  resolved = Pathname(resolved.realpath) if resolved.exist?
  unless resolved.parent == resolved_rsa_dir
    raise Error.new(
      code: ErrorCode::ERR_RSA_VERSION_MISSING,
      message: "Key folder is not under rsa/ for #{customer_id}",
    )
  end
  unless valid_rsa_key_dir?(resolved)
    raise Error.new(
      code: ErrorCode::ERR_RSA_VERSION_MISSING,
      message: "Key folder missing public.pem or private.pem: #{resolved.basename}",
    )
  end
  begin
    FileUtils.remove_entry_secure(resolved)
  rescue SystemCallError => exc
    raise Error.new(
      code: ErrorCode::ERR_VAULT_IO,
      message: "Failed to delete key folder #{resolved.basename}: #{exc.message}",
    )
  end
end

#delete_rsa_version(customer_id, version) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/robocap/sdk/key_vault.rb', line 170

def delete_rsa_version(customer_id, version)
  Config.validate_customer_id!(customer_id)
  unless exists_customer?(customer_id)
    raise Error.new(
      code: ErrorCode::ERR_CUSTOMER_NOT_FOUND,
      message: "Customer not found: #{customer_id}",
    )
  end
  dir = rsa_version_dir(customer_id, version)
  unless dir.directory?
    raise Error.new(
      code: ErrorCode::ERR_RSA_VERSION_MISSING,
      message: "RSA version v#{version} not found for #{customer_id}",
    )
  end
  begin
    FileUtils.remove_entry_secure(dir)
  rescue SystemCallError => exc
    raise Error.new(
      code: ErrorCode::ERR_VAULT_IO,
      message: "Failed to delete RSA version v#{version}: #{exc.message}",
    )
  end
end

#exists_customer?(customer_id) ⇒ Boolean

Returns:

  • (Boolean)


59
60
61
# File 'lib/robocap/sdk/key_vault.rb', line 59

def exists_customer?(customer_id)
  customer_root(customer_id).directory?
end

#get_latest_public_key(customer_id) ⇒ Object



106
107
108
# File 'lib/robocap/sdk/key_vault.rb', line 106

def get_latest_public_key(customer_id)
  get_public_key(customer_id, get_latest_rsa_version(customer_id))
end

#get_latest_rsa_version(customer_id) ⇒ Object



73
74
75
76
77
78
79
80
81
82
# File 'lib/robocap/sdk/key_vault.rb', line 73

def get_latest_rsa_version(customer_id)
  versions = list_rsa_versions(customer_id)
  if versions.empty?
    raise Error.new(
      code: ErrorCode::ERR_RSA_NOT_IMPORTED,
      message: "No RSA keys imported for customer #{customer_id}",
    )
  end
  versions.max
end

#get_private_key(customer_id, version) ⇒ Object



95
96
97
98
99
100
101
102
103
104
# File 'lib/robocap/sdk/key_vault.rb', line 95

def get_private_key(customer_id, version)
  path = private_pem(customer_id, version)
  unless path.file?
    raise Error.new(
      code: ErrorCode::ERR_RSA_VERSION_MISSING,
      message: "RSA private key v#{version} not found for #{customer_id}",
    )
  end
  load_private_key_from(path)
end

#get_public_key(customer_id, version) ⇒ Object



84
85
86
87
88
89
90
91
92
93
# File 'lib/robocap/sdk/key_vault.rb', line 84

def get_public_key(customer_id, version)
  path = public_pem(customer_id, version)
  unless path.file?
    raise Error.new(
      code: ErrorCode::ERR_RSA_VERSION_MISSING,
      message: "RSA public key v#{version} not found for #{customer_id}",
    )
  end
  load_public_key_from(path)
end

#import_rsa_version(customer_id:, public_pem:, private_pem:, meta:) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/robocap/sdk/key_vault.rb', line 121

def import_rsa_version(customer_id:, public_pem:, private_pem:, meta:)
  Config.validate_customer_id!(customer_id)
  version = meta.rsa_key_version
  pub_key  = OpenSSL::PKey::RSA.new(public_pem)
  priv_key = OpenSSL::PKey::RSA.new(private_pem)

  if pub_key.private? || !priv_key.private?
    raise Error.new(code: ErrorCode::ERR_RSA_IMPORT_INVALID, message: 'Invalid RSA key pair PEM')
  end

  unless Config::RSA_BITS_ALLOWED.include?(meta.rsa_bits)
    raise Error.new(
      code: ErrorCode::ERR_INVALID_RSA_BITS,
      message: "RSA rsa_bits must be one of #{Config::RSA_BITS_ALLOWED}",
    )
  end

  actual_bits_pub  = pub_key.n.num_bits
  actual_bits_priv = priv_key.n.num_bits
  if actual_bits_pub != meta.rsa_bits || actual_bits_priv != meta.rsa_bits
    raise Error.new(
      code: ErrorCode::ERR_INVALID_RSA_BITS,
      message: "RSA keys must be #{meta.rsa_bits} bits",
    )
  end

  if pub_key.n != priv_key.n || pub_key.e != priv_key.e
    raise Error.new(
      code: ErrorCode::ERR_RSA_IMPORT_INVALID,
      message: 'Public and private key do not match',
    )
  end

  dir = rsa_version_dir(customer_id, version)
  if dir.exist?
    raise Error.new(
      code: ErrorCode::ERR_CUSTOMER_ALREADY_EXISTS,
      message: "RSA version v#{version} already exists for #{customer_id}",
    )
  end

  VaultLayout.ensure_private_dir(dir)
  VaultLayout.atomic_write_bytes(self.public_pem(customer_id, version),  public_pem)
  VaultLayout.atomic_write_bytes(self.private_pem(customer_id, version), private_pem)
  VaultLayout.atomic_write_text(rsa_meta_path(customer_id, version), meta.to_pretty_json)
  VaultLayout.ensure_private_dir(customer_root(customer_id))
  version
end

#list_rsa_key_dirs(customer_id) ⇒ Object



195
196
197
198
199
200
201
202
203
# File 'lib/robocap/sdk/key_vault.rb', line 195

def list_rsa_key_dirs(customer_id)
  Config.validate_customer_id!(customer_id)
  dir = rsa_dir(customer_id)
  return [] unless dir.directory?
  dir.children.sort_by { |c| c.basename.to_s.downcase }.filter_map do |child|
    next nil unless valid_rsa_key_dir?(child)
    Pathname(child.realpath)
  end
end

#list_rsa_versions(customer_id) ⇒ Object



63
64
65
66
67
68
69
70
71
# File 'lib/robocap/sdk/key_vault.rb', line 63

def list_rsa_versions(customer_id)
  rsa_dir = customer_root(customer_id).join('rsa')
  return [] unless rsa_dir.directory?
  rsa_dir.children.each_with_object([]) do |child, acc|
    next unless child.directory?
    m = RSA_VERSION_DIR_RE.match(child.basename.to_s)
    acc << m[1].to_i if m
  end.sort
end

#load_rsa_meta(customer_id, version) ⇒ Object



110
111
112
113
114
115
116
117
118
119
# File 'lib/robocap/sdk/key_vault.rb', line 110

def load_rsa_meta(customer_id, version)
  path = rsa_meta_path(customer_id, version)
  unless path.file?
    raise Error.new(
      code: ErrorCode::ERR_RSA_VERSION_MISSING,
      message: "RSA meta v#{version} not found for #{customer_id}",
    )
  end
  RsaKeyMeta.from_json(path.read(encoding: 'UTF-8'))
end

#private_pem(customer_id, version) ⇒ Object



39
40
41
# File 'lib/robocap/sdk/key_vault.rb', line 39

def private_pem(customer_id, version)
  rsa_version_dir(customer_id, version).join('private.pem')
end

#public_pem(customer_id, version) ⇒ Object



35
36
37
# File 'lib/robocap/sdk/key_vault.rb', line 35

def public_pem(customer_id, version)
  rsa_version_dir(customer_id, version).join('public.pem')
end

#rsa_dir(customer_id) ⇒ Object



55
56
57
# File 'lib/robocap/sdk/key_vault.rb', line 55

def rsa_dir(customer_id)
  customer_root(customer_id).join('rsa')
end

#rsa_meta_path(customer_id, version) ⇒ Object



43
44
45
# File 'lib/robocap/sdk/key_vault.rb', line 43

def rsa_meta_path(customer_id, version)
  rsa_version_dir(customer_id, version).join('meta.json')
end

#rsa_version_dir(customer_id, version) ⇒ Object



31
32
33
# File 'lib/robocap/sdk/key_vault.rb', line 31

def rsa_version_dir(customer_id, version)
  customer_root(customer_id).join('rsa', "v#{version}")
end

#trial_unwrap_cek(customer_id, cek_wrapped) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/robocap/sdk/key_vault.rb', line 238

def trial_unwrap_cek(customer_id, cek_wrapped)
  Config.validate_customer_id!(customer_id)
  unless cek_wrapped.bytesize == Config::RSA_2048_CIPHERTEXT_BYTES
    raise Error.new(
      code: ErrorCode::ERR_CENC_CEKA_WRAP,
      message: "Wrapped CEK must be #{Config::RSA_2048_CIPHERTEXT_BYTES} bytes",
    )
  end

  eligible = list_rsa_versions(customer_id).filter_map do |version|
    meta = load_rsa_meta(customer_id, version)
    next nil unless meta.rsa_bits == 2048
    priv = get_private_key(customer_id, version)
    next nil unless priv.n.num_bits == 2048
    [version, priv]
  end

  if eligible.empty?
    raise Error.new(
      code: ErrorCode::ERR_CENC_CEKA_TRIAL_FAILED,
      message: "No 2048-bit RSA keys available for trial unwrap: #{customer_id}",
    )
  end

  first_success = nil
  eligible.each do |version, priv|
    begin
      cek = RSAOAEP.unwrap_cek(cek_wrapped, priv)
    rescue Error
      next
    end
    return first_success if first_success
    first_success = CekTrialUnwrapResult.new(cek: cek, rsa_key_version: version)
  end

  unless first_success
    raise Error.new(
      code: ErrorCode::ERR_CENC_CEKA_TRIAL_FAILED,
      message: "CEK trial unwrap failed for all vault versions: #{customer_id}",
    )
  end
  first_success
end