diff --git a/age/format/format.ha b/age/format/format.ha index 8db29bc..cdfe4a3 100644 --- a/age/format/format.ha +++ b/age/format/format.ha @@ -99,7 +99,7 @@ fn split_args(line: str) (str, []str) = { return (parts[0], parts[1..]); }; -fn decode_string(encoded: str) ([]u8 | parse_error) = { +export fn decode_string(encoded: str) ([]u8 | parse_error) = { if (strings::contains(encoded, "\n", "\r")) { return fmt::fatalf("unexpected newline character"); }; diff --git a/age/x25519/x25519.ha b/age/x25519/x25519.ha index f842563..7df4285 100644 --- a/age/x25519/x25519.ha +++ b/age/x25519/x25519.ha @@ -9,8 +9,17 @@ use crypto::random; use memio; use crypto::curve25519; use bytes; +use crypto::sha256; +use age::format; +use crypto::hkdf; +use crypto::chachapoly; -const MEGABYTE: size = 1024 * 1024; +export type incorrectidentity = !void; + +export type parsingerror = !void; + +def MEGABYTE: size = 1024 * 1024; +def FILE_KEY_SIZE: size = 16; // an x25519 identity. // @@ -90,3 +99,63 @@ export fn read_identity_file(file: str) []identity = { }; return identities; }; + +export type incorrect_ciphertext_size = !void; +export type aead_decrypt_error = !(void | incorrect_ciphertext_size); + +fn aead_decrypt(key: []u8, msg_size: size, ciphertext: []u8) ([]u8 | aead_decrypt_error) = { + let output: [32]u8 = [0...]; + const nonce: []u8 = [0...]; + + if (len(ciphertext) > (msg_size + chachapoly::TAGSZ)) { + return incorrect_ciphertext_size; + }; + const aead = chachapoly::chachapoly(); + chachapoly::init(&aead, &memio::fixed(output), key, nonce); + + return output; +}; + +export fn unwrap_file_key(identity: *identity, block: format::stanza) ([16]u8 | incorrectidentity | parsingerror | aead_decrypt_error) = { + if (block.recipient_type != "X25519") { + return incorrectidentity; + }; + if (len(block.args) != 1) { + return parsingerror; + }; + const public_key = format::decode_string(block.args[0])!; + const our_public_key = match (bech32::decodestr(private_to_public(*identity))) { + case let data: (str, []u8) => + let (hrp, key_bytes) = data; + if (ascii::strlower(hrp)! != "age" || len(key_bytes) != 32) { + fmt::fatal("invalid public key generated from private key"); + }; + defer bytes::zero(key_bytes); + const key: [32]u8 = [0...]; + io::copy(&memio::fixed(key), &memio::fixed(key_bytes))!; + yield key; + case => + fmt::fatal("invalid public key generated from private key"); + }; + if (len(public_key) != curve25519::POINTSZ) { + return parsingerror; + }; + + let shared_secret: []u8 = []; + curve25519::x25519(shared_secret, *identity, public_key); + + let salt: [64]u8 = [0...]; + salt[0..32] = public_key[0..32]; + salt[32..64] = our_public_key[0..32]; + + let wrapping_key: [32]u8 = [0...]; + let key_buf: [sha256::SZ + sha256::BLOCKSZ]u8 = [0...]; + const h = hkdf::hkdf(&sha256::sha256(), wrapping_key, shared_secret, strings::toutf8("age-encryption.org/v1/X25519"), salt, key_buf); + + const info = "age-encryption.org/v1/X25519"; + + let unwrapped: [16]u8 = [0...]; + unwrapped[0..16] = aead_decrypt(wrapping_key, FILE_KEY_SIZE, block.body)![0..16]; + + return unwrapped; +}; \ No newline at end of file