Module: SshTresor::Tresor

Defined in:
lib/ssh_tresor/tresor.rb

Class Method Summary collapse

Class Method Details

.add_all_keys(blob) ⇒ Object



64
65
66
# File 'lib/ssh_tresor/tresor.rb', line 64

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

.add_all_keys_with_agent(agent, blob) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/ssh_tresor/tresor.rb', line 68

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) ⇒ Object



47
48
49
# File 'lib/ssh_tresor/tresor.rb', line 47

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

.add_key_with_agent(agent, blob, fingerprint) ⇒ Object

Raises:



51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/ssh_tresor/tresor.rb', line 51

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



115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ssh_tresor/tresor.rb', line 115

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) ⇒ Object



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

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

.decrypt_with_agent(agent, blob) ⇒ Object

Raises:



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/ssh_tresor/tresor.rb', line 30

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



130
131
132
133
134
135
# File 'lib/ssh_tresor/tresor.rb', line 130

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: []) ⇒ Object



12
13
14
# File 'lib/ssh_tresor/tresor.rb', line 12

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

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



16
17
18
19
20
21
22
23
24
# File 'lib/ssh_tresor/tresor.rb', line 16

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



106
107
108
109
110
111
112
113
# File 'lib/ssh_tresor/tresor.rb', line 106

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_keysObject



98
99
100
# File 'lib/ssh_tresor/tresor.rb', line 98

def list_keys
  Agent.connect.list_keys
end

.list_slots(blob) ⇒ Object



102
103
104
# File 'lib/ssh_tresor/tresor.rb', line 102

def list_slots(blob)
  blob.slot_fingerprints
end

.recover_master_key(agent, blob) ⇒ Object

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/ssh_tresor/tresor.rb', line 137

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) ⇒ Object

Raises:



87
88
89
90
91
92
93
94
95
96
# File 'lib/ssh_tresor/tresor.rb', line 87

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



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ssh_tresor/tresor.rb', line 154

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