(*****************************************************************************) (* *) (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2018 Nomadic Labs, *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) (* to deal in the Software without restriction, including without limitation *) (* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) (* and/or sell copies of the Software, and to permit persons to whom the *) (* Software is furnished to do so, subject to the following conditions: *) (* *) (* The above copyright notice and this permission notice shall be included *) (* in all copies or substantial portions of the Software. *) (* *) (* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) (* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) (* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) (* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) (* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) (* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) (* DEALINGS IN THE SOFTWARE. *) (* *) (*****************************************************************************) type Base58.data += Encrypted_ed25519 of MBytes.t type Base58.data += Encrypted_secp256k1 of MBytes.t type Base58.data += Encrypted_p256 of MBytes.t open Client_keys let scheme = "encrypted" module Raw = struct (* https://tools.ietf.org/html/rfc2898#section-4.1 *) let salt_len = 8 (* Fixed zero nonce *) let nonce = Crypto_box.zero_nonce (* Secret keys for Ed25519, secp256k1, P256 are 32 bytes long. *) let encrypted_size = Crypto_box.boxzerobytes + 32 let pbkdf ~salt ~password = Pbkdf.SHA512.pbkdf2 ~count:32768 ~dk_len:32l ~salt ~password let encrypt ~password sk = let salt = Rand.generate salt_len in let key = Crypto_box.Secretbox.unsafe_of_bytes (pbkdf ~salt ~password) in let msg = match (sk : Signature.secret_key) with | Ed25519 sk -> Data_encoding.Binary.to_bytes_exn Ed25519.Secret_key.encoding sk | Secp256k1 sk -> Data_encoding.Binary.to_bytes_exn Secp256k1.Secret_key.encoding sk | P256 sk -> Data_encoding.Binary.to_bytes_exn P256.Secret_key.encoding sk in MBytes.concat "" [ salt ; Crypto_box.Secretbox.box key msg nonce ] let decrypt algo ~password ~encrypted_sk = let salt = MBytes.sub encrypted_sk 0 salt_len in let encrypted_sk = MBytes.sub encrypted_sk salt_len encrypted_size in let key = Crypto_box.Secretbox.unsafe_of_bytes (pbkdf ~salt ~password) in match Crypto_box.Secretbox.box_open key encrypted_sk nonce, algo with | None, _ -> return_none | Some bytes, Signature.Ed25519 -> begin match Data_encoding.Binary.of_bytes Ed25519.Secret_key.encoding bytes with | Some sk -> return_some (Ed25519 sk : Signature.Secret_key.t) | None -> failwith "Corrupted wallet, deciphered key is not a \ valid Ed25519 secret key" end | Some bytes, Signature.Secp256k1 -> begin match Data_encoding.Binary.of_bytes Secp256k1.Secret_key.encoding bytes with | Some sk -> return_some (Secp256k1 sk : Signature.Secret_key.t) | None -> failwith "Corrupted wallet, deciphered key is not a \ valid Secp256k1 secret key" end | Some bytes, Signature.P256 -> begin match Data_encoding.Binary.of_bytes P256.Secret_key.encoding bytes with | Some sk -> return_some (P256 sk : Signature.Secret_key.t) | None -> failwith "Corrupted wallet, deciphered key is not a \ valid P256 secret key" end end module Encodings = struct let ed25519 = let length = Hacl.Sign.skbytes + Crypto_box.boxzerobytes + Raw.salt_len in Base58.register_encoding ~prefix: Base58.Prefix.ed25519_encrypted_seed ~length ~to_raw: (fun sk -> MBytes.to_string sk) ~of_raw: (fun buf -> if String.length buf <> length then None else Some (MBytes.of_string buf)) ~wrap: (fun sk -> Encrypted_ed25519 sk) let secp256k1 = let open Libsecp256k1.External in let length = Key.secret_bytes + Crypto_box.boxzerobytes + Raw.salt_len in Base58.register_encoding ~prefix: Base58.Prefix.secp256k1_encrypted_secret_key ~length ~to_raw: (fun sk -> MBytes.to_string sk) ~of_raw: (fun buf -> if String.length buf <> length then None else Some (MBytes.of_string buf)) ~wrap: (fun sk -> Encrypted_secp256k1 sk) let p256 = let length = Uecc.(sk_size secp256r1) + Crypto_box.boxzerobytes + Raw.salt_len in Base58.register_encoding ~prefix: Base58.Prefix.p256_encrypted_secret_key ~length ~to_raw: (fun sk -> MBytes.to_string sk) ~of_raw: (fun buf -> if String.length buf <> length then None else Some (MBytes.of_string buf)) ~wrap: (fun sk -> Encrypted_p256 sk) let () = Base58.check_encoded_prefix ed25519 "edesk" 88 ; Base58.check_encoded_prefix secp256k1 "spesk" 88 ; Base58.check_encoded_prefix p256 "p2esk" 88 end let decrypted = Hashtbl.create 13 (* we cache the password in this list to avoid asking the user all the time *) let passwords = ref [] let rec interactive_decrypt_loop (cctxt : #Client_context.prompter) ?name ~encrypted_sk algo = begin match name with | None -> cctxt#prompt_password "Enter password for encrypted key: " | Some name -> cctxt#prompt_password "Enter password for encrypted key \"%s\": " name end >>=? fun password -> Raw.decrypt algo ~password ~encrypted_sk >>=? function | Some sk -> passwords := password :: !passwords ; return sk | None -> interactive_decrypt_loop cctxt ?name ~encrypted_sk algo (* add all passwords in [filename] to the list of known passwords *) let password_file_load = function |Some filename -> if Sys.file_exists filename then begin let stream = Lwt_io.lines_of_file filename in Lwt_stream.iter (fun p -> passwords := MBytes.of_string p :: !passwords) stream >>= fun () -> return_unit end else return_unit | None -> return_unit let rec noninteractive_decrypt_loop algo ~encrypted_sk = function | [] -> return_none | password :: passwords -> Raw.decrypt algo ~password ~encrypted_sk >>=? function | None -> noninteractive_decrypt_loop algo ~encrypted_sk passwords | Some sk -> return_some sk let decrypt_payload cctxt ?name encrypted_sk = begin match Base58.decode encrypted_sk with | Some (Encrypted_ed25519 encrypted_sk) -> return (Signature.Ed25519, encrypted_sk) | Some (Encrypted_secp256k1 encrypted_sk) -> return (Signature.Secp256k1, encrypted_sk) | Some (Encrypted_p256 encrypted_sk) -> return (Signature.P256, encrypted_sk) | _ -> failwith "Not a Base58Check-encoded encrypted key" end >>=? fun (algo, encrypted_sk) -> noninteractive_decrypt_loop algo ~encrypted_sk !passwords >>=? function | Some sk -> return sk | None -> interactive_decrypt_loop cctxt ?name ~encrypted_sk algo let decrypt (cctxt : #Client_context.prompter) ?name sk_uri = let payload = Uri.path (sk_uri : sk_uri :> Uri.t) in decrypt_payload cctxt ?name payload >>=? fun sk -> Hashtbl.replace decrypted sk_uri sk ; return sk let decrypt_all (cctxt : #Client_context.io_wallet) = Secret_key.load cctxt >>=? fun sks -> password_file_load cctxt#password_filename >>=? fun () -> iter_s begin fun (name, sk_uri) -> if Uri.scheme (sk_uri : sk_uri :> Uri.t) <> Some scheme then return_unit else decrypt cctxt ~name sk_uri >>=? fun _ -> return_unit end sks let decrypt_list (cctxt : #Client_context.io_wallet) keys = Secret_key.load cctxt >>=? fun sks -> password_file_load cctxt#password_filename >>=? fun () -> iter_s begin fun (name, sk_uri) -> if Uri.scheme (sk_uri : sk_uri :> Uri.t) = Some scheme && (keys = [] || List.mem name keys) then decrypt cctxt ~name sk_uri >>=? fun _ -> return_unit else return_unit end sks let rec read_password (cctxt : #Client_context.io) = cctxt#prompt_password "Enter password to encrypt your key: " >>=? fun password -> cctxt#prompt_password "Confirm password: " >>=? fun confirm -> if not (MBytes.equal password confirm) then cctxt#message "Passwords do not match." >>= fun () -> read_password cctxt else return password let encrypt cctxt sk = read_password cctxt >>=? fun password -> let payload = Raw.encrypt ~password sk in let encoding = match sk with | Ed25519 _ -> Encodings.ed25519 | Secp256k1 _ -> Encodings.secp256k1 | P256 _ -> Encodings.p256 in let path = Base58.simple_encode encoding payload in let sk_uri = Client_keys.make_sk_uri (Uri.make ~scheme ~path ()) in Hashtbl.replace decrypted sk_uri sk ; return sk_uri module Make(C : sig val cctxt: Client_context.prompter end) = struct let scheme = "encrypted" let title = "Built-in signer using encrypted keys." let description = "Valid secret key URIs are of the form\n\ \ - encrypted:\n\ where is the encrypted (password protected \ using Nacl's cryptobox and pbkdf) secret key, formatted in \ unprefixed Base58.\n\ Valid public key URIs are of the form\n\ \ - encrypted:\n\ where is the public key in Base58." let public_key = Unencrypted.public_key let public_key_hash = Unencrypted.public_key_hash let neuterize sk_uri = decrypt C.cctxt sk_uri >>=? fun sk -> return (Unencrypted.make_pk (Signature.Secret_key.to_public_key sk)) let sign ?watermark sk_uri buf = decrypt C.cctxt sk_uri >>=? fun sk -> return (Signature.sign ?watermark sk buf) let deterministic_nonce sk_uri buf = decrypt C.cctxt sk_uri >>=? fun sk -> return (Signature.deterministic_nonce sk buf) let deterministic_nonce_hash sk_uri buf = decrypt C.cctxt sk_uri >>=? fun sk -> return (Signature.deterministic_nonce_hash sk buf) let supports_deterministic_nonces _ = return_true end