/*
 This file is part of GNU Anastasis
 (C) 2021-2022 Anastasis SARL
 GNU Anastasis is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.
 GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
 You should have received a copy of the GNU Affero General Public License along with
 GNU Anastasis; see the file COPYING.  If not, see 
 */
import {
  canonicalJson,
  decodeCrock,
  encodeCrock,
  getRandomBytes,
  kdfKw,
  secretbox,
  crypto_sign_keyPair_fromSeed,
  stringToBytes,
  secretbox_open,
  hash,
  bytesToString,
} from "@gnu-taler/taler-util";
import { argon2id } from "hash-wasm";
export type Flavor = T & {
  _flavor?: `anastasis.${FlavorT}`;
};
export type FlavorP = T & {
  _flavor?: `anastasis.${FlavorT}`;
  _size?: S;
};
export type UserIdentifier = Flavor;
export type ServerSalt = Flavor;
export type PolicySalt = Flavor;
export type PolicyKey = FlavorP;
export type KeyShare = Flavor;
export type EncryptedKeyShare = Flavor;
export type EncryptedTruth = Flavor;
export type EncryptedCoreSecret = Flavor;
export type EncryptedMasterKey = Flavor;
export type EddsaPublicKey = Flavor;
export type EddsaPrivateKey = Flavor;
export type TruthUuid = Flavor;
export type SecureAnswerHash = Flavor;
/**
 * Truth-specific randomness, also called question salt sometimes.
 */
export type TruthSalt = Flavor;
/**
 * Truth key, found in the recovery document.
 */
export type TruthKey = Flavor;
export type EncryptionNonce = Flavor;
export type OpaqueData = Flavor;
const nonceSize = 24;
const masterKeySize = 64;
export async function userIdentifierDerive(
  idData: any,
  serverSalt: ServerSalt,
): Promise {
  const canonIdData = canonicalJson(idData);
  const hashInput = stringToBytes(canonIdData);
  const result = await argon2id({
    hashLength: 64,
    iterations: 3,
    memorySize: 1024 /* kibibytes */,
    parallelism: 1,
    password: hashInput,
    salt: decodeCrock(serverSalt),
    outputType: "binary",
  });
  return encodeCrock(result);
}
export interface AccountKeyPair {
  priv: EddsaPrivateKey;
  pub: EddsaPublicKey;
}
export function accountKeypairDerive(userId: UserIdentifier): AccountKeyPair {
  // FIXME: the KDF invocation looks fishy, but that's what the C code presently does.
  const d = kdfKw({
    outputLength: 32,
    ikm: decodeCrock(userId),
    info: stringToBytes("ver"),
  });
  const pair = crypto_sign_keyPair_fromSeed(d);
  return {
    priv: encodeCrock(d),
    pub: encodeCrock(pair.publicKey),
  };
}
/**
 * Encrypt the recovery document.
 *
 * The caller should first compress the recovery doc.
 */
export async function encryptRecoveryDocument(
  userId: UserIdentifier,
  recoveryDocData: OpaqueData,
): Promise {
  const nonce = encodeCrock(getRandomBytes(nonceSize));
  return anastasisEncrypt(nonce, asOpaque(userId), recoveryDocData, "erd");
}
/**
 * Encrypt the recovery document.
 *
 * The caller should first compress the recovery doc.
 */
export async function decryptRecoveryDocument(
  userId: UserIdentifier,
  recoveryDocData: OpaqueData,
): Promise {
  return anastasisDecrypt(asOpaque(userId), recoveryDocData, "erd");
}
export interface PolicyMetadata {
  secret_name: string;
  policy_hash: string;
}
export async function encryptPolicyMetadata(
  userId: UserIdentifier,
  metadata: PolicyMetadata,
): Promise {
  const metadataBytes = typedArrayConcat([
    decodeCrock(metadata.policy_hash),
    stringToBytes(metadata.secret_name),
  ]);
  const nonce = encodeCrock(getRandomBytes(nonceSize));
  return anastasisEncrypt(
    nonce,
    asOpaque(userId),
    encodeCrock(metadataBytes),
    "rmd",
  );
}
export async function decryptPolicyMetadata(
  userId: UserIdentifier,
  metadataEnc: OpaqueData,
): Promise {
  const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd");
  const metadataBytes = decodeCrock(plain);
  const policyHash = encodeCrock(metadataBytes.slice(0, 64));
  const secretName = bytesToString(metadataBytes.slice(64));
  return {
    policy_hash: policyHash,
    secret_name: secretName,
  };
}
export function typedArrayConcat(chunks: Uint8Array[]): Uint8Array {
  let payloadLen = 0;
  for (const c of chunks) {
    payloadLen += c.byteLength;
  }
  const buf = new ArrayBuffer(payloadLen);
  const u8buf = new Uint8Array(buf);
  let p = 0;
  for (const c of chunks) {
    u8buf.set(c, p);
    p += c.byteLength;
  }
  return u8buf;
}
export async function policyKeyDerive(
  keyShares: KeyShare[],
  policySalt: PolicySalt,
): Promise {
  const chunks = keyShares.map((x) => decodeCrock(x));
  const polKey = kdfKw({
    outputLength: 64,
    ikm: typedArrayConcat(chunks),
    salt: decodeCrock(policySalt),
    info: stringToBytes("anastasis-policy-key-derive"),
  });
  return encodeCrock(polKey);
}
async function deriveKey(
  keySeed: OpaqueData,
  nonce: EncryptionNonce,
  salt: string,
): Promise {
  return kdfKw({
    outputLength: 32,
    salt: decodeCrock(nonce),
    ikm: decodeCrock(keySeed),
    info: stringToBytes(salt),
  });
}
async function anastasisEncrypt(
  nonce: EncryptionNonce,
  keySeed: OpaqueData,
  plaintext: OpaqueData,
  salt: string,
): Promise {
  const key = await deriveKey(keySeed, nonce, salt);
  const nonceBuf = decodeCrock(nonce);
  const cipherText = secretbox(decodeCrock(plaintext), decodeCrock(nonce), key);
  return encodeCrock(typedArrayConcat([nonceBuf, cipherText]));
}
async function anastasisDecrypt(
  keySeed: OpaqueData,
  ciphertext: OpaqueData,
  salt: string,
): Promise {
  const ctBuf = decodeCrock(ciphertext);
  const nonceBuf = ctBuf.slice(0, nonceSize);
  const enc = ctBuf.slice(nonceSize);
  const key = await deriveKey(keySeed, encodeCrock(nonceBuf), salt);
  const clearText = secretbox_open(enc, nonceBuf, key);
  if (!clearText) {
    throw Error("could not decrypt");
  }
  return encodeCrock(clearText);
}
export const asOpaque = (x: string): OpaqueData => x;
const asEncryptedKeyShare = (x: OpaqueData): EncryptedKeyShare => x as string;
const asEncryptedTruth = (x: OpaqueData): EncryptedTruth => x as string;
const asKeyShare = (x: OpaqueData): KeyShare => x as string;
export async function encryptKeyshare(
  keyShare: KeyShare,
  userId: UserIdentifier,
  answerSalt?: string,
): Promise {
  const s = answerSalt ?? "eks";
  const nonce = encodeCrock(getRandomBytes(24));
  return asEncryptedKeyShare(
    await anastasisEncrypt(nonce, asOpaque(userId), asOpaque(keyShare), s),
  );
}
export async function decryptKeyShare(
  encKeyShare: EncryptedKeyShare,
  userId: UserIdentifier,
  answerSalt?: string,
): Promise {
  const s = answerSalt ?? "eks";
  return asKeyShare(
    await anastasisDecrypt(asOpaque(userId), asOpaque(encKeyShare), s),
  );
}
export async function encryptTruth(
  nonce: EncryptionNonce,
  truthEncKey: TruthKey,
  truth: OpaqueData,
): Promise {
  const salt = "ect";
  return asEncryptedTruth(
    await anastasisEncrypt(nonce, asOpaque(truthEncKey), truth, salt),
  );
}
export async function decryptTruth(
  truthEncKey: TruthKey,
  truthEnc: EncryptedTruth,
): Promise {
  const salt = "ect";
  return await anastasisDecrypt(
    asOpaque(truthEncKey),
    asOpaque(truthEnc),
    salt,
  );
}
export interface CoreSecretEncResult {
  encCoreSecret: EncryptedCoreSecret;
  encMasterKeys: EncryptedMasterKey[];
}
export async function coreSecretRecover(args: {
  encryptedMasterKey: OpaqueData;
  policyKey: PolicyKey;
  encryptedCoreSecret: OpaqueData;
}): Promise {
  const masterKey = await anastasisDecrypt(
    asOpaque(args.policyKey),
    args.encryptedMasterKey,
    "emk",
  );
  return await anastasisDecrypt(masterKey, args.encryptedCoreSecret, "cse");
}
export async function coreSecretEncrypt(
  policyKeys: PolicyKey[],
  coreSecret: OpaqueData,
): Promise {
  const masterKey = getRandomBytes(masterKeySize);
  const nonce = encodeCrock(getRandomBytes(nonceSize));
  const coreSecretEncSalt = "cse";
  const masterKeyEncSalt = "emk";
  const encCoreSecret = (await anastasisEncrypt(
    nonce,
    encodeCrock(masterKey),
    coreSecret,
    coreSecretEncSalt,
  )) as string;
  const encMasterKeys: EncryptedMasterKey[] = [];
  for (let i = 0; i < policyKeys.length; i++) {
    const polNonce = encodeCrock(getRandomBytes(nonceSize));
    const encMasterKey = await anastasisEncrypt(
      polNonce,
      asOpaque(policyKeys[i]),
      encodeCrock(masterKey),
      masterKeyEncSalt,
    );
    encMasterKeys.push(encMasterKey as string);
  }
  return {
    encCoreSecret,
    encMasterKeys,
  };
}
export async function pinAnswerHash(pin: number): Promise {
  return encodeCrock(hash(stringToBytes(pin.toString())));
}
export async function secureAnswerHash(
  answer: string,
  truthUuid: TruthUuid,
  questionSalt: TruthSalt,
): Promise {
  const powResult = await argon2id({
    hashLength: 64,
    iterations: 3,
    memorySize: 1024 /* kibibytes */,
    parallelism: 1,
    password: stringToBytes(answer),
    salt: decodeCrock(questionSalt),
    outputType: "binary",
  });
  const kdfResult = kdfKw({
    outputLength: 64,
    salt: decodeCrock(truthUuid),
    ikm: powResult,
    info: stringToBytes("anastasis-secure-question-hashing"),
  });
  return encodeCrock(kdfResult);
}