from typing import IO # Copied from sshd(8) key_types = [ "sk-ecdsa-sha2-nistp256@openssh.com", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "sk-ssh-ed25519@openssh.com", "ssh-ed25519", "ssh-dss", "ssh-rsa", ] class AuthorizedKey: def __init__(self, line): self.original = line line = line.strip() if line.startswith('#') or line == '': raise ValueError('This is not a key, it is a comment or an empty line.') if line.split(' ')[0] not in key_types: # The hard case: there are options at the beginning. # It is too simple to code the state machine myself, so no library :-) in_quotes = False backslash_preceding = False for idx, char in enumerate(line): if char == '\"' and not backslash_preceeding: in_quotes = not in_quotes elif char == ' ' and not in_quotes: break else: backslash_preceeding = char == '\\' else: raise ValueError('Badly formatted options not followed by a key.') if line[idx] != ' ': raise ValueError('I am just broken.') self.options = line[:idx] line = line[idx+1:] line = line.strip() # In case there are multiple spaces else: self.options = None # Now only the key follows, so this is simple split = line.split(' ', maxsplit=2) self.type = split[0] self.key_b64 = split[1] self.comment = split[2] if len(split) >= 3 else None def to_string(self): result = '' if self.options is not None: result += self.options + ' ' result += f'{self.type} {self.key_b64}' if self.comment is not None: result += ' ' + self.comment return result def parse_file(f: IO[str]) -> list[AuthorizedKey | str]: result = [] for line in f: stripped = line.strip() if stripped.startswith('#') or stripped == '': # This is a comment / empty line, should be preserved result.append(line) else: result.append(AuthorizedKey(line)) return result # TODO: Implement option parsing, key validation and decoding to bytes. def dump_file(keys: list[AuthorizedKey | str], f: IO[str]) -> None: for rec in keys: if isinstance(rec, AuthorizedKey): rec = rec.to_string() rec += '\n' f.write(rec)