// The key files.
//
// A public key is stored as the 32 raw bytes. A secret key is stored raw as
// well, but the form that a passphrase protects is the one to use: the raw
// form leaves the long-term secret key in clear text on disk, and anyone
// who reads it can decrypt every package that was ever sealed for it.
//
// The layout of the protected form is the one of the reference
// implementation, with a magic of its own:
//
// offset size field
// 0 8 magic "LUNAPK01"
// 8 1 format version (1)
// 9 1 algorithm (1)
// 10 4 iterations, big endian
// 14 16 salt
// 30 12 nonce
// 42 end of the header
// 42 32 the secret key, encrypted
// 74 16 the tag
//
// The header is the additional data of the encryption, so it cannot be
// changed without the tag failing.
///|
/// The magic of a protected key file.
pub const KEY_MAGIC : String = "LUNAPK01"
///|
/// The version of the protected key file format.
pub const KEY_VERSION : Int32 = 1
///|
/// The algorithm of the protected key file format.
pub const KEY_ALGORITHM : Int32 = 1
///|
/// The number of bytes of the salt of the protected key file.
pub const KEY_SALT_BYTES : Int32 = 16
///|
/// The number of bytes of the header of the protected key file.
pub const KEY_HEADER_BYTES : Int32 = 42
///|
/// Write the public key of a pair to `path`.
pub async fn save_public_key(path : String, public_key : Bytes) -> Unit {
@fs.write_binary(path, public_key)
}
///|
/// Read the public key from `path`.
pub async fn load_public_key(path : String) -> Bytes {
let data = @fs.read_binary(path)
if data.length() != KEY_BYTES {
fail(
"the public key file must hold exactly " +
KEY_BYTES.to_string() +
" bytes, but it holds " +
data.length().to_string(),
)
}
data
}
///|
/// Write the secret key of a pair to `path`, protected by a passphrase.
pub async fn save_secret_key(
path : String,
secret_key : Bytes,
passphrase : String,
) -> Unit {
let salt = random_bytes(KEY_SALT_BYTES)
let nonce = random_bytes(NONCE_BYTES)
let wrapping_key = pbkdf2_sha512(
string_to_utf8_bytes(passphrase),
salt,
PASSPHRASE_ITERATIONS,
KEY_BYTES,
)
let header = key_header(PASSPHRASE_ITERATIONS, salt, nonce)
let sealed_data = chacha20poly1305_seal(
wrapping_key, nonce, secret_key, header,
)
@fs.write_binary(path, bytes_concat(header, sealed_data))
}
///|
/// Write the secret key of a pair to `path`, without a passphrase.
///
/// The key is in clear text on disk, so anyone who reads the file can
/// decrypt every package that was sealed for it. Use the protected form
/// unless there is a reason not to.
pub async fn save_secret_key_raw(path : String, secret_key : Bytes) -> Unit {
@fs.write_binary(path, secret_key)
}
///|
/// The header of a protected key file.
fn key_header(iterations : Int32, salt : Bytes, nonce : Bytes) -> Bytes {
let out = FixedArray::make(KEY_HEADER_BYTES, b'\x00')
let magic = string_to_utf8_bytes(KEY_MAGIC)
let mut i = 0
while i < 8 {
out[i] = magic.at(i)
i = i + 1
}
out[8] = KEY_VERSION.to_byte()
out[9] = KEY_ALGORITHM.to_byte()
let n = iterations
out[10] = ((n >> 24) & 0xff).to_byte()
out[11] = ((n >> 16) & 0xff).to_byte()
out[12] = ((n >> 8) & 0xff).to_byte()
out[13] = (n & 0xff).to_byte()
i = 0
while i < KEY_SALT_BYTES {
out[14 + i] = salt.at(i)
i = i + 1
}
i = 0
while i < NONCE_BYTES {
out[30 + i] = nonce.at(i)
i = i + 1
}
Bytes::from_array(out[:])
}
///|
/// Is the file at `path` a key file that a passphrase protects?
pub async fn is_protected_key_file(path : String) -> Bool {
if !@fs.exists(path) {
return false
}
let data = @fs.read_binary(path)
if data.length() < KEY_HEADER_BYTES {
return false
}
let magic = string_to_utf8_bytes(KEY_MAGIC)
let mut i = 0
while i < 8 {
if data.at(i) != magic.at(i) {
return false
}
i = i + 1
}
true
}
///|
/// Read the secret key of a pair from `path`.
///
/// A file that a passphrase protects needs the passphrase; a file that holds
/// the raw key is read as it is, and the passphrase is not used.
pub async fn load_secret_key(path : String, passphrase : String) -> Bytes {
if !is_protected_key_file(path) {
let data = @fs.read_binary(path)
if data.length() != KEY_BYTES {
fail(
"the secret key file must hold exactly " +
KEY_BYTES.to_string() +
" bytes, but it holds " +
data.length().to_string(),
)
}
return data
}
let file = @fs.read_binary(path)
if file.length() < KEY_HEADER_BYTES + KEY_BYTES + 16 {
fail("the secret key file is too short")
}
let magic = string_to_utf8_bytes(KEY_MAGIC)
let mut i = 0
while i < 8 {
if file.at(i) != magic.at(i) {
fail("the secret key file has a bad magic")
}
i = i + 1
}
if file.at(8) != KEY_VERSION.to_byte() {
fail("the secret key file has an unsupported version")
}
if file.at(9) != KEY_ALGORITHM.to_byte() {
fail("the secret key file has an unsupported algorithm")
}
let iterations = (file.at(10).to_int() << 24) |
(file.at(11).to_int() << 16) |
(file.at(12).to_int() << 8) |
file.at(13).to_int()
let salt = FixedArray::make(KEY_SALT_BYTES, b'\x00')
i = 0
while i < KEY_SALT_BYTES {
salt[i] = file.at(14 + i)
i = i + 1
}
let nonce = FixedArray::make(NONCE_BYTES, b'\x00')
i = 0
while i < NONCE_BYTES {
nonce[i] = file.at(30 + i)
i = i + 1
}
let header = truncate_bytes(file, KEY_HEADER_BYTES)
let sealed_data = FixedArray::make(file.length() - KEY_HEADER_BYTES, b'\x00')
i = 0
while i < sealed_data.length() {
sealed_data[i] = file.at(KEY_HEADER_BYTES + i)
i = i + 1
}
let wrapping_key = pbkdf2_sha512(
string_to_utf8_bytes(passphrase),
Bytes::from_array(salt[:]),
iterations,
KEY_BYTES,
)
let secret = chacha20poly1305_open(
wrapping_key,
Bytes::from_array(nonce[:]),
Bytes::from_array(sealed_data[:]),
header,
)
match secret {
Some(k) => k
None => fail("the passphrase is wrong, or the secret key file is damaged")
}
}