// The key files.
//
// A secret key is stored raw, or protected by a passphrase: 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.
//
// A pair of keys serves two purposes, and each purpose has a public key of
// its own. The X25519 one is the key a package is sealed to, and the Ed25519
// one is the key a signature is checked with. The two are derived from the
// same secret with a label of their own, so that the key that opens a package
// is not the key that signs one.
//
// The public key file therefore holds both of them, as a text a person can
// read:
//
//   lunapack-public-key v1
//   x25519 
//   ed25519 
//
// A public key file of the older form holds the 32 raw bytes of the
// encryption public key alone, and is still read as such.
//
// The layout of the protected secret key file 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

///|
/// The first line of a public key file.
pub const PUBLIC_KEY_HEADER : String = "lunapack-public-key v1"

///|
/// The name of the encryption public key in a public key file.
pub const PUBLIC_KEY_ENCRYPTION : String = "x25519"

///|
/// The name of the signing public key in a public key file.
pub const PUBLIC_KEY_SIGNING : String = "ed25519"

///|
/// The label of the derivation of the signing seed.
const LABEL_SIGNING_SEED : String = "lunapack-v6|ed25519-seed"

///|
/// The two public keys of a pair.
pub struct PublicKeyPair {
  /// The X25519 public key: the key a package is sealed to.
  encryption : Bytes
  /// The Ed25519 public key: the key a signature is checked with.
  signing : Bytes
}

///|
/// The seed of the signing key of the secret key `secret`.
///
/// The seed is derived with HKDF-SHA512 and a label of its own, so the key
/// that signs is not the key that decrypts: the two roles cannot be taken for
/// one another, and a fault in one of them does not hand the other over.
pub fn ed25519_seed(secret : Bytes) -> Bytes {
  hkdf_sha512(
    secret,
    zero_bytes(SHA512_BYTES),
    string_to_utf8_bytes(LABEL_SIGNING_SEED),
    KEY_BYTES,
  )
}

///|
/// The two public keys of the secret key `secret`.
pub fn public_key_pair(secret : Bytes) -> PublicKeyPair raise {
  {
    encryption: x25519_public_key(secret),
    signing: ed25519_public_key(ed25519_seed(secret)),
  }
}

///|
/// Write the public key of a pair to `path`.
///
/// This is the older form: the 32 raw bytes of the encryption public key
/// alone. A file written this way cannot be used to check a signature; use
/// `save_public_key_pair` for a pair that serves both purposes.
pub async fn save_public_key(path : String, public_key : Bytes) -> Unit {
  @fs.write_binary(path, public_key)
}

///|
/// Write the two public keys of a pair to `path`.
///
/// The file is a text, so that a person can read what it holds.
pub async fn save_public_key_pair(
  path : String,
  encryption : Bytes,
  signing : Bytes,
) -> Unit {
  let text = PUBLIC_KEY_HEADER +
    "\n" +
    PUBLIC_KEY_ENCRYPTION +
    " " +
    bytes_to_hex(encryption) +
    "\n" +
    PUBLIC_KEY_SIGNING +
    " " +
    bytes_to_hex(signing) +
    "\n"
  @fs.write_text(path, text)
}

///|
/// Read the encryption public key from `path`.
///
/// A file that holds the two public keys of a pair gives the X25519 one, and
/// a file that holds the 32 raw bytes of an older pair gives those: the
/// encryption public key is the same in both.
pub async fn load_public_key(path : String) -> Bytes {
  let data = @fs.read_binary(path)
  if data.length() == KEY_BYTES {
    return data
  }
  match public_key_of(data, PUBLIC_KEY_ENCRYPTION) {
    Some(key) => key
    None =>
      fail(
        "the public key file does not hold a public key of the kind that is needed",
      )
  }
}

///|
/// Read the signing public key from `path`.
///
/// It is the Ed25519 public key of the pair: the one a signature of a package
/// is checked with. A file of the older form holds the encryption public key
/// alone, so it cannot be used here.
pub async fn load_signing_public_key(path : String) -> Bytes {
  let data = @fs.read_binary(path)
  if data.length() == KEY_BYTES {
    fail(
      "this public key file holds only the encryption public key of an older pair: make a new pair with keygen to sign with it",
    )
  }
  match public_key_of(data, PUBLIC_KEY_SIGNING) {
    Some(key) => key
    None =>
      fail(
        "the public key file does not hold the signing public key that is needed",
      )
  }
}

///|
/// The public key named `algorithm` in the bytes of a public key file.
///
/// Returns None when the file is not a public key file or does not hold a
/// key of that algorithm.
fn public_key_of(data : Bytes, algorithm : String) -> Bytes? {
  let lines = text_lines(utf8_bytes_to_string(data))
  if lines.length() < 3 {
    return None
  }
  if lines[0] != PUBLIC_KEY_HEADER {
    return None
  }
  let prefix = algorithm + " "
  for line in lines {
    if line.has_prefix(prefix) {
      return hex_to_bytes(line[prefix.length():].to_owned())
    }
  }
  None
}

///|
/// 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")
  }
}