Class: SshTresor::TresorBlob

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

Overview

Parsed SSHTRESR v3 encrypted file.

A blob contains one or more key slots and one AES-256-GCM encrypted payload. It can be read from or written to the binary wire format, and it can also be represented as base64 armor for terminal-friendly transport.

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

Creates an in-memory tresor blob.

Parameters:

  • slots (Array<SshTresor::Slot>)

    key-wrapping slots.

  • data_nonce (String)

    AES-GCM nonce for the payload ciphertext.

  • ciphertext (String)

    payload ciphertext with authentication tag.



142
143
144
145
146
# File 'lib/ssh_tresor/format.rb', line 142

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.



48
49
50
# File 'lib/ssh_tresor/format.rb', line 48

def ciphertext
  @ciphertext
end

#data_nonceObject (readonly)

Returns the value of attribute data_nonce.



48
49
50
# File 'lib/ssh_tresor/format.rb', line 48

def data_nonce
  @data_nonce
end

#slotsObject (readonly)

Returns the value of attribute slots.



48
49
50
# File 'lib/ssh_tresor/format.rb', line 48

def slots
  @slots
end

Class Method Details

.from_armored(text) ⇒ SshTresor::TresorBlob

Parses armored tresor text.

Parameters:

  • text (String)

    armor containing base64 encoded binary data.

Returns:

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ssh_tresor/format.rb', line 69

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) ⇒ SshTresor::TresorBlob

Parses binary SSHTRESR v3 bytes.

Parameters:

  • data (String)

    binary tresor data.

Returns:

Raises:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/ssh_tresor/format.rb', line 87

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) ⇒ SshTresor::TresorBlob

Parses binary or armored tresor content.

Parameters:

  • data (String)

    binary SSHTRESR bytes or armored text.

Returns:

Raises:



55
56
57
58
59
60
61
62
# File 'lib/ssh_tresor/format.rb', line 55

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) ⇒ SshTresor::Slot

Parses a fixed-width key slot from binary data.

Parameters:

  • bytes (String)

    binary slot data.

Returns:

Raises:



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/ssh_tresor/format.rb', line 117

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) ⇒ SshTresor::Slot?

Finds a slot by raw SHA-256 fingerprint bytes.

Parameters:

  • fingerprint (String)

    32-byte SHA-256 fingerprint digest.

Returns:



172
173
174
# File 'lib/ssh_tresor/format.rb', line 172

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

#slot_fingerprintsArray<String>

Lists raw slot fingerprints.

Returns:

  • (Array<String>)

    raw 32-byte SHA-256 fingerprint bytes.



179
180
181
# File 'lib/ssh_tresor/format.rb', line 179

def slot_fingerprints
  slots.map(&:fingerprint)
end

#to_armoredString

Serializes the blob as PEM-like base64 armor.

Returns:

  • (String)

    armored tresor text.



162
163
164
165
166
# File 'lib/ssh_tresor/format.rb', line 162

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_bytesString

Serializes the blob as binary SSHTRESR v3 bytes.

Returns:

  • (String)

    binary tresor data.

Raises:



152
153
154
155
156
157
# File 'lib/ssh_tresor/format.rb', line 152

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