Module: SshTresor::Tresor

Defined in:
lib/ssh_tresor/tresor.rb

Overview

Lower-level envelope encryption operations.

Tresor works directly with TresorBlob instances and an SSH agent. Most applications should prefer Vault, which handles parsing and serialization.

See Also:

Class Method Summary collapse

Class Method Details

.add_all_keys(blob) ⇒ Array(SshTresor::TresorBlob, Integer)

Adds slots for all currently available SSH agent keys.

Parameters:

Returns:



111
112
113
# File 'lib/ssh_tresor/tresor.rb', line 111

def add_all_keys(blob)
  add_all_keys_with_agent(Agent.connect, blob)
end

.add_all_keys_with_agent(agent, blob) ⇒ Array(SshTresor::TresorBlob, Integer)

Adds slots for all currently available keys from a supplied SSH agent.

Parameters:

Returns:



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ssh_tresor/tresor.rb', line 120

def add_all_keys_with_agent(agent, blob)
  master_key = recover_master_key(agent, blob)
  new_slots = blob.slots.dup
  added = 0

  agent.list_keys.each do |key|
    next if blob.find_slot(key.fingerprint_bytes)

    begin
      new_slots << create_slot(agent, key, master_key)
      added += 1
    rescue Error
      next
    end
  end

  [TresorBlob.new(slots: new_slots, data_nonce: blob.data_nonce, ciphertext: blob.ciphertext), added]
end

.add_key(blob, fingerprint) ⇒ SshTresor::TresorBlob

Adds one key slot using the default SSH agent.

Parameters:

  • blob (SshTresor::TresorBlob)
  • fingerprint (String)

    fingerprint or unambiguous prefix of the key to add.

Returns:



82
83
84
# File 'lib/ssh_tresor/tresor.rb', line 82

def add_key(blob, fingerprint)
  add_key_with_agent(Agent.connect, blob, fingerprint)
end

.add_key_with_agent(agent, blob, fingerprint) ⇒ SshTresor::TresorBlob

Adds one key slot using a supplied SSH agent.

Parameters:

Returns:

Raises:



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/ssh_tresor/tresor.rb', line 94

def add_key_with_agent(agent, blob, fingerprint)
  master_key = recover_master_key(agent, blob)
  new_key = agent.find_key(fingerprint)

  raise Error, "Invalid tresor format: key already exists in tresor" if blob.find_slot(new_key.fingerprint_bytes)

  TresorBlob.new(
    slots: blob.slots + [create_slot(agent, new_key, master_key)],
    data_nonce: blob.data_nonce,
    ciphertext: blob.ciphertext
  )
end

.create_slot(agent, key, master_key) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates one encrypted master-key slot.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/ssh_tresor/tresor.rb', line 187

def create_slot(agent, key, master_key)
  challenge = Crypto.random_challenge
  signature = agent.sign(key, challenge)
  slot_key = Crypto.derive_key(signature)
  nonce = Crypto.random_nonce
  encrypted_key = Crypto.encrypt(slot_key, nonce, master_key)

  Slot.new(
    fingerprint: key.fingerprint_bytes,
    challenge: challenge,
    nonce: nonce,
    encrypted_key: encrypted_key
  )
end

.decrypt(blob) ⇒ String

Decrypts a blob using the default SSH agent from SSH_AUTH_SOCK.

Parameters:

Returns:

  • (String)

    plaintext bytes.



50
51
52
# File 'lib/ssh_tresor/tresor.rb', line 50

def decrypt(blob)
  decrypt_with_agent(Agent.connect, blob)
end

.decrypt_with_agent(agent, blob) ⇒ String

Decrypts a blob using any matching key available in the supplied agent.

Parameters:

Returns:

  • (String)

    plaintext bytes.

Raises:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ssh_tresor/tresor.rb', line 60

def decrypt_with_agent(agent, blob)
  keys = agent.list_keys.sort_by(&:security_key?)

  keys.each do |key|
    slot = blob.find_slot(key.fingerprint_bytes)
    next if slot.nil?

    begin
      return decrypt_with_slot(agent, key, slot, blob)
    rescue DecryptionError
      next
    end
  end

  raise NoMatchingSlot
end

.decrypt_with_slot(agent, key, slot, blob) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Decrypts a blob through one matching slot.



205
206
207
208
209
210
# File 'lib/ssh_tresor/tresor.rb', line 205

def decrypt_with_slot(agent, key, slot, blob)
  signature = agent.sign(key, slot.challenge)
  slot_key = Crypto.derive_key(signature)
  master_key = Crypto.decrypt(slot_key, slot.nonce, slot.encrypted_key)
  Crypto.decrypt(master_key, blob.data_nonce, blob.ciphertext)
end

.encrypt(plaintext, fingerprints: []) ⇒ SshTresor::TresorBlob

Encrypts plaintext using the default SSH agent from SSH_AUTH_SOCK.

Parameters:

  • plaintext (String)

    plaintext bytes.

  • fingerprints (Array<String>) (defaults to: [])

    optional key fingerprints to encrypt for.

Returns:



25
26
27
# File 'lib/ssh_tresor/tresor.rb', line 25

def encrypt(plaintext, fingerprints: [])
  encrypt_with_agent(Agent.connect, plaintext, fingerprints: fingerprints)
end

.encrypt_with_agent(agent, plaintext, fingerprints: []) ⇒ SshTresor::TresorBlob

Encrypts plaintext using a supplied SSH agent.

Parameters:

  • agent (SshTresor::Agent)

    SSH agent or compatible object.

  • plaintext (String)

    plaintext bytes.

  • fingerprints (Array<String>) (defaults to: [])

    optional key fingerprints to encrypt for.

Returns:

Raises:



36
37
38
39
40
41
42
43
44
# File 'lib/ssh_tresor/tresor.rb', line 36

def encrypt_with_agent(agent, plaintext, fingerprints: [])
  keys = if fingerprints.empty?
           [agent.first_key]
         else
           fingerprints.map { |fingerprint| agent.find_key(fingerprint) }
         end

  encrypt_with_keys(agent, keys, plaintext)
end

.encrypt_with_keys(agent, keys, plaintext) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Encrypts plaintext for concrete agent keys.



175
176
177
178
179
180
181
182
# File 'lib/ssh_tresor/tresor.rb', line 175

def encrypt_with_keys(agent, keys, plaintext)
  master_key = Crypto.random_master_key
  slots = keys.map { |key| create_slot(agent, key, master_key) }
  data_nonce = Crypto.random_nonce
  ciphertext = Crypto.encrypt(master_key, data_nonce, plaintext)

  TresorBlob.new(slots: slots, data_nonce: data_nonce, ciphertext: ciphertext)
end

.list_keysArray<SshTresor::AgentKey>

Lists keys currently available through the default SSH agent.

Returns:



160
161
162
# File 'lib/ssh_tresor/tresor.rb', line 160

def list_keys
  Agent.connect.list_keys
end

.list_slots(blob) ⇒ Array<String>

Lists raw slot fingerprints stored in a blob.

Parameters:

Returns:

  • (Array<String>)

    raw 32-byte SHA-256 fingerprint bytes.



168
169
170
# File 'lib/ssh_tresor/tresor.rb', line 168

def list_slots(blob)
  blob.slot_fingerprints
end

.recover_master_key(agent, blob) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Recovers the data master key from any matching slot.

Raises:



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/ssh_tresor/tresor.rb', line 215

def recover_master_key(agent, blob)
  agent.list_keys.each do |key|
    slot = blob.find_slot(key.fingerprint_bytes)
    next if slot.nil?

    begin
      signature = agent.sign(key, slot.challenge)
      slot_key = Crypto.derive_key(signature)
      return Crypto.decrypt(slot_key, slot.nonce, slot.encrypted_key)
    rescue DecryptionError
      next
    end
  end

  raise NoMatchingSlot
end

.remove_key(blob, fingerprint) ⇒ SshTresor::TresorBlob

Removes one key slot by fingerprint or unambiguous prefix.

Parameters:

  • blob (SshTresor::TresorBlob)
  • fingerprint (String)

    fingerprint or unambiguous prefix of the slot to remove.

Returns:

Raises:



146
147
148
149
150
151
152
153
154
155
# File 'lib/ssh_tresor/tresor.rb', line 146

def remove_key(blob, fingerprint)
  raise Error, "Invalid tresor format: cannot remove the last key from tresor" if blob.slots.length == 1

  fingerprint_bytes = resolve_slot_fingerprint(blob, fingerprint)
  new_slots = blob.slots.reject { |slot| slot.fingerprint == fingerprint_bytes }

  raise KeyNotFound, "Key not found: #{fingerprint}" if new_slots.length == blob.slots.length

  TresorBlob.new(slots: new_slots, data_nonce: blob.data_nonce, ciphertext: blob.ciphertext)
end

.resolve_slot_fingerprint(blob, fingerprint) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Resolves a slot fingerprint prefix to raw fingerprint bytes.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/ssh_tresor/tresor.rb', line 235

def resolve_slot_fingerprint(blob, fingerprint)
  normalized = fingerprint.delete_prefix("SHA256:")
  matches = blob.slot_fingerprints.select do |slot_fingerprint|
    Base64.strict_encode64(slot_fingerprint).delete("=").start_with?(normalized)
  end

  case matches.length
  when 0
    raise KeyNotFound, "Key not found: #{fingerprint}"
  when 1
    matches.first
  else
    raise KeyNotFound, "Key not found: #{fingerprint} (ambiguous: #{matches.length} slots match this prefix, please be more specific)"
  end
end