File and message encryption for MoonBit, for one or several recipients.
// Alice has a key pair; the public half is what a sender needs.
let alice = @x25519.Identity::generate(random=@system.source) catch {
_ => abort("no randomness")
}
println(alice.code()) // AGE-SECRET-KEY-1…
println(alice.public().code()) // age1…
let recipients : Array[&@spec.Recipient] = [alice.public()]
let file = @moonseal.seal(recipients, b"the quick brown fox", random=@system.source) catch {
_ => abort("cannot seal")
}
let readers : Array[&@spec.Identity] = [alice]
println(@moonseal.open(readers, file)) // b"the quick brown fox"| Package | What it does | Specification |
|---|---|---|
| moonseal | Sealing and opening, in memory or as a stream, with or without an armor | age v1 |
| spec | The error type, the stanza, and the two traits a recipient type implements | |
| header | The stanzas and the MAC that binds them to the payload | age v1 |
| stream | The payload: chunks of 64 KiB, each authenticated on its own | age v1 |
| armor | The same file as text, when a binary cannot travel | RFC 7468 |
| x25519 | A recipient type for a public key | RFC 7748 |
| scrypt | A recipient type for a passphrase | RFC 7914 |
moon add moonbitstack/moonseal
moon add moonbitstack/moonseedlet file = @moonseal.seal(
[@scrypt.Recipient::new("correct horse battery staple", log_n=14)],
b"the quick brown fox",
random=@system.source,
) catch {
_ => abort("cannot seal")
}
let readers : Array[&@spec.Identity] = [
@scrypt.Identity::new("correct horse battery staple"),
]
println(@moonseal.open(readers, file))let sink = @moonseal.BytesSink::new()
@moonseal.seal_stream(
sink,
[@x25519.Recipient::of("age1…") catch { _ => abort("no") }],
@moonseal.BytesSource::new(b"the quick brown fox"),
random=@system.source,
) catch {
_ => abort("cannot seal")
}let text = @moonseal.seal_armored(
[@x25519.Recipient::of("age1…") catch { _ => abort("no") }],
b"the quick brown fox",
random=@system.source,
) catch {
_ => abort("cannot seal")
}
println(text)struct Example {}
impl @spec.Recipient for Example with fn wrap(self, file_key~, random~) {
ignore(self)
ignore(random)
// The body is whatever the matching identity can unwrap. This one hands the
// file key over as it is, which is not a design to use; a real type draws
// its ephemeral material with `@spec.draw(random, len)`.
@spec.Stanza::new(kind="example.com/plain", args=[], body=file_key.to_owned())
}
impl @spec.Identity for Example with fn unwrap(self, stanza~) {
ignore(self)
if stanza.kind == "example.com/plain" {
return Some(stanza.body)
}
None
}| seal(recipients, plain, random~) -> Bytes | Seal bytes for a list of recipients. |
| open(identities, cipher) -> Bytes | Open a file with any one of these identities. |
| seal_armored(recipients, plain, random~) -> String | The same file, as text. |
| open_armored(identities, text) -> Bytes | Open an armored block. |
| seal_stream(sink, recipients, source, random~) | Seal from a source into a sink. |
| open_stream(sink, identities, source) | Open from a source into a sink. |
| seal_stream_armored(sink, recipients, source, random~) | The same, as text. |
| open_stream_armored(sink, identities, source) | The same, as text. |
| drain(source) -> Bytes | Everything a source has. |
| BytesSource::new(BytesView) / BytesSink::new() / BytesSink::bytes() | The in-memory pair. |
| Source::read(limit~) / Sink::write(bytes~) | What a caller implements to attach a file, a socket or a pipe. |
| Refused | Why a call did not do what it was asked: a header that is not the format, a stanza of a known type that is not either, a MAC that does not verify, no identity that opens anything, a chunk that does not authenticate, armor that is not armor, a key of the wrong size, a random source that failed, an input past a limit. |
| Stanza { kind, args, body } | One recipient stanza, as the header carries it. |
| draw(random, len) -> Bytes | Bytes from a random source, with its failure as a Refused. |
| Recipient::wrap(self, file_key~, random~) -> Stanza | What a recipient type implements to address a file. |
| Identity::unwrap(self, stanza~) -> Bytes? | What it implements to open one. |
| Header::decode(bytes) -> (Header, Int) | Read a header, and say where the payload starts. |
| Header::encode(file_key~) -> Bytes | Write it, MAC line and all. |
| Header::new(stanzas) / verify(file_key~) | Build one; check the MAC of one that was read. |
| Header::stanzas() | The stanzas, in the order they are written. |
| stream.chunk / stream.tag / stream.nonce | 64 KiB, 16 bytes, 16 bytes. |
| stream.payload_key(file_key~, nonce~) | The payload key for one file. |
| Sealer::chunk(plain~, last~) / Opener::chunk(cipher~, last~) | One chunk at a time, for callers building their own plumbing. |
| armor.encode(bytes) / armor.decode(text) | The text form. |
| armor.Encoder / armor.Decoder | The same a piece at a time. |
| x25519.Identity::generate(random~) / Identity::new(scalar) | A secret key, from randomness or from bytes. |
| x25519.Identity::of(text) / code() | AGE-SECRET-KEY-1… in and out. |
| x25519.Identity::public() / Recipient::of(text) / Recipient::code() | age1… in and out. |
| scrypt.Recipient::new(passphrase, log_n?) / scrypt.Identity::new(passphrase, max_log_n?) | A passphrase, with the work factor to write and the one to accept. |
age-encryption.org/v1
-> X25519 aQwdcB4/zsAqbGhOXDVcZMGvC5J5bTdhqqZyiK6V9l0
n3ccxIiZEos8us5SboQVs0swdWU9NV2gbGJgYKAu+Xk
--- A7bGuYb5XUntlaP2+ePJxehnm4uFu1f3Loora2+eEmg
<16-byte nonce><chunks of 64 KiB, each with a 16-byte tag>| Thing | Why not here |
|---|---|
| Key storage, trust, rotation, revocation | All four need state, and a library with state is no longer a format. |
| A random-number generator | moonrand has them; the caller picks one and hands it in. |
| Certificates and tokens | Those assert who somebody is. This is a message, and asserts nothing. |
pub struct BytesSink {
// private fields
}pub struct BytesSource {
// private fields
}impl Source for BytesSourceInstall
Download zipFile and message encryption for MoonBit, for one or several recipients.