2018-08-29 21:16:22 +00:00
|
|
|
import hashing
|
2018-08-29 09:32:01 +00:00
|
|
|
|
2018-08-28 10:51:11 +00:00
|
|
|
import entry
|
|
|
|
|
2018-08-29 11:04:07 +00:00
|
|
|
class FileFormatError(Exception):
|
|
|
|
def __init__(self, string):
|
|
|
|
self.string = string
|
|
|
|
self.line = None
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
if self.line == None:
|
|
|
|
return self.string
|
|
|
|
else:
|
|
|
|
return 'Line %i: %s' % (self.line, self.string)
|
|
|
|
|
|
|
|
class VersionMismatch(Exception):
|
|
|
|
def __init__(self, string):
|
|
|
|
self.string = string
|
|
|
|
self.line = None
|
2018-08-28 10:51:11 +00:00
|
|
|
|
2018-08-29 11:04:07 +00:00
|
|
|
def __str__(self):
|
|
|
|
if self.line == None:
|
|
|
|
return self.string
|
|
|
|
else:
|
|
|
|
return 'Line %i: %s' % (self.line, self.string)
|
2018-08-28 10:51:11 +00:00
|
|
|
|
2018-08-29 09:32:01 +00:00
|
|
|
def parse_header(header):
|
|
|
|
"""parse_header(bytes) → str
|
|
|
|
Throw an error if the header isn't good and return the file comment
|
|
|
|
(if any) if it is"""
|
|
|
|
assert type(header) == bytes
|
|
|
|
|
2018-08-29 21:16:22 +00:00
|
|
|
# Check that it ends in a newline
|
|
|
|
if len(header) == 0 or header[-1] != 0x0a:
|
2018-08-29 09:32:01 +00:00
|
|
|
raise FileFormatError('No newline after header')
|
2018-08-29 21:16:22 +00:00
|
|
|
|
|
|
|
# Split it into fields and make sure we have at least the magic and
|
|
|
|
# the version
|
|
|
|
fields = header[:-1].split(b' ', 3)
|
|
|
|
if len(fields) < 2:
|
|
|
|
raise FileFormatError('Too few fields in the header, expected at least magic and version')
|
|
|
|
|
|
|
|
# Check the magic
|
|
|
|
if fields[0] != b'SSHWOT':
|
|
|
|
raise FileFormatError('Invalid magic')
|
2018-08-28 10:51:11 +00:00
|
|
|
|
2018-08-29 09:32:01 +00:00
|
|
|
# See if we have a comment
|
2018-08-29 21:16:22 +00:00
|
|
|
if len(fields) == 3:
|
2018-08-29 09:32:01 +00:00
|
|
|
# It says we have
|
2018-08-29 21:16:22 +00:00
|
|
|
if len(fields[2]) == 0:
|
2018-08-29 09:32:01 +00:00
|
|
|
# No, we don't, but we do have a space telling we
|
|
|
|
# have. The header is malformed
|
|
|
|
raise FileFormatError('Missing comment or spurious space in the header')
|
|
|
|
else:
|
2018-08-29 21:16:22 +00:00
|
|
|
# Yes, we do. Extract it
|
2018-08-29 09:32:01 +00:00
|
|
|
try:
|
2018-08-29 21:16:22 +00:00
|
|
|
file_comment = fields[2].decode('utf-8')
|
2018-08-29 09:32:01 +00:00
|
|
|
except UnicodeDecodeError:
|
|
|
|
raise FileFormatError('Comment is not valid utf-8')
|
|
|
|
|
|
|
|
else:
|
2018-08-29 21:16:22 +00:00
|
|
|
file_comment = ''
|
|
|
|
|
|
|
|
return file_comment
|
2018-08-29 09:32:01 +00:00
|
|
|
|
|
|
|
def parse_entry(line):
|
|
|
|
"""parse_entry(bytes) → Entry"""
|
|
|
|
assert type(line) == bytes
|
|
|
|
|
2018-08-29 21:16:22 +00:00
|
|
|
def decode_b64_field(b64):
|
2018-08-29 09:32:01 +00:00
|
|
|
try:
|
2018-08-29 21:16:22 +00:00
|
|
|
return hashing.base64dec(b64)
|
2018-08-29 09:32:01 +00:00
|
|
|
except (ValueError, base64.binascii.Error) as err:
|
2018-08-29 21:16:22 +00:00
|
|
|
raise FileFormatError('Malformed base64 string: %s' % b64.decode('utf-8')) from err
|
2018-08-29 09:32:01 +00:00
|
|
|
|
2018-08-29 21:16:22 +00:00
|
|
|
# Check that it ends in a newline
|
|
|
|
if len(line) == 0 or line[-1] != 0x0a:
|
|
|
|
raise FileFormatError('No newline after entry')
|
2018-08-29 09:32:01 +00:00
|
|
|
|
2018-08-29 21:16:22 +00:00
|
|
|
# Split the line into fields and make sure we have at least the
|
|
|
|
# salt, the hashed host, and the fingerprint
|
|
|
|
fields = line[:-1].split(b' ', 3)
|
|
|
|
if len(fields) < 3:
|
|
|
|
raise FileFormatError('Too few fields in the entry, expected in the very least salt, hashed host, and fingerprint')
|
2018-08-29 09:32:01 +00:00
|
|
|
|
2018-08-29 21:16:22 +00:00
|
|
|
salt = decode_b64_field(fields[0])
|
|
|
|
hashed_host = decode_b64_field(fields[1])
|
|
|
|
fingerprint = decode_b64_field(fields[2])
|
|
|
|
|
|
|
|
# See if we have a comment
|
|
|
|
if len(fields) == 4:
|
|
|
|
# It says we have
|
|
|
|
if len(fields[3]) == 0:
|
|
|
|
# No, we don't, but we do have a space telling we
|
|
|
|
# have. The header is malformed
|
2018-08-29 09:32:01 +00:00
|
|
|
raise FileFormatError('Missing comment or spurious space in the entry')
|
|
|
|
else:
|
2018-08-29 21:16:22 +00:00
|
|
|
# Yes, we do. Extract it
|
2018-08-29 09:32:01 +00:00
|
|
|
try:
|
2018-08-29 21:16:22 +00:00
|
|
|
comment = fields[3].decode('utf-8')
|
2018-08-29 09:32:01 +00:00
|
|
|
except UnicodeDecodeError:
|
|
|
|
raise FileFormatError('Comment is not valid utf-8')
|
|
|
|
|
|
|
|
else:
|
2018-08-29 21:16:22 +00:00
|
|
|
comment = ''
|
2018-08-28 10:51:11 +00:00
|
|
|
|
|
|
|
return entry.Entry(salt, hashed_host, fingerprint, comment)
|
|
|
|
|
|
|
|
def read(f):
|
2018-08-29 09:32:01 +00:00
|
|
|
"""read(file(rb)) → ([Entry]: entries, str: file_comment)"""
|
|
|
|
lines = [line for line in f]
|
|
|
|
|
|
|
|
if len(lines) == 0:
|
|
|
|
raise FileFormatError('Missing header')
|
|
|
|
|
2018-08-29 11:04:07 +00:00
|
|
|
try:
|
|
|
|
file_comment = parse_header(lines[0])
|
|
|
|
except (FileFormatError, VersionMismatch) as err:
|
|
|
|
err.line = 1
|
|
|
|
raise err
|
2018-08-28 10:51:11 +00:00
|
|
|
|
|
|
|
entries = []
|
2018-08-29 11:04:07 +00:00
|
|
|
# Since line numbers are 1-indexed while lists in python are
|
|
|
|
# 0-indexed and we handle the first one separately, first one in the
|
|
|
|
# list is line 2
|
|
|
|
for linenum_minus_2, line in enumerate(lines[1:]):
|
|
|
|
try:
|
|
|
|
entries.append(parse_entry(line))
|
|
|
|
except FileFormatError as err:
|
|
|
|
err.line = linenum_minus_2 + 2
|
|
|
|
raise err
|
2018-08-28 10:51:11 +00:00
|
|
|
|
2018-08-29 09:32:01 +00:00
|
|
|
return entries, file_comment
|