ligo/src/lib_client_commands/client_keys_commands.ml
2018-06-30 17:41:32 +02:00

429 lines
19 KiB
OCaml

(*****************************************************************************)
(* *)
(* Open Source License *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* 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. *)
(* *)
(*****************************************************************************)
open Client_keys
let group =
{ Clic.name = "keys" ;
title = "Commands for managing the wallet of cryptographic keys" }
let sig_algo_arg =
Clic.default_arg
~doc:"use custom signature algorithm"
~long:"sig"
~short:'s'
~placeholder:"ed25519|secp256k1|p256"
~default: "ed25519"
(Signature.algo_param ())
let gen_keys_containing
?(encrypted = false) ?(prefix=false) ?(force=false)
~containing ~name (cctxt : #Client_context.io_wallet) =
let unrepresentable =
List.filter (fun s -> not @@ Base58.Alphabet.all_in_alphabet Base58.Alphabet.bitcoin s) containing in
let good_initial_char = "KLMNPQRSTUVWXYZabcdefghi" in
let bad_initial_char = "123456789ABCDEFGHJjkmnopqrstuvwxyz" in
match unrepresentable with
| _ :: _ ->
cctxt#error
"@[<v 0>The following words can't be written in the key alphabet: %a.@,\
Valid characters: %a@,\
Extra restriction for the first character: %s@]"
(Format.pp_print_list
~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
(fun ppf s -> Format.fprintf ppf "'%s'" s))
unrepresentable
Base58.Alphabet.pp Base58.Alphabet.bitcoin
good_initial_char
| [] ->
let unrepresentable =
List.filter (fun s -> prefix &&
String.contains bad_initial_char s.[0]) containing in
match unrepresentable with
| _ :: _ ->
cctxt#error
"@[<v 0>The following words don't respect the first character restriction: %a.@,\
Valid characters: %a@,\
Extra restriction for the first character: %s@]"
(Format.pp_print_list
~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
(fun ppf s -> Format.fprintf ppf "'%s'" s))
unrepresentable
Base58.Alphabet.pp Base58.Alphabet.bitcoin
good_initial_char
| [] ->
Public_key_hash.mem cctxt name >>=? fun name_exists ->
if name_exists && not force
then
cctxt#warning
"Key for name '%s' already exists. Use --force to update." name >>= return
else
begin
cctxt#warning "This process uses a brute force search and \
may take a long time to find a key." >>= fun () ->
let matches =
if prefix then
let containing_tz1 = List.map ((^) "tz1") containing in
(fun key -> List.exists
(fun containing ->
String.sub key 0 (String.length containing) = containing)
containing_tz1)
else
let re = Re.Str.regexp (String.concat "\\|" containing) in
(fun key -> try ignore (Re.Str.search_forward re key 0); true
with Not_found -> false) in
let rec loop attempts =
let public_key_hash, public_key, secret_key =
Signature.generate_key () in
let hash = Signature.Public_key_hash.to_b58check @@
Signature.Public_key.hash public_key in
if matches hash
then
let pk_uri = Tezos_signer_backends.Unencrypted.make_pk public_key in
begin
if encrypted then
Tezos_signer_backends.Encrypted.encrypt cctxt secret_key
else
return (Tezos_signer_backends.Unencrypted.make_sk secret_key)
end >>=? fun sk_uri ->
register_key cctxt ~force
(public_key_hash, pk_uri, sk_uri) name >>=? fun () ->
return hash
else begin if attempts mod 25_000 = 0
then
cctxt#message "Tried %d keys without finding a match" attempts
else Lwt.return_unit end >>= fun () ->
loop (attempts + 1) in
loop 1 >>=? fun key_hash ->
cctxt#message
"Generated '%s' under the name '%s'." key_hash name >>= fun () ->
return_unit
end
let rec input_fundraiser_params (cctxt : #Client_context.io_wallet) =
let rec get_boolean_answer (cctxt : #Client_context.io_wallet) ~default ~msg =
let prompt = if default then "(Y/n/q)" else "(y/N/q)" in
cctxt#prompt "%s %s: " msg prompt >>=? fun gen ->
match default, String.lowercase_ascii gen with
| default, "" -> return default
| _, "y" -> return_true
| _, "n" -> return_false
| _, "q" -> failwith "Exit by user request."
| _ -> get_boolean_answer cctxt ~msg ~default in
cctxt#prompt "Enter the e-mail used for the paper wallet: " >>=? fun email ->
let rec loop_words acc i =
if i > 14 then return (List.rev acc) else
cctxt#prompt_password "Enter word %d: " i >>=? fun word ->
match Bip39.index_of_word (MBytes.to_string word) with
| None -> loop_words acc i
| Some wordidx -> loop_words (wordidx :: acc) (succ i) in
loop_words [] 0 >>=? fun words ->
match Bip39.of_indices words with
| None -> assert false
| Some t ->
cctxt#prompt_password
"Enter the password used for the paper wallet: " >>=? fun password ->
(* TODO: unicode normalization (NFKD)... *)
let sk =
Bip39.to_seed ~passphrase:(email ^ MBytes.to_string password) t in
let sk = Cstruct.(to_bigarray (sub sk 0 32)) in
let sk : Signature.Secret_key.t =
Ed25519
(Data_encoding.Binary.of_bytes_exn Ed25519.Secret_key.encoding sk) in
let pk = Signature.Secret_key.to_public_key sk in
let pkh = Signature.Public_key.hash pk in
let msg = Format.asprintf
"Your public Tezos address is %a is that correct?"
Signature.Public_key_hash.pp pkh in
get_boolean_answer cctxt ~msg ~default:true >>=? function
| true -> return sk
| false -> input_fundraiser_params cctxt
let commands version : Client_context.io_wallet Clic.command list =
let open Clic in
let encrypted_switch () =
if List.exists
(fun (scheme, _) ->
scheme = Tezos_signer_backends.Unencrypted.scheme)
(Client_keys.registered_signers ()) then
Clic.switch
~long:"encrypted"
~doc:("Encrypt the key on-disk") ()
else
Clic.constant true in
let show_private_switch =
switch
~long:"show-secret"
~short:'S'
~doc:"show the private key" () in
[
command ~group
~desc: "List supported signing schemes.\n\
Signing schemes are identifiers for signer modules: the \
built-in signing routines, a hardware wallet, an \
external agent, etc.\n\
Each signer has its own format for describing secret \
keys, such a raw secret key for the default \
`unencrypted` scheme, the path on a hardware security \
module, an alias for an external agent, etc.\n\
This command gives the list of signer modules that this \
version of the tezos client supports."
no_options
(fixed [ "list" ; "signing" ; "schemes" ])
(fun () (cctxt : Client_context.io_wallet) ->
let signers =
List.sort
(fun (ka, _) (kb, _) -> String.compare ka kb)
(registered_signers ()) in
Lwt_list.iter_s
(fun (n, (module S : SIGNER)) ->
cctxt#message "@[<v 2>Scheme `%s`: %s@,@[<hov 0>%a@]@]"
n S.title Format.pp_print_text S.description)
signers >>= return) ;
begin match version with
| Some `Betanet ->
command ~group ~desc: "Generate a pair of keys."
(args2 (Secret_key.force_switch ()) sig_algo_arg)
(prefixes [ "gen" ; "keys" ]
@@ Secret_key.fresh_alias_param
@@ stop)
(fun (force, algo) name (cctxt : Client_context.io_wallet) ->
Secret_key.of_fresh cctxt force name >>=? fun name ->
let (pkh, pk, sk) = Signature.generate_key ~algo () in
let pk_uri = Tezos_signer_backends.Unencrypted.make_pk pk in
Tezos_signer_backends.Encrypted.encrypt cctxt sk >>=? fun sk_uri ->
register_key cctxt ~force (pkh, pk_uri, sk_uri) name)
| _ ->
command ~group ~desc: "Generate a pair of keys."
(args3 (Secret_key.force_switch ()) sig_algo_arg (encrypted_switch ()))
(prefixes [ "gen" ; "keys" ]
@@ Secret_key.fresh_alias_param
@@ stop)
(fun (force, algo, encrypted) name (cctxt : Client_context.io_wallet) ->
Secret_key.of_fresh cctxt force name >>=? fun name ->
let (pkh, pk, sk) = Signature.generate_key ~algo () in
let pk_uri = Tezos_signer_backends.Unencrypted.make_pk pk in
begin
if encrypted then
Tezos_signer_backends.Encrypted.encrypt cctxt sk
else
return (Tezos_signer_backends.Unencrypted.make_sk sk)
end >>=? fun sk_uri ->
register_key cctxt ~force (pkh, pk_uri, sk_uri) name)
end ;
begin match version with
| Some `Betanet ->
command ~group ~desc: "Generate keys including the given string."
(args2
(switch
~long:"prefix"
~short:'P'
~doc:"the key must begin with tz1[word]"
())
(force_switch ()))
(prefixes [ "gen" ; "vanity" ; "keys" ]
@@ Public_key_hash.fresh_alias_param
@@ prefix "matching"
@@ (seq_of_param @@ string ~name:"words" ~desc:"string key must contain one of these words"))
(fun (prefix, force) name containing (cctxt : Client_context.io_wallet) ->
Public_key_hash.of_fresh cctxt force name >>=? fun name ->
gen_keys_containing ~encrypted:true ~force ~prefix ~containing ~name cctxt)
| _ ->
command ~group ~desc: "Generate keys including the given string."
(args3
(switch
~long:"prefix"
~short:'P'
~doc:"the key must begin with tz1[word]"
())
(force_switch ())
(encrypted_switch ()))
(prefixes [ "gen" ; "vanity" ; "keys" ]
@@ Public_key_hash.fresh_alias_param
@@ prefix "matching"
@@ (seq_of_param @@ string ~name:"words" ~desc:"string key must contain one of these words"))
(fun (prefix, force, encrypted) name containing (cctxt : Client_context.io_wallet) ->
Public_key_hash.of_fresh cctxt force name >>=? fun name ->
gen_keys_containing ~encrypted ~force ~prefix ~containing ~name cctxt)
end ;
command ~group ~desc: "Add a secret key to the wallet."
(args1 (Secret_key.force_switch ()))
(prefix "import"
@@ prefixes [ "secret" ; "key" ]
@@ Secret_key.fresh_alias_param
@@ Client_keys.sk_uri_param
@@ stop)
(fun force name sk_uri (cctxt : Client_context.io_wallet) ->
Secret_key.of_fresh cctxt force name >>=? fun name ->
Client_keys.neuterize sk_uri >>=? fun pk_uri ->
begin
Public_key.find_opt cctxt name >>=? function
| None -> return_unit
| Some (pk_uri_found, _) ->
fail_unless (pk_uri = pk_uri_found || force)
(failure
"public and secret keys '%s' don't correspond, \
please don't use --force" name)
end >>=? fun () ->
Client_keys.public_key_hash pk_uri >>=? fun (pkh, public_key) ->
cctxt#message
"Tezos address added: %a"
Signature.Public_key_hash.pp pkh >>= fun () ->
register_key cctxt ~force (pkh, pk_uri, sk_uri) ?public_key name) ;
] @
(if version <> (Some `Betanet) then [] else [
command ~group ~desc: "Add a fundraiser secret key to the wallet."
(args1 (Secret_key.force_switch ()))
(prefix "import"
@@ prefixes [ "fundraiser" ; "secret" ; "key" ]
@@ Secret_key.fresh_alias_param
@@ stop)
(fun force name (cctxt : Client_context.io_wallet) ->
Secret_key.of_fresh cctxt force name >>=? fun name ->
input_fundraiser_params cctxt >>=? fun sk ->
Tezos_signer_backends.Encrypted.encrypt cctxt sk >>=? fun sk_uri ->
Client_keys.neuterize sk_uri >>=? fun pk_uri ->
begin
Public_key.find_opt cctxt name >>=? function
| None -> return_unit
| Some (pk_uri_found, _) ->
fail_unless (pk_uri = pk_uri_found || force)
(failure
"public and secret keys '%s' don't correspond, \
please don't use --force" name)
end >>=? fun () ->
Client_keys.public_key_hash pk_uri >>=? fun (pkh, _public_key) ->
register_key cctxt ~force (pkh, pk_uri, sk_uri) name) ;
]) @
[
command ~group ~desc: "Add a public key to the wallet."
(args1 (Public_key.force_switch ()))
(prefix "import"
@@ prefixes [ "public" ; "key" ]
@@ Public_key.fresh_alias_param
@@ Client_keys.pk_uri_param
@@ stop)
(fun force name pk_uri (cctxt : Client_context.io_wallet) ->
Public_key.of_fresh cctxt force name >>=? fun name ->
Client_keys.public_key_hash pk_uri >>=? fun (pkh, public_key) ->
Public_key_hash.add ~force cctxt name pkh >>=? fun () ->
cctxt#message
"Tezos address added: %a"
Signature.Public_key_hash.pp pkh >>= fun () ->
Public_key.add ~force cctxt name (pk_uri, public_key)) ;
command ~group ~desc: "Add an address to the wallet."
(args1 (Public_key.force_switch ()))
(prefixes [ "add" ; "address" ]
@@ Public_key_hash.fresh_alias_param
@@ Public_key_hash.source_param
@@ stop)
(fun force name hash cctxt ->
Public_key_hash.of_fresh cctxt force name >>=? fun name ->
Public_key_hash.add ~force cctxt name hash) ;
command ~group ~desc: "List all addresses and associated keys."
no_options
(fixed [ "list" ; "known" ; "addresses" ])
(fun () (cctxt : #Client_context.io_wallet) ->
list_keys cctxt >>=? fun l ->
iter_s begin fun (name, pkh, pk, sk) ->
Public_key_hash.to_source pkh >>=? fun v ->
begin match pk, sk with
| None, None ->
cctxt#message "%s: %s" name v
| _, Some uri ->
let scheme =
Option.unopt ~default:"unencrypted" @@
Uri.scheme (uri : sk_uri :> Uri.t) in
cctxt#message "%s: %s (%s sk known)" name v scheme
| Some _, _ ->
cctxt#message "%s: %s (pk known)" name v
end >>= fun () -> return_unit
end l) ;
command ~group ~desc: "Show the keys associated with an implicit account."
(args1 show_private_switch)
(prefixes [ "show" ; "address"]
@@ Public_key_hash.alias_param
@@ stop)
(fun show_private (name, _) (cctxt : #Client_context.io_wallet) ->
alias_keys cctxt name >>=? fun key_info ->
match key_info with
| None ->
cctxt#message "No keys found for address" >>= fun () ->
return_unit
| Some (pkh, pk, skloc) ->
cctxt#message "Hash: %a"
Signature.Public_key_hash.pp pkh >>= fun () ->
match pk with
| None -> return_unit
| Some pk ->
cctxt#message "Public Key: %a"
Signature.Public_key.pp pk >>= fun () ->
if show_private then
match skloc with
| None -> return_unit
| Some skloc ->
Secret_key.to_source skloc >>=? fun skloc ->
cctxt#message "Secret Key: %s" skloc >>= fun () ->
return_unit
else
return_unit) ;
command ~group ~desc: "Forget one address."
(args1 (Clic.switch
~long:"force" ~short:'f'
~doc:"delete associated keys when present" ()))
(prefixes [ "forget" ; "address"]
@@ Public_key_hash.alias_param
@@ stop)
(fun force (name, _pkh) (cctxt : Client_context.io_wallet) ->
Secret_key.mem cctxt name >>=? fun has_secret_key ->
Public_key.mem cctxt name >>=? fun has_public_key ->
fail_when (not force && (has_secret_key || has_public_key))
(failure "secret or public key present for %s, \
use --force to delete" name) >>=? fun () ->
Secret_key.del cctxt name >>=? fun () ->
Public_key.del cctxt name >>=? fun () ->
Public_key_hash.del cctxt name) ;
command ~group ~desc: "Forget the entire wallet of keys."
(args1 (Clic.switch
~long:"force" ~short:'f'
~doc:"you got to use the force for that" ()))
(fixed [ "forget" ; "all" ; "keys" ])
(fun force (cctxt : Client_context.io_wallet) ->
fail_unless force
(failure "this can only be used with option --force") >>=? fun () ->
Public_key.set cctxt [] >>=? fun () ->
Secret_key.set cctxt [] >>=? fun () ->
Public_key_hash.set cctxt []) ;
]