diff --git a/src/clutch.erl b/src/clutch.erl index 25db1ca..af49889 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -55,6 +55,7 @@ start(normal, _Args) -> ok = tell(error, "DANGER! This node is in distributed mode!"), init:stop(1) end, + ok = application:ensure_started(sasl), ok = application:ensure_started(hakuzaru), ok = application:ensure_started(zxwidgets), gmc_sup:start_link(). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 60db7be..41e5fb9 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -15,7 +15,7 @@ open_wallet/2, close_wallet/0, new_wallet/3, import_wallet/3, drop_wallet/2, password/2, refresh/0, - nonce/1, spend/2, chain/1, grids/1, + nonce/1, spend/2, chain/1, grids/1, sign_mess/1, make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, add_node/1, set_sole_node/1]). -export([encrypt/2, decrypt/2]). @@ -147,6 +147,13 @@ grids(String) -> gen_server:cast(?MODULE, {grids, String}). +-spec sign_mess(Request) -> ok + when Request :: map(). + +sign_mess(Request) -> + gen_server:cast(?MODULE, {sign_mess, Request}). + + -spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok when Type :: {eddsa, ed25519}, Size :: 256, @@ -335,6 +342,9 @@ handle_cast({chain, ID}, State) -> handle_cast({grids, String}, State) -> ok = do_grids(String), {noreply, State}; +handle_cast({sign_mess, Request}, State) -> + ok = do_sign_mess(Request, State), + {noreply, State}; handle_cast({make_key, Name, Seed, Encoding, Transform}, State) -> NewState = do_make_key(Name, Seed, Encoding, Transform, State), {noreply, NewState}; @@ -521,9 +531,82 @@ do_grids(String) -> Error -> gmc_gui:trouble(Error) end. +do_grids2({{sign, http}, URL}) -> + case httpc:request(URL) of + {ok, {{_, 200, _}, _, JSON}} -> do_grids_sig(JSON, URL); + Error -> gmc_gui:trouble(Error) + end; do_grids2(Instruction) -> tell("GRIDS: ~tp", [Instruction]). +do_grids_sig(JSON, URL) -> + case zj:decode(JSON) of + {ok, GRIDS} -> do_grids_sig2(GRIDS#{"url" => URL}); + Error -> gmc_gui:trouble(Error) + end. + +do_grids_sig2(Request = #{"grids" := 1, "type" := "message"}) -> + gmc_gui:grids_mess_sig(Request); +do_grids_sig2(WTF) -> + gmc_gui:trouble({trash, WTF}). + + +do_sign_mess(Request = #{"public_id" := ID, "payload" := Message}, + #s{wallet = #wallet{keys = Keys}}) -> + case lists:keyfind(ID, #key.id, Keys) of + #key{pair = #{secret := PrivKey}} -> + Sig = base64:encode(sign_message(list_to_binary(Message), PrivKey)), + do_sign_mess2(Request#{"signature" => Sig}); + false -> + gmc_gui:trouble({bad_key, ID}) + end. + +do_sign_mess2(Request = #{"url" := URL}) -> + ResponseKeys = + ["grids", + "chain", + "network_id", + "type", + "public_id", + "payload", + "signature"], + Response = zj:encode(maps:with(ResponseKeys, Request)), + case httpc:request(post, {URL, [], "application/json", Response}, [], []) of + {ok, {{_, 200, _}, _, JSON}} -> log(info, "Signature posted: ~p", [JSON]); + Error -> gmc_gui:trouble(Error) + end. + + +% TODO: Should probably be part of Hakuzaru +sign_message(Message, PrivKey) -> + Prefix = <<"Gajumaru Signed Message:\n">>, + {ok, PSize} = vencode(byte_size(Prefix)), + {ok, MSize} = vencode(byte_size(Message)), + Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]), + {ok, Hashed} = eblake2:blake2b(32, Smashed), + ecu_eddsa:sign_detached(Hashed, PrivKey). + + +vencode(N) when N < 0 -> + {error, {negative_N, N}}; +vencode(N) when N < 16#FD -> + {ok, <>}; +vencode(N) when N =< 16#FFFF -> + NBytes = eu(N, 2), + {ok, <<16#FD, NBytes/binary>>}; +vencode(N) when N =< 16#FFFF_FFFF -> + NBytes = eu(N, 4), + {ok, <<16#FE, NBytes/binary>>}; +vencode(N) when N < (2 bsl 64) -> + NBytes = eu(N, 8), + {ok, <<16#FF, NBytes/binary>>}. + +eu(N, Size) -> + Bytes = binary:encode_unsigned(N, little), + NExtraZeros = Size - byte_size(Bytes), + ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>, + <>. + do_spend(KeyID, TX, State = #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(KeyID, #key.id, Keys) of diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 7b52e09..f64fa9b 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -15,6 +15,7 @@ -behavior(wx_object). -include_lib("wx/include/wx.hrl"). -export([show/1, wallet/1, chain/2, trouble/1, ask_password/0]). +-export([grids_mess_sig/1]). -export([start_link/1]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). @@ -68,6 +69,10 @@ ask_password() -> wx_object:cast(?MODULE, password). +grids_mess_sig(Request) -> + wx_object:cast(?MODULE, {grids_mess_sig, Request}). + + %%% Startup Functions @@ -235,6 +240,9 @@ handle_cast({trouble, Info}, State) -> handle_cast(password, State) -> ok = do_ask_password(State), {noreply, State}; +handle_cast({grids_mess_sig, Request}, State) -> + ok = do_grids_mess_sig(Request, State), + {noreply, State}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. @@ -925,6 +933,58 @@ do_ask_password(#s{frame = Frame, prefs = Prefs, j = J}) -> end, wxDialog:destroy(Dialog). +do_grids_mess_sig(_, #s{accounts = []}) -> + ok; +do_grids_mess_sig(Request = #{"public_id" := false}, + State = #s{picker = Picker, accounts = Accounts}) -> + case wxListBox:getSelection(Picker) of + -1 -> + ok; + Selected -> + #poa{id = ID} = lists:nth(Selected + 1, Accounts), + do_grids_mess_sig2(Request#{"public_id" := ID}, State) + end. + +do_grids_mess_sig2(Request = #{"url" := URL, "public_id" := ID, "payload" := Message}, + #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Message Signature Request")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + Instruction = + J("The server at the URL below is requesting you sign the following message."), + InstTx = wxStaticText:new(Dialog, ?wxID_ANY, Instruction), + AcctSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Account")}]), + AcctTx = wxStaticText:new(Dialog, ?wxID_ANY, ID), + _ = wxStaticBoxSizer:add(AcctSz, AcctTx, zxw:flags(wide)), + URL_Label = J("Originating URL"), + URL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, URL_Label}]), + URL_Tx = wxStaticText:new(Dialog, ?wxID_ANY, URL), + _ = wxStaticBoxSizer:add(URL_Sz, URL_Tx, zxw:flags(wide)), + MessSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Message")}]), + MessTx = wxStaticText:new(Dialog, ?wxID_ANY, Message), + _ = wxStaticBoxSizer:add(MessSz, MessTx, zxw:flags(wide)), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, InstTx, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, AcctSz, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, URL_Sz, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, MessTx, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxFrame:setSize(Dialog, {500, 500}), + ok = wxBoxSizer:layout(Sizer), + ok = wxFrame:center(Dialog), + ok = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> gmc_con:sign_mess(Request); + ?wxID_CANCEL -> ok + end, + wxDialog:destroy(Dialog); +do_grids_mess_sig2(BadRequest, _) -> + tell("Bad request: ~tp", [BadRequest]). + %%% Helpers diff --git a/src/gmc_v.erl b/src/gmc_v.erl index 6736057..f8e3309 100644 --- a/src/gmc_v.erl +++ b/src/gmc_v.erl @@ -32,15 +32,13 @@ safe_size(Prefs) -> Display = wxDisplay:new(), GSize = wxDisplay:getGeometry(Display), ok = wxDisplay:destroy(Display), - Geometry = - case maps:find(geometry, Prefs) of - {ok, none} -> - {X, Y, _, _} = GSize, - {center, {X, Y, 700, 500}}; - {ok, G} -> - {pref, G}; - error -> - {X, Y, _, _} = GSize, - {center, {X, Y, 700, 500}} - end, - Geometry. + case maps:find(geometry, Prefs) of + {ok, none} -> + {X, Y, _, _} = GSize, + {center, {X, Y, 700, 500}}; + {ok, G} -> + {pref, G}; + error -> + {X, Y, _, _} = GSize, + {center, {X, Y, 700, 500}} + end.