Class: SshTresor::TresorBlob

Inherits:
Object
  • Object
show all
Defined in:
lib/ssh_tresor/format.rb

Constant Summary collapse

MAGIC =
"SSHTRESR".b
VERSION =
0x03
FINGERPRINT_SIZE =
32
CHALLENGE_SIZE =
32
NONCE_SIZE =
12
AUTH_TAG_SIZE =
16
MASTER_KEY_SIZE =
32
ENCRYPTED_KEY_SIZE =
MASTER_KEY_SIZE + AUTH_TAG_SIZE
SLOT_SIZE =
FINGERPRINT_SIZE + CHALLENGE_SIZE + NONCE_SIZE + ENCRYPTED_KEY_SIZE
HEADER_SIZE =
10
MAX_TRESOR_SIZE =
100 * 1024 * 1024
ARMOR_BEGIN =
"-----BEGIN SSH TRESOR-----"
ARMOR_END =
"-----END SSH TRESOR-----"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(slots:, data_nonce:, ciphertext:) ⇒ TresorBlob

Returns a new instance of TresorBlob.



99
100
101
102
103
# File 'lib/ssh_tresor/format.rb', line 99

def initialize(slots:, data_nonce:, ciphertext:)
  @slots = slots
  @data_nonce = data_nonce
  @ciphertext = ciphertext
end

Instance Attribute Details

#ciphertextObject (readonly)

Returns the value of attribute ciphertext.



30
31
32
# File 'lib/ssh_tresor/format.rb', line 30

def ciphertext
  @ciphertext
end

#data_nonceObject (readonly)

Returns the value of attribute data_nonce.



30
31
32
# File 'lib/ssh_tresor/format.rb', line 30

def data_nonce
  @data_nonce
end

#slotsObject (readonly)

Returns the value of attribute slots.



30
31
32
# File 'lib/ssh_tresor/format.rb', line 30

def slots
  @slots
end

Class Method Details

.from_armored(text) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/ssh_tresor/format.rb', line 41

def self.from_armored(text)
  start = text.index(ARMOR_BEGIN)
  finish = text.index(ARMOR_END)
  raise Error, "Invalid tresor format: missing BEGIN header" if start.nil?
  raise Error, "Invalid tresor format: missing END footer" if finish.nil?
  raise Error, "Invalid tresor format: invalid armor structure" if start >= finish

  base64 = text[(start + ARMOR_BEGIN.length)...finish].chars.reject { |char| char =~ /\s/ }.join
  from_binary(Base64.strict_decode64(base64))
rescue ArgumentError => e
  raise Error, "Invalid tresor format: base64 decoding failed: #{e.message}"
end

.from_binary(data) ⇒ Object

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/ssh_tresor/format.rb', line 54

def self.from_binary(data)
  min_size = HEADER_SIZE + SLOT_SIZE + NONCE_SIZE + AUTH_TAG_SIZE
  raise Error, "Invalid tresor format: data too short: #{data.bytesize} bytes, minimum #{min_size} required" if data.bytesize < min_size
  raise Error, "Invalid tresor format: invalid magic header" unless data.byteslice(0, 8) == MAGIC

  version = data.getbyte(8)
  raise Error, "Invalid tresor format: unsupported version: #{version}, expected #{VERSION}" unless version == VERSION

  slot_count = data.getbyte(9)
  raise Error, "Invalid tresor format: tresor has no key slots" if slot_count.zero?

  slots_end = HEADER_SIZE + (slot_count * SLOT_SIZE)
  raise Error, "Invalid tresor format: data too short for #{slot_count} slots" if data.bytesize < slots_end + NONCE_SIZE + AUTH_TAG_SIZE

  slots = Array.new(slot_count) do |index|
    offset = HEADER_SIZE + (index * SLOT_SIZE)
    parse_slot(data.byteslice(offset, SLOT_SIZE))
  end

  data_nonce = data.byteslice(slots_end, NONCE_SIZE)
  ciphertext = data.byteslice(slots_end + NONCE_SIZE, data.bytesize - slots_end - NONCE_SIZE)

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

.from_bytes(data) ⇒ Object



32
33
34
35
36
37
38
39
# File 'lib/ssh_tresor/format.rb', line 32

def self.from_bytes(data)
  bytes = data.b
  if bytes.valid_encoding? && bytes.strip.start_with?(ARMOR_BEGIN)
    from_armored(bytes)
  else
    from_binary(bytes)
  end
end

.parse_slot(bytes) ⇒ Object

Raises:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ssh_tresor/format.rb', line 79

def self.parse_slot(bytes)
  raise Error, "Invalid tresor format: slot data too short" if bytes.bytesize < SLOT_SIZE

  offset = 0
  fingerprint = bytes.byteslice(offset, FINGERPRINT_SIZE)
  offset += FINGERPRINT_SIZE
  challenge = bytes.byteslice(offset, CHALLENGE_SIZE)
  offset += CHALLENGE_SIZE
  nonce = bytes.byteslice(offset, NONCE_SIZE)
  offset += NONCE_SIZE
  encrypted_key = bytes.byteslice(offset, ENCRYPTED_KEY_SIZE)

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

Instance Method Details

#find_slot(fingerprint) ⇒ Object



118
119
120
# File 'lib/ssh_tresor/format.rb', line 118

def find_slot(fingerprint)
  slots.find { |slot| slot.fingerprint == fingerprint }
end

#slot_fingerprintsObject



122
123
124
# File 'lib/ssh_tresor/format.rb', line 122

def slot_fingerprints
  slots.map(&:fingerprint)
end

#to_armoredObject



112
113
114
115
116
# File 'lib/ssh_tresor/format.rb', line 112

def to_armored
  encoded = Base64.strict_encode64(to_bytes)
  wrapped = encoded.scan(/.{1,64}/).join("\n")
  "#{ARMOR_BEGIN}\n#{wrapped}\n#{ARMOR_END}\n"
end

#to_bytesObject

Raises:



105
106
107
108
109
110
# File 'lib/ssh_tresor/format.rb', line 105

def to_bytes
  raise Error, "Invalid tresor format: tresor has no key slots" if slots.empty?
  raise Error, "Invalid tresor format: tresor has too many slots (max 255)" if slots.length > 255

  MAGIC + [VERSION, slots.length].pack("CC") + slots.map(&:to_bytes).join.b + data_nonce + ciphertext
end