diff --git a/age/format.ha b/age/format.ha deleted file mode 100644 index e61902d..0000000 --- a/age/format.ha +++ /dev/null @@ -1,126 +0,0 @@ - -use io; -use os; -use bufio; -use strings; -use encoding::base64; -use age; -use x25519; - -def INTRO: str = "age-encryption.org/v1"; -def STANZA_PREFIX: []byte = strings::toutf8("->"); -def FOOTER_PREFIX: []byte = strings::toutf8("---"); - -def COLUMNS_PER_LINE: int = 64; -def BYTES_PER_LINE: int = COLUMNS_PER_LINE / 4 * 3; - -type header = struct { - mac: str, - recipients: []stanza, -}; - -fn is_valid_string(s: str) bool = { - if (len(s) == 0) { - return false; - }; - for (let char .. strings::toutf8(s)) { - if (char < 33 || char > 126) { - return false; - }; - }; - return true; -}; - -fn read_stanza(scanner: *bufio::scanner) !stanza = { - const line = bufio::scan_string(&scanner, "\n")!; - if (!strings::hasprefix(line, STANZA_PREFIX)) { - return error("malformed stanza opening line: {}", line); - }; - - const (prefix, args) = split_args(line); - if (prefix != STANZA_PREFIX || len(args) < 1) { - return error("malformed stanza: {}", line); - }; - - for (let arg .. args) { - if (!is_valid_string(arg)) { - return error("malformed stanza: {}", line); - }; - }; - - const recipient_type = args[0]; - const args = args[1..]; - let body: []byte = []; - - for (true) { - next_line = bufio::scan_string(&scanner, "\n")!; - const b = decode_string(next_line)!; - - if (len(b) > BYTES_PER_LINE) { - return error("malformed body line {}: too long", next_line); - }; - - append(body, b)?; - if (len(b) < BYTES_PER_LINE) { - // stanza bodies always end with short lines - return stanza{ - recipient_type = recipient_type, - args = args, - body = body - }; - }; - - }; -}; - -fn split_args(line: str) (str, []str) = { - const parts = strings.split(line, ' '); - return (parts[0], parts[1..]); -}; - -fn decode_string(encoded: str) ![]byte = { - if (strings::contains(encoded, "\n", "\r")) { - return error("unexpected newline character"); - }; - return base64.decode(encoded)!; -}; - -export fn parse(path: str) !(header, bufio::scanner) = { - const file = os::open_buffered(path)!; - const scanner = bufio::newscanner(file); - - let recipients: []stanza = []; - - const version_line = bufio::scan_string(&scanner, '\n')!; - if (version_line != "age-encryption.org/v1") { - return error("unexpected intro: {}", version_line); - }; - for (true) { - const next_line = bufio::scan_line(&scanner)!; - - if (next_line[0..3] == FOOTER_PREFIX) { - // we're at the footer line - const (prefix, args) = split_args(next_line); - if (prefix != FOOTER_PREFIX || len(args) != 1) { - return error("malformed closing line: {}", next_line); - }; - - const mac = decode_string(args[0]); - if (len(mac) != 32) { - return error("malformed closing line: {}", next_line); - }; - break; - }; - bufio::unread(&scanner, next_line); - - const stanza = read_stanza(scanner)!; - append(recipients, stanza)?; - }; - const prefix = bufio::scan_buffer(&scanner, ); - bufio::unread(&scanner, prefix); - - return (header{ - mac = mac, - recipients = recipients, - }, scanner); -}; \ No newline at end of file diff --git a/age/format/base64/base64.ha b/age/format/base64/base64.ha new file mode 100644 index 0000000..0da7d0a --- /dev/null +++ b/age/format/base64/base64.ha @@ -0,0 +1,634 @@ +// SPDX-License-Identifier: MPL-2.0 +// (c) Hare authors + +use ascii; +use bytes; +use errors; +use io; +use memio; +use os; +use strings; + +def PADDING: u8 = '='; + +export type encoding = struct { + encmap: [64]u8, + decmap: [128]u8, +}; + +// Represents the standard base-64 encoding alphabet as defined in RFC 4648. +export const std_encoding: encoding = encoding { + encmap = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'], + decmap = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, + 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0xff... + ], +}; + +// Represents the "base64url" alphabet as defined in RFC 4648, suitable for use +// in URLs and file paths. +export const url_encoding: encoding = encoding { + encmap = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'], + decmap = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, + 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0xff... + ], +}; + +// Initializes a new encoding based on the passed alphabet, which must be a +// 64-byte ASCII string. +export fn encoding_init(enc: *encoding, alphabet: str) void = { + const alphabet = strings::toutf8(alphabet); + assert(len(alphabet) == 64); + enc.decmap[..] = [0xff...]; + for (let i: u8 = 0; i < 64; i += 1) { + const ch = alphabet[i]; + assert(ascii::valid(ch: rune) && enc.decmap[ch] == 0xff); + enc.encmap[i] = ch; + enc.decmap[ch] = i; + }; +}; + +@test fn std_encodings() void = { + let std = encoding { ... }; + const std_alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + encoding_init(&std, std_alpha); + for (let i = 0z; i < len(std.encmap); i += 1) { + assert(std.encmap[i] == std_encoding.encmap[i]); + }; + for (let i = 0z; i < len(std.decmap); i += 1) { + assert(std.decmap[i] == std_encoding.decmap[i]); + }; + + let url = encoding { ... }; + const url_alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + encoding_init(&url, url_alpha); + for (let i = 0z; i < len(url.encmap); i += 1) { + assert(url.encmap[i] == url_encoding.encmap[i]); + }; + for (let i = 0z; i < len(url.decmap); i += 1) { + assert(url.decmap[i] == url_encoding.decmap[i]); + }; +}; + +export type encoder = struct { + stream: io::stream, + out: io::handle, + enc: *encoding, + ibuf: [3]u8, + obuf: [4]u8, + iavail: u8, + oavail: u8, +}; + +const encoder_vtable: io::vtable = io::vtable { + writer = &encode_writer, + closer = &encode_closer, + ... +}; + +// Creates a stream that encodes writes as base64 before writing them to a +// secondary stream. Afterwards [[io::close]] must be called to write any +// unwritten bytes, in case of padding. Closing this stream will not close the +// underlying stream. After a write returns an error, the stream must not be +// written to again or closed. +export fn newencoder( + enc: *encoding, + out: io::handle, +) encoder = { + return encoder { + stream = &encoder_vtable, + out = out, + enc = enc, + ... + }; +}; + +fn encode_writer( + s: *io::stream, + in: const []u8 +) (size | io::error) = { + let s = s: *encoder; + let i = 0z; + for (i < len(in)) { + let b = s.ibuf[..]; + // fill ibuf + for (let j = s.iavail; j < 3 && i < len(in); j += 1) { + b[j] = in[i]; + i += 1; + s.iavail += 1; + }; + + if (s.iavail != 3) { + return i; + }; + + fillobuf(s); + + match (writeavail(s)) { + case let e: io::error => + if (i == 0) { + return e; + }; + return i; + case void => void; + }; + }; + + return i; +}; + +fn fillobuf(s: *encoder) void = { + assert(s.iavail == 3); + let b = s.ibuf[..]; + s.obuf[..] = [ + s.enc.encmap[b[0] >> 2], + s.enc.encmap[(b[0] & 0x3) << 4 | b[1] >> 4], + s.enc.encmap[(b[1] & 0xf) << 2 | b[2] >> 6], + s.enc.encmap[b[2] & 0x3f], + ][..]; + s.oavail = 4; +}; + +fn writeavail(s: *encoder) (void | io::error) = { + if (s.oavail == 0) { + return; + }; + + for (s.oavail > 0) { + let n = io::write(s.out, s.obuf[len(s.obuf) - s.oavail..])?; + s.oavail -= n: u8; + }; + + if (s.oavail == 0) { + s.iavail = 0; + }; +}; + +// Flushes pending writes to the underlying stream. +fn encode_closer(s: *io::stream) (void | io::error) = { + let s = s: *encoder; + let finished = false; + defer if (finished) clear(s); + + if (s.oavail > 0) { + for (s.oavail > 0) { + writeavail(s)?; + }; + finished = true; + return; + }; + + if (s.iavail == 0) { + finished = true; + return; + }; + + // prepare padding as input length was not a multiple of 3 + // 0 1 2 + static const npa: []u8 = [0, 2, 1]; + const np = npa[s.iavail]; + + for (let i = s.iavail; i < 3; i += 1) { + s.ibuf[i] = 0; + s.iavail += 1; + }; + + fillobuf(s); + for (let i = 0z; i < np; i += 1) { + s.obuf[3 - i] = PADDING; + }; + + for (s.oavail > 0) { + writeavail(s)?; + }; + finished = true; +}; + +fn clear(e: *encoder) void = { + bytes::zero(e.ibuf); + bytes::zero(e.obuf); +}; + +@test fn partialwrite() void = { + const raw: [_]u8 = [ + 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, + 0x61, 0x00, + ]; + const expected: str = `AAAAB3NzaC1yc2EA`; + + let buf = memio::dynamic(); + let e = newencoder(&std_encoding, &buf); + io::writeall(&e, raw[..4])!; + io::writeall(&e, raw[4..11])!; + io::writeall(&e, raw[11..])!; + io::close(&e)!; + + assert(memio::string(&buf)! == expected); + + let encb = memio::buffer(&buf); + free(encb); +}; + +// Encodes a byte slice in base 64, using the given encoding, returning a slice +// of ASCII bytes. The caller must free the return value. +export fn encodeslice(enc: *encoding, in: []u8) ([]u8 | nomem) = { + let out = memio::dynamic(); + let encoder = newencoder(enc, &out); + match (io::writeall(&encoder, in)) { + case size => void; + case nomem => + io::close(&out)!; + return nomem; + case => abort(); // unreachable + }; + match (io::close(&encoder)) { + case void => void; + case nomem => + io::close(&out)!; + return nomem; + case => abort(); // unreachable + }; + return memio::buffer(&out); +}; + +// Encodes base64 data using the given alphabet and writes it to a stream, +// returning the number of bytes of data written (i.e. len(buf)). +export fn encode( + out: io::handle, + enc: *encoding, + buf: []u8, +) (size | io::error) = { + const enc = newencoder(enc, out); + match (io::writeall(&enc, buf)) { + case let z: size => + io::close(&enc)?; + return z; + case let err: io::error => + clear(&enc); + return err; + }; +}; + +// Encodes a byte slice in base 64, using the given encoding, returning a +// string. The caller must free the return value. +export fn encodestr(enc: *encoding, in: []u8) (str | nomem) = { + return strings::fromutf8(encodeslice(enc, in)?)!; +}; + +@test fn encode() void = { + // RFC 4648 test vectors + const in: [_]u8 = ['f', 'o', 'o', 'b', 'a', 'r']; + const expect: [_]str = [ + "", + "Zg==", + "Zm8=", + "Zm9v", + "Zm9vYg==", + "Zm9vYmE=", + "Zm9vYmFy" + ]; + for (let i = 0z; i <= len(in); i += 1) { + let out = memio::dynamic(); + let encoder = newencoder(&std_encoding, &out); + io::writeall(&encoder, in[..i])!; + io::close(&encoder)!; + let encb = memio::buffer(&out); + defer free(encb); + assert(bytes::equal(encb, strings::toutf8(expect[i]))); + + // Testing encodestr should cover encodeslice too + let s = encodestr(&std_encoding, in[..i])!; + defer free(s); + assert(s == expect[i]); + }; +}; + +export type decoder = struct { + stream: io::stream, + in: io::handle, + enc: *encoding, + obuf: [3]u8, // leftover decoded output + ibuf: [4]u8, + iavail: u8, + oavail: u8, + pad: bool, // if padding was seen in a previous read +}; + +const decoder_vtable: io::vtable = io::vtable { + reader = &decode_reader, + ... +}; + +// Creates a stream that reads and decodes base 64 data from a secondary stream. +// This stream does not need to be closed, and closing it will not close the +// underlying stream. If a read returns an error, the stream must not be read +// from again. +export fn newdecoder( + enc: *encoding, + in: io::handle, +) decoder = { + return decoder { + stream = &decoder_vtable, + in = in, + enc = enc, + ... + }; +}; + +fn decode_reader( + s: *io::stream, + out: []u8 +) (size | io::EOF | io::error) = { + let s = s: *decoder; + if (len(out) == 0) { + return 0z; + }; + let n = 0z; + if (s.oavail != 0) { + if (len(out) <= s.oavail) { + out[..] = s.obuf[..len(out)]; + s.obuf[..len(s.obuf) - len(out)] = s.obuf[len(out)..]; + s.oavail = s.oavail - len(out): u8; + return len(out); + }; + n = s.oavail; + s.oavail = 0; + out[..n] = s.obuf[..n]; + out = out[n..]; + }; + let buf: [os::BUFSZ]u8 = [0...]; + buf[..s.iavail] = s.ibuf[..s.iavail]; + + let want = encodedsize(len(out)); + let nr = s.iavail: size; + let lim = if (want > len(buf)) len(buf) else want; + let eof = false; + match (io::readall(s.in, buf[s.iavail..lim])) { + case let n: size => + nr += n; + case io::EOF => + eof = true; + case let err: io::error => + if (!(err is io::underread)) { + return err; + }; + nr += err: io::underread; + }; + if (eof && nr == 0) { + return if (n != 0) n else io::EOF; + }; + if (s.pad) { + return errors::invalid; + }; + if (eof) { + const rem = nr % 4; + if (rem == 1) { + return errors::invalid; + }; + if (rem != 0) { + if (nr + (4 - rem) > len(buf)) { + return errors::invalid; + }; + for (let i = 0z; i < 4 - rem; i += 1) { + buf[nr + i] = PADDING; + }; + nr += 4 - rem; + s.pad = true; + }; + s.iavail = 0; + } else { + s.iavail = nr: u8 % 4; + s.ibuf[..s.iavail] = buf[nr - s.iavail..nr]; + nr -= s.iavail; + if (nr == 0) { + return 0z; + }; + }; + // Validating read buffer + let np = 0z; // Number of padding chars. + for (let i = 0z; i < nr; i += 1) { + if (buf[i] == PADDING) { + for (i + np < nr; np += 1) { + if (np > 2 || buf[i + np] != PADDING) { + return errors::invalid; + }; + }; + s.pad = true; + break; + }; + if (!ascii::valid(buf[i]: u32: rune) + || s.enc.decmap[buf[i]] == 0xff) { + return errors::invalid; + }; + buf[i] = s.enc.decmap[buf[i]]; + }; + + if (nr / 4 * 3 - np < len(out)) { + out = out[..nr / 4 * 3 - np]; + }; + let i = 0z, j = 0z; + nr -= 4; + for (i < nr) { + out[j ] = buf[i ] << 2 | buf[i + 1] >> 4; + out[j + 1] = buf[i + 1] << 4 | buf[i + 2] >> 2; + out[j + 2] = buf[i + 2] << 6 | buf[i + 3]; + + i += 4; + j += 3; + }; + s.obuf = [ + buf[i ] << 2 | buf[i + 1] >> 4, + buf[i + 1] << 4 | buf[i + 2] >> 2, + buf[i + 2] << 6 | buf[i + 3], + ]; + out[j..] = s.obuf[..len(out) - j]; + s.oavail = (len(s.obuf) - (len(out) - j)): u8; + s.obuf[..s.oavail] = s.obuf[len(s.obuf) - s.oavail..]; + s.oavail -= np: u8; + return n + len(out); +}; + +// Decodes a byte slice of ASCII-encoded base 64 data, using the given encoding, +// returning a slice of decoded bytes. The caller must free the return value. +export fn decodeslice( + enc: *encoding, + in: []u8, +) ([]u8 | errors::invalid | nomem) = { + if (len(in) == 0) { + return []; + }; + if (len(in) % 4 == 1) { + return errors::invalid; + }; + let ins = memio::fixed(in); + let decoder = newdecoder(enc, &ins); + let out = alloc([0u8...], decodedsize(len(in)))?; + let outs = memio::fixed(out); + match (io::copy(&outs, &decoder)) { + case nomem => + free(out); + return nomem; + case errors::invalid => + free(out); + return errors::invalid; + case let sz: size => + return memio::buffer(&outs)[..sz]; + case => abort(); // unreachable + }; +}; + +// Decodes a string of ASCII-encoded base 64 data, using the given encoding, +// returning a slice of decoded bytes. The caller must free the return value. +export fn decodestr(enc: *encoding, in: str) ([]u8 | errors::invalid | nomem) = { + return decodeslice(enc, strings::toutf8(in)); +}; + +// Decodes base64 data from a stream using the given alphabet, returning the +// number of bytes of bytes read (i.e. len(buf)). +export fn decode( + in: io::handle, + enc: *encoding, + buf: []u8, +) (size | io::EOF | io::error) = { + const dec = newdecoder(enc, in); + return io::readall(&dec, buf); +}; + +@test fn decode() void = { + // RFC 4648 test vectors + const cases: [_](str, str, *encoding) = [ + ("", "", &std_encoding), + ("Zg==", "f", &std_encoding), + ("Zm8=", "fo", &std_encoding), + ("Zm9v", "foo", &std_encoding), + ("Zm9vYg==", "foob", &std_encoding), + ("Zm9vYmE=", "fooba", &std_encoding), + ("Zm9vYmFy", "foobar", &std_encoding), + ]; + const invalid: [_](str, *encoding) = [ + // invalid padding + ("=", &std_encoding), + ("==", &std_encoding), + ("===", &std_encoding), + ("=====", &std_encoding), + ("======", &std_encoding), + // invalid characters + ("@Zg=", &std_encoding), + ("ê==", &std_encoding), + ("êg==", &std_encoding), + ("$3d==", &std_encoding), + ("%3d==", &std_encoding), + ("[==", &std_encoding), + ("!", &std_encoding), + // data after padding is encountered + ("Zg===", &std_encoding), + ("Zg====", &std_encoding), + ("Zg==Zg==", &std_encoding), + ("Zm8=Zm8=", &std_encoding), + ]; + let buf: [12]u8 = [0...]; + for (let bufsz = 1z; bufsz <= 12; bufsz += 1) { + for (let (input, expected, encoding) .. cases) { + let in = memio::fixed(strings::toutf8(input)); + let decoder = newdecoder(encoding, &in); + let buf = buf[..bufsz]; + let decb: []u8 = []; + defer free(decb); + for (true) match (io::read(&decoder, buf)!) { + case let z: size => + if (z > 0) { + append(decb, buf[..z]...)!; + }; + case io::EOF => + break; + }; + assert(bytes::equal(decb, strings::toutf8(expected))); + + // Testing decodestr should cover decodeslice too + let decb = decodestr(encoding, input) as []u8; + defer free(decb); + assert(bytes::equal(decb, strings::toutf8(expected))); + }; + + for (let (input, encoding) .. invalid) { + let in = memio::fixed(strings::toutf8(input)); + let decoder = newdecoder(encoding, &in); + let buf = buf[..bufsz]; + let valid = false; + for (true) match(io::read(&decoder, buf)) { + case errors::invalid => + break; + case size => + void; + case io::EOF => + abort(); + }; + + // Testing decodestr should cover decodeslice too + assert(decodestr(encoding, input) is errors::invalid); + }; + }; +}; + +// Given the length of the message, returns the size of its base64 encoding +export fn encodedsize(sz: size) size = if (sz == 0) 0 else ((sz - 1)/ 3 + 1) * 4; + +// Given the size of base64 encoded data, returns maximal length of decoded message. +// The message may be at most 2 bytes shorter than the returned value. +export fn decodedsize(sz: size) size = { + const rem = sz % 4; + const extra: size = if (rem == 0) 0 + else if (rem == 1) 0 + else if (rem == 2) 1 + else 2; + return (sz / 4) * 3 + extra; +}; + +@test fn sizecalc() void = { + let enc: [_](size, size) = [(1, 4), (2, 4), (3, 4), (4, 8), (10, 16), + (119, 160), (120, 160), (121, 164), (122, 164), (123, 164) + ]; + assert(encodedsize(0) == 0 && decodedsize(0) == 0); + for (let i = 0z; i < len(enc); i += 1) { + let (decoded, encoded) = enc[i]; + assert(encodedsize(decoded) == encoded); + assert(decodedsize(encoded) == ((decoded - 1) / 3 + 1) * 3); + }; +}; \ No newline at end of file diff --git a/age/format/format.ha b/age/format/format.ha index f7f1b2e..8db29bc 100644 --- a/age/format/format.ha +++ b/age/format/format.ha @@ -2,8 +2,9 @@ use os; use bufio; use strings; use fmt; -use encoding::base64; +use age::format::base64; use io; +use encoding::utf8; def INTRO: str = "age-encryption.org/v1"; def STANZA_PREFIX: str = "->"; @@ -38,7 +39,7 @@ fn is_valid_string(s: str) bool = { }; fn read_stanza(scanner: *bufio::scanner) (stanza | parse_error) = { - const line = match (bufio::scan_string(scanner, "\n")!) { + const line = match (bufio::scan_line(scanner)!) { case io::EOF => return fmt::fatal("unexpected end of file"); case let line: str => @@ -59,19 +60,24 @@ fn read_stanza(scanner: *bufio::scanner) (stanza | parse_error) = { }; }; - const recipient_type = args[0]; - const args = args[1..]; + fmt::printfln("recipient type: {}, args: {}\n", args[0], strings::join(", ", args[1..]...)!)!; + + const recipient_type = strings::dup(args[0])!; + let args_copy: []str = []; + for (let arg .. args[1..]) { + append(args_copy, strings::dup(arg)!)!; + }; let body: []u8 = []; for (true) { - const next_line = match (bufio::scan_string(scanner, "\n")!) { + const next_line = match (bufio::scan_line(scanner)!) { case io::EOF => return fmt::fatal("unexpected end of file"); case let line: str => + yield line; }; const b = decode_string(next_line)!; - if (len(b) > BYTES_PER_LINE) { return fmt::fatalf("malformed body line {}: too long", next_line); }; @@ -81,11 +87,10 @@ fn read_stanza(scanner: *bufio::scanner) (stanza | parse_error) = { // stanza bodies always end with short lines return stanza{ recipient_type = recipient_type, - args = args, + args = args_copy, body = body }; }; - }; }; @@ -107,7 +112,7 @@ export fn parse(path: str) ((header, bufio::scanner) | parse_error) = { let recipients: []stanza = []; - const version_line = match (bufio::scan_string(&scanner, "\n")!) { + const version_line = match (bufio::scan_line(&scanner)!) { case io::EOF => return fmt::fatal("unexpected end of file"); case let line: str => @@ -118,14 +123,18 @@ export fn parse(path: str) ((header, bufio::scanner) | parse_error) = { }; let header_mac: [32]u8 = [0...]; for (true) { - const next_line = match (bufio::scan_line(&scanner)!) { + const next_line = match (bufio::scan_line(&scanner)) { case io::EOF => return fmt::fatal("unexpected end of file"); + case utf8::invalid => + return fmt::fatal("invalid UTF-8 in input"); + case io::error => + return fmt::fatal("I/O error while reading input"); case let line: str => yield line; }; - if (strings::fromutf8(strings::toutf8(next_line)[0..3])! == FOOTER_PREFIX) { + if (strings::hasprefix(next_line, FOOTER_PREFIX)) { // we're at the footer line const (prefix, args) = split_args(next_line); if (prefix != FOOTER_PREFIX || len(args) != 1) { @@ -140,10 +149,12 @@ export fn parse(path: str) ((header, bufio::scanner) | parse_error) = { header_mac[..] = mac[0..32]; break; }; - bufio::unread(&scanner, strings::toutf8(next_line)); + bufio::unread(&scanner, strings::toutf8(strings::concat(next_line, "\n")!)); + const found_stanza = read_stanza(&scanner)!; append(recipients, found_stanza)!; + }; const prefix = bufio::scan_buffer(&scanner); bufio::unread(&scanner, prefix); diff --git a/cmd/hage.ha b/cmd/hage.ha index a044a39..2a6a300 100644 --- a/cmd/hage.ha +++ b/cmd/hage.ha @@ -1,12 +1,9 @@ use fmt; -use memio; use os; use getopt; -use time::date; -use bytes; -use strings; -use age::x25519; use age::format; +use strings; +use encoding::base64; type command_mode = enum { encrypt, @@ -17,11 +14,13 @@ export fn main() void = { const cmd = getopt::parse(os::args, "age encryption implementation", ('o', "output", "output file"), ('d', "decrypt the input to the output file"), - ('e', "encrypt the input to the output file. default if omitted")); + ('e', "encrypt the input to the output file. default if omitted"), "INPUT"); if (len(cmd.args) > 1) { fmt::fatal("too many arguments"); + } else if (len(cmd.args) == 0) { + fmt::fatal("missing input file argument"); }; @@ -41,6 +40,12 @@ export fn main() void = { if (mode == command_mode::encrypt) { fmt::println("encrypting")!; } else { - fmt::println("decrypting")!; + const output = format::parse(cmd.args[0])!; + const header = output.0; + fmt::printfln("header mac: {}", base64::encodestr(&base64::std_encoding, header.mac)!)!; + for (let stanza .. header.recipients) { + fmt::printfln("recipient type: {}, args: {}", stanza.recipient_type, strings::join(", ", stanza.args...)!)!; + }; + fmt::println("decrypting")!; }; };