(* partial PKCS12 implementation, as defined in RFC 7292
   - no public/private key mode, only password privacy and integrity
   - algorithmidentifier those I need for openssl interop (looking at the p12 I have on my disk)
   - require version being 3

   some definitions from PKCS7 (RFC 2315) are implemented as well, as needed
*)

type content_info = Asn.oid * string

type digest_info = Algorithm.t * string

type mac_data = digest_info * string * int

type t = string * mac_data

module Asn = struct
  open Asn_grammars
  open Asn.S
  open Registry

  let encrypted_content_info =
    let f (oid, algo, content) =
      if Asn.OID.equal PKCS7.data oid then
        (algo, content)
      else
        parse_error "expected OID PKCS7 data"
    and g (algo, content) =
      (PKCS7.data, algo, content)
    in
    Asn.S.map f g @@
    sequence3
      (required ~label:"content type" oid) (* here we assume data!? *)
      (required ~label:"content encryption algorithm" Algorithm.identifier)
      (optional ~label:"encrypted content" (implicit 0 octet_string))

  let encrypted_data =
    let f (v, eci) =
      if v = 0 then eci else parse_error "unknown encrypted data version"
    and g eci = 0, eci
    in
    map f g @@
    sequence2
      (required ~label:"version" int)
      (required ~label:"encrypted content info" encrypted_content_info)

  let content_info =
    let f (oid, data) =
      match data with
      | None -> parse_error "found no value for content info"
      | Some `C1 data when Asn.OID.equal PKCS7.data oid -> `Data data
      | Some `C2 eci when Asn.OID.equal PKCS7.encrypted_data oid -> `Encrypted eci
      | _ -> parse_error "couldn't match PKCS7 oid with choice"
    and g = function
      | `Data data -> PKCS7.data, Some (`C1 data)
      | `Encrypted eci -> PKCS7.encrypted_data, Some (`C2 eci)
    in
    map f g @@
    sequence2
      (required ~label:"content type" oid)
      (optional ~label:"content" (explicit 0
                                    (choice2 octet_string encrypted_data)))

  let digest_info =
    sequence2
      (required ~label:"digest algorithm" Algorithm.identifier)
      (required ~label:"digest" octet_string)

  let mac_data =
    sequence3
      (required ~label:"mac" digest_info)
      (required ~label:"mac salt" octet_string)
      (required ~label:"iterations" int)

  let pfx =
    let f (version, content_info, mac_data) =
      if version = 3 then
        match content_info, mac_data with
        | `Data data, Some md -> data, md
        | _, None -> parse_error "missing mac_data"
        | _, _ -> parse_error "unsupported content_info"
      else
        parse_error "unsupported pfx version"
    and g (content, mac_data) =
      (3, `Data content, Some mac_data)
    in
    map f g @@
    sequence3
      (required ~label:"version" int)
      (required ~label:"auth safe" content_info)
      (* contentType is signedData in public-key integrity mode and data in
         password integrity mode *)
      (optional ~label:"mac data" mac_data) (* not present if public keys used *)

  let pfx_of_cs, pfx_to_cs = projections_of Asn.der pfx

  (* payload is a sequence of content_info *)
  let authenticated_safe = sequence_of content_info

  let auth_safe_of_cs, auth_safe_to_cs =
    projections_of Asn.der authenticated_safe

  let pkcs12_attribute =
    sequence2
      (required ~label:"attribute id" oid)
      (required ~label:"attribute value" (set_of octet_string))

  (* here:
     key_bag = PKCS8 private key
     pkcs8_shrouded_key_bag = encrypted private key info ==
       sequence2 Algorithm octet_string
     cert_bag =
      sequence2
        cert_id (PKCS9 cert_types <| 1 (X509) or 2 (SDSI))
        expl 0 cert_value (DER-encoded certificate)
     crl_bag = sequence2 crl_id (PKCS9 crl_types <| 1) (expl 0 crl (DER-encoded))

^^^---^^^ those we plan to support

     secret_bag = sequence2 secret_type (expl 0 secret_value)
     safe_contents_bag = (any of the above) safe_contents (recursive!)
  *)
  (* since asn1 does not yet support ANY defined BY, we develop a rather
     complex grammar covering all supported bags *)
  let safe_bag =
    let cert_oid, crl_oid =
      Asn.OID.(PKCS9.cert_types <| 1, PKCS9.crl_types <| 1)
    in
    let f (oid, (a, algo, data), attrs) =
      match a, algo, data with
      | `C1 v, Some a, `C1 data when Asn.OID.equal oid PKCS12.key_bag ->
        let key = Private_key.Asn.reparse_private (v, a, data) in
        `Private_key key, attrs
      | `C2 id, None, `C2 data ->
        if Asn.OID.equal oid PKCS12.cert_bag && Asn.OID.equal id cert_oid then
          match Certificate.decode_der data with
          | Error (`Msg e) -> error (`Parse e)
          | Ok cert -> `Certificate cert, attrs
        else if Asn.OID.equal oid PKCS12.crl_bag && Asn.OID.equal id crl_oid then
          match Crl.decode_der data with
          | Error (`Msg e) -> error (`Parse e)
          | Ok crl -> `Crl crl, attrs
        else
          parse_error "crl bag with non-standard crl"
      | `C3 algo, None, `C1 data when Asn.OID.equal oid PKCS12.pkcs8_shrouded_key_bag ->
        `Encrypted_private_key (algo, data), attrs
      | _ -> parse_error "safe bag OID not supported"
    and g (v, attrs) =
      let oid, d = match v with
        | `Encrypted_private_key (algo, data) ->
          PKCS12.pkcs8_shrouded_key_bag, (`C3 algo, None, `C1 data)
        | `Private_key pk ->
          let v, algo, data = Private_key.Asn.unparse_private pk in
          PKCS12.key_bag, (`C1 v, Some algo, `C1 data)
        | `Certificate cert -> PKCS12.cert_bag, (`C2 cert_oid, None, `C2 (Certificate.encode_der cert))
        | `Crl crl -> PKCS12.crl_bag, (`C2 crl_oid, None, `C2 (Crl.encode_der crl))
      in
      (oid, d, attrs)
    in
    map f g @@
    sequence3
      (required ~label:"bag id" oid)
      (required ~label:"bag value"
         (explicit 0
            (sequence3
               (required ~label:"fst" (choice3 int oid Algorithm.identifier))
               (optional ~label:"algorithm" Algorithm.identifier)
               (required ~label:"data" (choice2 octet_string (explicit 0 octet_string))))))
      (* (explicit 0 (* encrypted private key *)
         (sequence2
            (required ~label:"encryption algorithm" Algorithm.identifier)
            (required ~label:"encrypted data" octet_string))) *)
      (* (explicit 0 (* private key ] *)
            (sequence3
              (required ~label:"version"             int)
              (required ~label:"privateKeyAlgorithm" Algorithm.identifier)
              (required ~label:"privateKey"          octet_string)))  *)
      (* (explicit 0 (* cert / crl *)
            (sequence2
               (required ~label:"oid" oid)
               (required ~label:"data" (explicit 0 octet_string)))) *)
      (optional ~label:"bag attributes" (set_of pkcs12_attribute))

  let safe_contents = sequence_of safe_bag

  let safe_contents_of_cs, safe_contents_to_cs =
    projections_of Asn.der safe_contents
end

let prepare_pw str =
  let l = String.length str in
  let cs = Bytes.make ((succ l) * 2) '\000' in
  for i = 0 to pred l do
    Bytes.set cs (succ (i * 2)) (String.get str i)
  done;
  Bytes.unsafe_to_string cs

let id len purpose =
  let id = match purpose with
    | `Encryption -> 1
    | `Iv -> 2
    | `Hmac -> 3
  in
  String.make len (Char.unsafe_chr id)

let v = function
  | `MD5 | `SHA1 | `SHA224 | `SHA256 -> 512 / 8
  | `SHA384 | `SHA512 -> 1024 / 8

let fill ~data ~out =
  let len = Bytes.length out
  and l = String.length data
  in
  let rec c off =
    if off < len then begin
      Bytes.blit_string data 0 out off (min (len - off) l);
      c (off + l)
    end
  in
  c 0

let fill_or_empty size data =
  let l = String.length data in
  if l = 0 then data
  else
    let len = size * ((l + size - 1) / size) in
    let buf = Bytes.make len '\000' in
    fill ~data ~out:buf;
    Bytes.unsafe_to_string buf

let pbes algorithm purpose password salt iterations n =
  let module Hash = (val (Digestif.module_of_hash' (algorithm :> Digestif.hash'))) in
  let pw = prepare_pw password
  and v = v algorithm
  and u = Hash.digest_size
  in
  let diversifier = id v purpose in
  let salt = fill_or_empty v salt in
  let pass = fill_or_empty v pw in
  let out = Bytes.make n '\000' in
  let rec one off i =
    let ai = ref Hash.(to_raw_string (digest_string (diversifier ^ i))) in
    for _j = 1 to pred iterations do
      ai := Hash.(to_raw_string (digest_string !ai));
    done;
    Bytes.blit_string !ai 0 out off (min (n - off) u);
    if u >= n - off then () else
      (* 6B *)
      let b = Bytes.make v '\000' in
      fill ~data:!ai ~out:b;
      (* 6C *)
      let i' = Bytes.create (String.length i) in
      for j = 0 to pred (String.length i / v) do
        let c = ref 1 in
        for k = pred v downto 0 do
          let idx = j * v + k in
          c := (!c + String.get_uint8 i idx + Bytes.get_uint8 b k) land 0xFFFF;
          Bytes.set_uint8 i' idx (!c land 0xFF);
          c := !c lsr 8;
        done;
      done;
      one (off + u) (Bytes.to_string i')
  in
  let i = salt ^ pass in
  one 0 i;
  Bytes.unsafe_to_string out

let split str off =
  String.sub str 0 off,
  String.sub str off (String.length str - off)

(* TODO PKCS5/7 padding is "k - (l mod k)" i.e. always > 0!
   (and rc4 being a stream cipher has no padding!) *)
let unpad x =
  (* TODO can there be bad padding in this scheme? *)
  let l = String.length x in
  if l > 0 then
    let amount = String.get_uint8 x (pred l) in
    let split_point = if l > amount then l - amount else l in
    let data, pad = split x split_point in
    let good = ref true in
    for i = 0 to pred amount do
      if String.get_uint8 pad i <> amount then good := false
    done;
    if !good then data else x
  else
    x

let pad bs x =
  let l = String.length x in
  let to_pad = bs - (l mod bs) in
  let amount = String.make to_pad (Char.unsafe_chr to_pad) in
  x ^ amount

let ( let* ) = Result.bind

(* there are 3 possibilities to encrypt / decrypt things:
   - PKCS12 KDF (see above), with RC2/RC4/DES
   - PKCS5 v1 (PBES, PBKDF1) -- not (yet?) supported
   - PKCS5 v2 (PBES2, PBKDF2)
*)
let pkcs12_decrypt algo password data =
  let open Algorithm in
  let hash = `SHA1 in
  let* salt, count, key_len, iv_len =
    match algo with
    | SHA_RC4_128 (s, i) -> Ok (s, i, 16, 0)
    | SHA_RC4_40 (s, i) -> Ok (s, i, 5, 0)
    | SHA_3DES_CBC (s, i) -> Ok (s, i, 24, 8)
    | SHA_2DES_CBC (s, i) -> Ok (s, i, 16, 8) (* TODO 2des -> 3des keys (if relevant)*)
    | SHA_RC2_128_CBC (s, i) -> Ok (s, i, 16, 8)
    | SHA_RC2_40_CBC (s, i) -> Ok (s, i, 5, 8)
    | _ -> Error (`Msg "unsupported algorithm")
  in
  let key = pbes hash `Encryption password salt count key_len
  and iv = pbes hash `Iv password salt count iv_len
  in
  let open Mirage_crypto in
  let* data =
    match algo with
    | SHA_RC2_40_CBC _ | SHA_RC2_128_CBC _ ->
      Ok (Rc2.decrypt_cbc ~effective:(key_len * 8) ~key ~iv data)
    | SHA_RC4_40 _ | SHA_RC4_128 _ ->
      let key = ARC4.of_secret key in
      let { ARC4.message ; _ } = ARC4.decrypt ~key data in
      Ok message
    | SHA_3DES_CBC _ ->
      let key = DES.CBC.of_secret key in
      Ok (DES.CBC.decrypt ~key ~iv data)
    | _ -> Error (`Msg "encryption algorithm not supported")
  in
  Ok (unpad data)

let pkcs5_2_decrypt kdf enc password data =
  let* dk_len, iv =
    match enc with
    | Algorithm.AES128_CBC iv -> Ok (16l, iv)
    | Algorithm.AES192_CBC iv -> Ok (24l, iv)
    | Algorithm.AES256_CBC iv -> Ok (32l, iv)
    | _ -> Error (`Msg "unsupported encryption algorithm")
  in
  let* salt, count, prf =
    match kdf with
    | Algorithm.PBKDF2 (salt, iterations, _ (* todo handle keylength *), prf) ->
      let* prf =
        match Algorithm.to_hmac prf with
        | Some prf -> Ok prf
        | None -> Error (`Msg "unsupported PRF")
      in
      Ok (salt, iterations, prf)
    | _ -> Error (`Msg "expected kdf being pbkdf2")
  in
  let key = Pbkdf.pbkdf2 ~prf ~password ~salt ~count ~dk_len in
  let key = Mirage_crypto.AES.CBC.of_secret key in
  let msg = Mirage_crypto.AES.CBC.decrypt ~key ~iv data in
  Ok (unpad msg)

let pkcs5_2_encrypt (mac : [ `SHA1 | `SHA224 | `SHA256 | `SHA384 | `SHA512 ]) count algo password data =
  let module Hash = (val (Digestif.module_of_hash' (mac :> Digestif.hash'))) in
  let bs = Mirage_crypto.AES.CBC.block_size in
  let iv = Mirage_crypto_rng.generate bs in
  let enc, dk_len =
    match algo with
    | `AES128_CBC -> Algorithm.AES128_CBC iv, 16l
    | `AES192_CBC -> Algorithm.AES192_CBC iv, 24l
    | `AES256_CBC -> Algorithm.AES256_CBC iv, 32l
  in
  let salt = Mirage_crypto_rng.generate Hash.digest_size in
  let key = Pbkdf.pbkdf2 ~prf:(mac :> Digestif.hash') ~password ~salt ~count ~dk_len in
  let key = Mirage_crypto.AES.CBC.of_secret key in
  let padded_data = pad bs data in
  let enc_data =
    Mirage_crypto.AES.CBC.encrypt ~key ~iv padded_data
  in
  let kdf = Algorithm.PBKDF2 (salt, count, None, Algorithm.of_hmac mac) in
  Algorithm.PBES2 (kdf, enc), enc_data

let decrypt algo password data =
  let open Algorithm in
  match algo with
  | SHA_RC4_128 _ | SHA_RC4_40 _
  | SHA_3DES_CBC _ | SHA_2DES_CBC _
  | SHA_RC2_128_CBC _ | SHA_RC2_40_CBC _ -> pkcs12_decrypt algo password data
  | PBES2 (kdf, enc) -> pkcs5_2_decrypt kdf enc password data
  | _ -> Error (`Msg "unsupported encryption algorithm")

let password_decrypt password (algo, data) =
  match data with
  | None -> Error (`Msg "no data to decrypt")
  | Some data -> decrypt algo password data

let verify password (data, ((algorithm, digest), salt, iterations)) =
  let* hash =
    Option.to_result
      ~none:(`Msg "unsupported hash algorithm")
      (Algorithm.to_hash algorithm)
  in
  let module Hash = (val (Digestif.module_of_hash' (hash :> Digestif.hash'))) in
  let key =
    pbes hash `Hmac password salt iterations Hash.digest_size
  in
  let computed = Hash.(to_raw_string (hmac_string ~key data)) in
  if String.equal computed digest then begin
    let* content = Asn_grammars.err_to_msg (Asn.auth_safe_of_cs data) in
    let* safe_contents =
      List.fold_left (fun acc c ->
          let* acc = acc in
          match c with
          | `Data data -> Ok (data :: acc)
          | `Encrypted data ->
            let* data = password_decrypt password data in
            Ok (data :: acc))
        (Ok []) content
    in
    List.fold_left (fun acc cs ->
        let* acc = acc in
        let* bags = Asn_grammars.err_to_msg (Asn.safe_contents_of_cs cs) in
        List.fold_left (fun acc bag ->
            let* acc = acc in
            match bag with
            | `Certificate c, _ -> Ok (`Certificate c :: acc)
            | `Crl c, _ -> Ok (`Crl c :: acc)
            | `Private_key p, _ -> Ok (`Private_key p :: acc)
            | `Encrypted_private_key (algo, enc_data), _ ->
              let* data = decrypt algo password enc_data in
              let* p =
                Asn_grammars.err_to_msg (Private_key.Asn.private_of_octets data)
              in
              Ok (`Decrypted_private_key p :: acc))
          (Ok acc) bags)
      (Ok []) safe_contents
  end else
    Error (`Msg "invalid signature")

let create ?(mac = `SHA256) ?(algorithm = `AES256_CBC) ?(iterations = 2048) password certificates private_key =
  let key_fp pub = Public_key.fingerprint pub in
  let priv_fp = key_fp (Private_key.public private_key) in
  let attributes = [ Registry.PKCS9.local_key_id, [ priv_fp ]] in
  let maybe_attr c =
    if String.equal priv_fp (key_fp (Certificate.public_key c)) then
      Some attributes
    else
      None
  in
  let cert_sc =
    Asn.safe_contents_to_cs (List.map (fun c -> `Certificate c, maybe_attr c) certificates)
  and priv_sc =
    let data = Private_key.Asn.private_to_octets private_key in
    let algo, data = pkcs5_2_encrypt mac iterations algorithm password data in
    Asn.safe_contents_to_cs [ `Encrypted_private_key (algo, data), Some attributes ]
  in
  let cert_sc_enc =
    let algo, data = pkcs5_2_encrypt mac iterations algorithm password cert_sc in
    algo, Some data
  in
  let auth_data =
    Asn.auth_safe_to_cs [ `Encrypted cert_sc_enc ; `Data priv_sc ]
  in
  let module Hash = (val (Digestif.module_of_hash' (mac :> Digestif.hash'))) in
  let mac_size = Hash.digest_size in
  let salt = Mirage_crypto_rng.generate mac_size in
  let key = pbes mac `Hmac password salt iterations mac_size in
  let digest = Hash.(to_raw_string (hmac_string ~key auth_data)) in
  auth_data, ((Algorithm.of_hash mac, digest), salt, iterations)

let decode_der cs = Asn_grammars.err_to_msg (Asn.pfx_of_cs cs)

let encode_der = Asn.pfx_to_cs
