pythonsigner.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import sys
  2. import subprocess
  3. from tinyrpc.transports import ServerTransport
  4. from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
  5. from tinyrpc.dispatch import public, RPCDispatcher
  6. from tinyrpc.server import RPCServer
  7. """
  8. This is a POC example of how to write a custom UI for Clef.
  9. The UI starts the clef process with the '--stdio-ui' option
  10. and communicates with clef using standard input / output.
  11. The standard input/output is a relatively secure way to communicate,
  12. as it does not require opening any ports or IPC files. Needless to say,
  13. it does not protect against memory inspection mechanisms
  14. where an attacker can access process memory.
  15. To make this work install all the requirements:
  16. pip install -r requirements.txt
  17. """
  18. try:
  19. import urllib.parse as urlparse
  20. except ImportError:
  21. import urllib as urlparse
  22. class StdIOTransport(ServerTransport):
  23. """Uses std input/output for RPC"""
  24. def receive_message(self):
  25. return None, urlparse.unquote(sys.stdin.readline())
  26. def send_reply(self, context, reply):
  27. print(reply)
  28. class PipeTransport(ServerTransport):
  29. """Uses std a pipe for RPC"""
  30. def __init__(self, input, output):
  31. self.input = input
  32. self.output = output
  33. def receive_message(self):
  34. data = self.input.readline()
  35. print(">> {}".format(data))
  36. return None, urlparse.unquote(data)
  37. def send_reply(self, context, reply):
  38. reply = str(reply, "utf-8")
  39. print("<< {}".format(reply))
  40. self.output.write("{}\n".format(reply))
  41. def sanitize(txt, limit=100):
  42. return txt[:limit].encode("unicode_escape").decode("utf-8")
  43. def metaString(meta):
  44. """
  45. "meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}
  46. """ # noqa: E501
  47. message = (
  48. "\tRequest context:\n"
  49. "\t\t{remote} -> {scheme} -> {local}\n"
  50. "\tAdditional HTTP header data, provided by the external caller:\n"
  51. "\t\tUser-Agent: {user_agent}\n"
  52. "\t\tOrigin: {origin}\n"
  53. )
  54. return message.format(
  55. remote=meta.get("remote", "<missing>"),
  56. scheme=meta.get("scheme", "<missing>"),
  57. local=meta.get("local", "<missing>"),
  58. user_agent=sanitize(meta.get("User-Agent"), 200),
  59. origin=sanitize(meta.get("Origin"), 100),
  60. )
  61. class StdIOHandler:
  62. def __init__(self):
  63. pass
  64. @public
  65. def approveTx(self, req):
  66. """
  67. Example request:
  68. {"jsonrpc":"2.0","id":20,"method":"ui_approveTx","params":[{"transaction":{"from":"0xDEADbEeF000000000000000000000000DeaDbeEf","to":"0xDEADbEeF000000000000000000000000DeaDbeEf","gas":"0x3e8","gasPrice":"0x5","maxFeePerGas":null,"maxPriorityFeePerGas":null,"value":"0x6","nonce":"0x1","data":"0x"},"call_info":null,"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]}
  69. :param transaction: transaction info
  70. :param call_info: info abou the call, e.g. if ABI info could not be
  71. :param meta: metadata about the request, e.g. where the call comes from
  72. :return:
  73. """ # noqa: E501
  74. message = (
  75. "Sign transaction request:\n"
  76. "\t{meta_string}\n"
  77. "\n"
  78. "\tFrom: {from_}\n"
  79. "\tTo: {to}\n"
  80. "\n"
  81. "\tAuto-rejecting request"
  82. )
  83. meta = req.get("meta", {})
  84. transaction = req.get("transaction")
  85. sys.stdout.write(
  86. message.format(
  87. meta_string=metaString(meta),
  88. from_=transaction.get("from", "<missing>"),
  89. to=transaction.get("to", "<missing>"),
  90. )
  91. )
  92. return {
  93. "approved": False,
  94. }
  95. @public
  96. def approveSignData(self, req):
  97. """
  98. Example request:
  99. {"jsonrpc":"2.0","id":8,"method":"ui_approveSignData","params":[{"content_type":"application/x-clique-header","address":"0x0011223344556677889900112233445566778899","raw_data":"+QIRoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIFOYIFOYIFOoIFOoIFOppFeHRyYSBkYXRhIEV4dHJhIGRhdGEgRXh0cqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAA==","messages":[{"name":"Clique header","value":"clique header 1337 [0x44381ab449d77774874aca34634cb53bc21bd22aef2d3d4cf40e51176cb585ec]","type":"clique"}],"call_info":null,"hash":"0xa47ab61438a12a06c81420e308c2b7aae44e9cd837a5df70dd021421c0f58643","meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]}
  100. """ # noqa: E501
  101. message = (
  102. "Sign data request:\n"
  103. "\t{meta_string}\n"
  104. "\n"
  105. "\tContent-type: {content_type}\n"
  106. "\tAddress: {address}\n"
  107. "\tHash: {hash_}\n"
  108. "\n"
  109. "\tAuto-rejecting request\n"
  110. )
  111. meta = req.get("meta", {})
  112. sys.stdout.write(
  113. message.format(
  114. meta_string=metaString(meta),
  115. content_type=req.get("content_type"),
  116. address=req.get("address"),
  117. hash_=req.get("hash"),
  118. )
  119. )
  120. return {
  121. "approved": False,
  122. "password": None,
  123. }
  124. @public
  125. def approveNewAccount(self, req):
  126. """
  127. Example request:
  128. {"jsonrpc":"2.0","id":25,"method":"ui_approveNewAccount","params":[{"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]}
  129. """ # noqa: E501
  130. message = (
  131. "Create new account request:\n"
  132. "\t{meta_string}\n"
  133. "\n"
  134. "\tAuto-rejecting request\n"
  135. )
  136. meta = req.get("meta", {})
  137. sys.stdout.write(message.format(meta_string=metaString(meta)))
  138. return {
  139. "approved": False,
  140. }
  141. @public
  142. def showError(self, req):
  143. """
  144. Example request:
  145. {"jsonrpc":"2.0","method":"ui_showError","params":[{"text":"If you see this message, enter 'yes' to the next question"}]}
  146. :param message: to display
  147. :return:nothing
  148. """ # noqa: E501
  149. message = (
  150. "## Error\n{text}\n"
  151. "Press enter to continue\n"
  152. )
  153. text = req.get("text")
  154. sys.stdout.write(message.format(text=text))
  155. input()
  156. return
  157. @public
  158. def showInfo(self, req):
  159. """
  160. Example request:
  161. {"jsonrpc":"2.0","method":"ui_showInfo","params":[{"text":"If you see this message, enter 'yes' to next question"}]}
  162. :param message: to display
  163. :return:nothing
  164. """ # noqa: E501
  165. message = (
  166. "## Info\n{text}\n"
  167. "Press enter to continue\n"
  168. )
  169. text = req.get("text")
  170. sys.stdout.write(message.format(text=text))
  171. input()
  172. return
  173. @public
  174. def onSignerStartup(self, req):
  175. """
  176. Example request:
  177. {"jsonrpc":"2.0", "method":"ui_onSignerStartup", "params":[{"info":{"extapi_http":"n/a","extapi_ipc":"/home/user/.clef/clef.ipc","extapi_version":"6.1.0","intapi_version":"7.0.1"}}]}
  178. """ # noqa: E501
  179. message = (
  180. "\n"
  181. "\t\tExt api url: {extapi_http}\n"
  182. "\t\tInt api ipc: {extapi_ipc}\n"
  183. "\t\tExt api ver: {extapi_version}\n"
  184. "\t\tInt api ver: {intapi_version}\n"
  185. )
  186. info = req.get("info")
  187. sys.stdout.write(
  188. message.format(
  189. extapi_http=info.get("extapi_http"),
  190. extapi_ipc=info.get("extapi_ipc"),
  191. extapi_version=info.get("extapi_version"),
  192. intapi_version=info.get("intapi_version"),
  193. )
  194. )
  195. @public
  196. def approveListing(self, req):
  197. """
  198. Example request:
  199. {"jsonrpc":"2.0","id":23,"method":"ui_approveListing","params":[{"accounts":[{"address":...
  200. """ # noqa: E501
  201. message = (
  202. "\n"
  203. "## Account listing request\n"
  204. "\t{meta_string}\n"
  205. "\tDo you want to allow listing the following accounts?\n"
  206. "\t-{addrs}\n"
  207. "\n"
  208. "->Auto-answering No\n"
  209. )
  210. meta = req.get("meta", {})
  211. accounts = req.get("accounts", [])
  212. addrs = [x.get("address") for x in accounts]
  213. sys.stdout.write(
  214. message.format(
  215. addrs="\n\t-".join(addrs),
  216. meta_string=metaString(meta)
  217. )
  218. )
  219. return {}
  220. @public
  221. def onInputRequired(self, req):
  222. """
  223. Example request:
  224. {"jsonrpc":"2.0","id":1,"method":"ui_onInputRequired","params":[{"title":"Master Password","prompt":"Please enter the password to decrypt the master seed","isPassword":true}]}
  225. :param message: to display
  226. :return:nothing
  227. """ # noqa: E501
  228. message = (
  229. "\n"
  230. "## {title}\n"
  231. "\t{prompt}\n"
  232. "\n"
  233. "> "
  234. )
  235. sys.stdout.write(
  236. message.format(
  237. title=req.get("title"),
  238. prompt=req.get("prompt")
  239. )
  240. )
  241. isPassword = req.get("isPassword")
  242. if not isPassword:
  243. return {"text": input()}
  244. return ""
  245. def main(args):
  246. cmd = ["clef", "--stdio-ui"]
  247. if len(args) > 0 and args[0] == "test":
  248. cmd.extend(["--stdio-ui-test"])
  249. print("cmd: {}".format(" ".join(cmd)))
  250. dispatcher = RPCDispatcher()
  251. dispatcher.register_instance(StdIOHandler(), "ui_")
  252. # line buffered
  253. p = subprocess.Popen(
  254. cmd,
  255. bufsize=1,
  256. universal_newlines=True,
  257. stdin=subprocess.PIPE,
  258. stdout=subprocess.PIPE,
  259. )
  260. rpc_server = RPCServer(
  261. PipeTransport(p.stdout, p.stdin), JSONRPCProtocol(), dispatcher
  262. )
  263. rpc_server.serve_forever()
  264. if __name__ == "__main__":
  265. main(sys.argv[1:])