Move to a space-separated format
This commit is contained in:
parent
5397293f33
commit
3a9c4baa4b
|
@ -1,3 +1,4 @@
|
||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -23,3 +24,44 @@ def hash_host(host):
|
||||||
salt = generate_salt()
|
salt = generate_salt()
|
||||||
hashed_host = hash_with_salt(host, salt)
|
hashed_host = hash_with_salt(host, salt)
|
||||||
return salt, hashed_host
|
return salt, hashed_host
|
||||||
|
|
||||||
|
def base64enc(b):
|
||||||
|
"""base64enc(bytes) → bytes
|
||||||
|
Uses no padding"""
|
||||||
|
# Base 64 encodes 3 bytes as 4 characters
|
||||||
|
# /byte 1\/byte 2\/byte 3\
|
||||||
|
# ABCDEFGHijklmnopQRSTUVWX
|
||||||
|
# \64 1/\64 2/\64 3/\64 4/
|
||||||
|
#
|
||||||
|
# If you have only one or two bytes, you don't have enough bits to
|
||||||
|
# fill all of the characters. The rest of the bits will be taken to
|
||||||
|
# be zeroes.
|
||||||
|
# /byte 1\
|
||||||
|
# ABCDEFGH0000
|
||||||
|
# \64 1/\64 2/
|
||||||
|
# /byte 1\
|
||||||
|
#
|
||||||
|
# /byte 1\/byte 2\
|
||||||
|
# ABCDEFGHijklmnop00
|
||||||
|
# \64 1/\64 2/\64 3/
|
||||||
|
#
|
||||||
|
# This way you end up with only 2 or 3 characters containing info.
|
||||||
|
# This usually gets padded into a multiple of 4 with =. However,
|
||||||
|
# since the amount of bytes left over mod 4 is enough to generate
|
||||||
|
# the padding back, we can strip it out
|
||||||
|
return base64.b64encode(b).replace(b'=', b'')
|
||||||
|
|
||||||
|
def base64dec(b64):
|
||||||
|
"""base64dec(bytes) → bytes
|
||||||
|
Can handle lack of padding."""
|
||||||
|
assert type(b64) == bytes
|
||||||
|
|
||||||
|
# Padded base64 is always a multiple of 4 bytes in length. The
|
||||||
|
# reasoning for this is because base64 decoding operates in groups
|
||||||
|
# of 4 base64 characters.
|
||||||
|
# Since we know the length of the string minus the padding, we can
|
||||||
|
# just pad it to the nearest multiple of 4
|
||||||
|
|
||||||
|
missing_padding_len = (4 - len(b64)%4) % 4
|
||||||
|
padding = b'=' * missing_padding_len
|
||||||
|
return base64.b64decode(b64 + padding, validate = True)
|
||||||
|
|
101
src/read_file.py
101
src/read_file.py
|
@ -1,4 +1,4 @@
|
||||||
import base64
|
import hashing
|
||||||
|
|
||||||
import entry
|
import entry
|
||||||
|
|
||||||
|
@ -30,88 +30,79 @@ def parse_header(header):
|
||||||
(if any) if it is"""
|
(if any) if it is"""
|
||||||
assert type(header) == bytes
|
assert type(header) == bytes
|
||||||
|
|
||||||
magic = header[0:6]
|
# Check that it ends in a newline
|
||||||
if magic != b'SSHWOT':
|
if len(header) == 0 or header[-1] != 0x0a:
|
||||||
|
raise FileFormatError('No newline after header')
|
||||||
|
|
||||||
|
# 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')
|
raise FileFormatError('Invalid magic')
|
||||||
|
|
||||||
# Version 0 is the current one
|
|
||||||
version = header[6:7]
|
|
||||||
if version == b'':
|
|
||||||
raise FileFormatError('No newline after header')
|
|
||||||
if version != b'0':
|
|
||||||
raise VersionMismatch('Version %i not supported' % version[0])
|
|
||||||
|
|
||||||
# See if we have a comment
|
# See if we have a comment
|
||||||
if header[7:8] == b' ':
|
if len(fields) == 3:
|
||||||
# It says we have
|
# It says we have
|
||||||
if header[8:9] == b'\n':
|
if len(fields[2]) == 0:
|
||||||
# No, we don't, but we do have a space telling we
|
# No, we don't, but we do have a space telling we
|
||||||
# have. The header is malformed
|
# have. The header is malformed
|
||||||
raise FileFormatError('Missing comment or spurious space in the header')
|
raise FileFormatError('Missing comment or spurious space in the header')
|
||||||
else:
|
else:
|
||||||
# Yes, we do
|
# Yes, we do. Extract it
|
||||||
# Check it ends with a newline
|
|
||||||
if header[-1] != 0x0a:
|
|
||||||
raise FileFormatError('Missing newline at the end of the header')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_comment = header[8:-1].decode('utf-8')
|
file_comment = fields[2].decode('utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise FileFormatError('Comment is not valid utf-8')
|
raise FileFormatError('Comment is not valid utf-8')
|
||||||
|
|
||||||
return file_comment
|
|
||||||
|
|
||||||
elif header[7:8] == b'\n':
|
|
||||||
# No, we have newline
|
|
||||||
return ''
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# No, we have something else
|
file_comment = ''
|
||||||
raise FileFormatError("Expected a space or a newline but got '%s' instead" % header[7:].decode('utf-8'))
|
|
||||||
|
return file_comment
|
||||||
|
|
||||||
def parse_entry(line):
|
def parse_entry(line):
|
||||||
"""parse_entry(bytes) → Entry"""
|
"""parse_entry(bytes) → Entry"""
|
||||||
assert type(line) == bytes
|
assert type(line) == bytes
|
||||||
|
|
||||||
def extract_b64_field(rest):
|
def decode_b64_field(b64):
|
||||||
"""extract_b64_field(bytes) → (bytes: decoded_field, bytes:rest)"""
|
|
||||||
field_b64 = rest[0:44]
|
|
||||||
if len(field_b64) != 44:
|
|
||||||
raise FileFormatError('Unexpected end of line')
|
|
||||||
try:
|
try:
|
||||||
field = base64.b64decode(field_b64, validate = True)
|
return hashing.base64dec(b64)
|
||||||
except (ValueError, base64.binascii.Error) as err:
|
except (ValueError, base64.binascii.Error) as err:
|
||||||
raise FileFormatError('Malformed base64 string: %s' % field_b64.decode('utf-8')) from err
|
raise FileFormatError('Malformed base64 string: %s' % b64.decode('utf-8')) from err
|
||||||
|
|
||||||
return field, rest[44:]
|
# Check that it ends in a newline
|
||||||
|
if len(line) == 0 or line[-1] != 0x0a:
|
||||||
salt, rest = extract_b64_field(line)
|
|
||||||
hashed_host, rest = extract_b64_field(rest)
|
|
||||||
fingerprint, rest = extract_b64_field(rest)
|
|
||||||
|
|
||||||
# What do we have after that?
|
|
||||||
if rest[0:1] == b' ':
|
|
||||||
# A comment?
|
|
||||||
if rest[1:2] == b'\n':
|
|
||||||
# No, but it says we have. It's malformed
|
|
||||||
raise FileFormatError('Missing comment or spurious space in the entry')
|
|
||||||
else:
|
|
||||||
# Yes. Make sure it ends in a newline
|
|
||||||
if rest[-1] != 0x0a:
|
|
||||||
raise FileFormatError('No newline after entry')
|
raise FileFormatError('No newline after entry')
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
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
|
||||||
|
raise FileFormatError('Missing comment or spurious space in the entry')
|
||||||
|
else:
|
||||||
|
# Yes, we do. Extract it
|
||||||
try:
|
try:
|
||||||
comment = rest[1:-1].decode('utf-8')
|
comment = fields[3].decode('utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise FileFormatError('Comment is not valid utf-8')
|
raise FileFormatError('Comment is not valid utf-8')
|
||||||
|
|
||||||
elif rest[0:1] == b'\n':
|
|
||||||
# A newline
|
|
||||||
comment = ''
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Something else
|
comment = ''
|
||||||
raise FileFormatError('Expected a space or a newline but got "%s" instead' % rest.decode('utf-8'))
|
|
||||||
|
|
||||||
return entry.Entry(salt, hashed_host, fingerprint, comment)
|
return entry.Entry(salt, hashed_host, fingerprint, comment)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import base64
|
import hashing
|
||||||
|
|
||||||
def write_header(f, file_comment):
|
def write_header(f, file_comment):
|
||||||
"""write_header(file(wb), str)
|
"""write_header(file(wb), str)
|
||||||
|
@ -6,6 +6,8 @@ def write_header(f, file_comment):
|
||||||
assert type(file_comment) == str
|
assert type(file_comment) == str
|
||||||
# b'SSHWOT' magic
|
# b'SSHWOT' magic
|
||||||
f.write(b'SSHWOT')
|
f.write(b'SSHWOT')
|
||||||
|
# Separating space
|
||||||
|
f.write(b' ')
|
||||||
# Version number
|
# Version number
|
||||||
f.write(b'0')
|
f.write(b'0')
|
||||||
# b' ' + file_comment, if there is one
|
# b' ' + file_comment, if there is one
|
||||||
|
@ -24,14 +26,20 @@ def write_entry(f, salt, hashed_host, fingerprint, comment):
|
||||||
assert type(fingerprint) == bytes and len(fingerprint) == 32
|
assert type(fingerprint) == bytes and len(fingerprint) == 32
|
||||||
assert type(comment) == str
|
assert type(comment) == str
|
||||||
|
|
||||||
# base64 encoded (44 bytes): salt
|
# base64 encoded salt
|
||||||
f.write(base64.b64encode(salt))
|
f.write(hashing.base64enc(salt))
|
||||||
|
|
||||||
# base64 encoded (44 bytes): hashed_host
|
# Separating space
|
||||||
f.write(base64.b64encode(hashed_host))
|
f.write(b' ')
|
||||||
|
|
||||||
# base64 encoded (44 bytes): fingerprint
|
# base64 encoded hashed_host
|
||||||
f.write(base64.b64encode(fingerprint))
|
f.write(hashing.base64enc(hashed_host))
|
||||||
|
|
||||||
|
# Separating space
|
||||||
|
f.write(b' ')
|
||||||
|
|
||||||
|
# base64 encoded fingerprint
|
||||||
|
f.write(hashing.base64enc(fingerprint))
|
||||||
|
|
||||||
# b' ' + comment, if there is one
|
# b' ' + comment, if there is one
|
||||||
if len(comment) > 0:
|
if len(comment) > 0:
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
Please note that all text insire quotes in the EBNF here is to be taken to
|
Please note that all text insire quotes in the EBNF here is to be taken to
|
||||||
mean bytes that would decode as that using either the ASCII or the UTF-8
|
mean bytes that would decode as that using the ASCII. character encoding.
|
||||||
character encoding. "\n" refers specifically to the byte 0x0a, and no
|
"\n" refers specifically to the byte 0x0a, and no alternative newlines are
|
||||||
alternative newlines are acceptable.
|
acceptable.
|
||||||
|
|
||||||
The file has a header like:
|
The file has a header like:
|
||||||
|
|
||||||
magic = "SSHWOT" ;
|
magic = "SSHWOT" ;
|
||||||
version = "0" ;
|
version = "0" ;
|
||||||
comment = " ", ? General comment about the file. Valid utf-8, no '\n'. ? ;
|
comment = ? General comment about the file. Valid utf-8, no '\n'. ? ;
|
||||||
header = magic, version, [comment], "\n" ;
|
header = magic, " ", version, [" ", comment], "\n" ;
|
||||||
|
|
||||||
Examples of valid headers would be "SSHWOT0\n" and "SSHWOT0 Emma G. 2018\n".
|
Examples of valid headers would be "SSHWOT 0\n" and "SSHWOT 0 Emma G. 2018\n".
|
||||||
|
|
||||||
"SSHWOT0 \n" is not valid, since a space marks that there will be a comment.
|
"SSHWOT 0 \n" is not valid, since a space marks that there will be a comment.
|
||||||
|
|
||||||
After the header the entries are laid out as:
|
After the header the entries are laid out as:
|
||||||
|
|
||||||
salt = ? base64 encoded salt, 44 bytes long ? ;
|
salt = ? base64(salt) ? ;
|
||||||
hashed host = ? base64 encoded sha256(host concat salt), 44 bytes long ? ;
|
hashed host = ? base64(sha256(host concat salt)) ? ;
|
||||||
fingerprint = ? base64 encoded sha256-fingerprint, 44 bytes long ? ;
|
fingerprint = ? base64(sha256-fingerprint) ? ;
|
||||||
comment = " ", ? Comment about the host/key. Valid utf-8, no '\n'. ? ;
|
comment = ? Comment about the host/key. Valid utf-8, no '\n'. ? ;
|
||||||
entry = salt, hashed host, fingerprint, [comment], "\n" ;
|
entry = salt, " ", hashed host, " ", fingerprint, [" ", comment], "\n" ;
|
||||||
|
|
||||||
The version of base64 used uses + for 62 and / for 63, uses = for padding,
|
The version of base64 used uses + for 62 and / for 63, doesn't use = for
|
||||||
and contains no breaks.
|
padding, and contains no breaks.
|
||||||
|
|
||||||
Examples of valid entries are
|
Examples of valid entries are
|
||||||
"Yixx+B6zrFoubPhBddgyx0nXHmbqMW1Wzneo4JqJv0U=yPUACFC/zPt/ENoIluOuWiTXor3r7oHhac63qej637E=QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2A=\n"
|
"Yixx+B6zrFoubPhBddgyx0nXHmbqMW1Wzneo4JqJv0U yPUACFC/zPt/ENoIluOuWiTXor3r7oHhac63qej637E QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2A\n"
|
||||||
and
|
and
|
||||||
"bd/MfFs+DMVqNQQoZGGCvpTopeS0/Jt6GS5vg7J+638=cbbdTnuIh0ZwnM+/r3sAu4iHgaN3mpkcP9kJND4vBUo=YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4A= The old one\n"
|
"bd/MfFs+DMVqNQQoZGGCvpTopeS0/Jt6GS5vg7J+638 cbbdTnuIh0ZwnM+/r3sAu4iHgaN3mpkcP9kJND4vBUo YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4A The old one\n"
|
||||||
|
|
||||||
Again, if there is a space following the necessary parts, there must also be
|
Again, if there is a space following the necessary parts, there must also be
|
||||||
a comment or else the entry is malformed.
|
a comment or else the entry is malformed.
|
||||||
|
|
Loading…
Reference in New Issue