pythonsigner.py 10.3 KB
Newer Older
1 2 3
import sys
import subprocess

4 5
from tinyrpc.transports import ServerTransport
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
6
from tinyrpc.dispatch import public, RPCDispatcher
7 8
from tinyrpc.server import RPCServer

9 10 11 12 13 14 15 16 17
"""
This is a POC example of how to write a custom UI for Clef.
The UI starts the clef process with the '--stdio-ui' option
and communicates with clef using standard input / output.

The standard input/output is a relatively secure way to communicate,
as it does not require opening any ports or IPC files. Needless to say,
it does not protect against memory inspection mechanisms
where an attacker can access process memory.
18

19 20 21 22
To make this work install all the requirements:

  pip install -r requirements.txt
"""
23 24 25 26 27 28

try:
    import urllib.parse as urlparse
except ImportError:
    import urllib as urlparse

29

30
class StdIOTransport(ServerTransport):
31 32
    """Uses std input/output for RPC"""

33 34 35 36 37 38
    def receive_message(self):
        return None, urlparse.unquote(sys.stdin.readline())

    def send_reply(self, context, reply):
        print(reply)

39

40
class PipeTransport(ServerTransport):
41
    """Uses std a pipe for RPC"""
42

43
    def __init__(self, input, output):
44 45 46 47 48
        self.input = input
        self.output = output

    def receive_message(self):
        data = self.input.readline()
49
        print(">> {}".format(data))
50 51 52
        return None, urlparse.unquote(data)

    def send_reply(self, context, reply):
53 54 55 56
        reply = str(reply, "utf-8")
        print("<< {}".format(reply))
        self.output.write("{}\n".format(reply))

57

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
def sanitize(txt, limit=100):
    return txt[:limit].encode("unicode_escape").decode("utf-8")


def metaString(meta):
    """
    "meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}
    """  # noqa: E501
    message = (
        "\tRequest context:\n"
        "\t\t{remote} -> {scheme} -> {local}\n"
        "\tAdditional HTTP header data, provided by the external caller:\n"
        "\t\tUser-Agent: {user_agent}\n"
        "\t\tOrigin: {origin}\n"
    )
    return message.format(
        remote=meta.get("remote", "<missing>"),
        scheme=meta.get("scheme", "<missing>"),
        local=meta.get("local", "<missing>"),
        user_agent=sanitize(meta.get("User-Agent"), 200),
        origin=sanitize(meta.get("Origin"), 100),
    )


class StdIOHandler:
83 84 85 86
    def __init__(self):
        pass

    @public
87
    def approveTx(self, req):
88 89
        """
        Example request:
90 91

        {"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":""}}]}
92 93 94 95

        :param transaction: transaction info
        :param call_info: info abou the call, e.g. if ABI info could not be
        :param meta: metadata about the request, e.g. where the call comes from
96
        :return:
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        """  # noqa: E501
        message = (
            "Sign transaction request:\n"
            "\t{meta_string}\n"
            "\n"
            "\tFrom: {from_}\n"
            "\tTo: {to}\n"
            "\n"
            "\tAuto-rejecting request"
        )
        meta = req.get("meta", {})
        transaction = req.get("transaction")
        sys.stdout.write(
            message.format(
                meta_string=metaString(meta),
                from_=transaction.get("from", "<missing>"),
                to=transaction.get("to", "<missing>"),
            )
        )
116
        return {
117
            "approved": False,
118 119 120
        }

    @public
121
    def approveSignData(self, req):
122
        """
123
        Example request:
124

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
        {"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":""}}]}
        """  # noqa: E501
        message = (
            "Sign data request:\n"
            "\t{meta_string}\n"
            "\n"
            "\tContent-type: {content_type}\n"
            "\tAddress: {address}\n"
            "\tHash: {hash_}\n"
            "\n"
            "\tAuto-rejecting request\n"
        )
        meta = req.get("meta", {})
        sys.stdout.write(
            message.format(
                meta_string=metaString(meta),
                content_type=req.get("content_type"),
                address=req.get("address"),
                hash_=req.get("hash"),
            )
        )
146

147 148 149 150
        return {
            "approved": False,
            "password": None,
        }
151 152

    @public
153
    def approveNewAccount(self, req):
154
        """
155
        Example request:
156

157 158 159 160 161 162 163 164 165 166 167 168 169
        {"jsonrpc":"2.0","id":25,"method":"ui_approveNewAccount","params":[{"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]}
        """  # noqa: E501
        message = (
            "Create new account request:\n"
            "\t{meta_string}\n"
            "\n"
            "\tAuto-rejecting request\n"
        )
        meta = req.get("meta", {})
        sys.stdout.write(message.format(meta_string=metaString(meta)))
        return {
            "approved": False,
        }
170

171 172
    @public
    def showError(self, req):
173
        """
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        Example request:

        {"jsonrpc":"2.0","method":"ui_showError","params":[{"text":"If you see this message, enter 'yes' to the next question"}]}

        :param message: to display
        :return:nothing
        """  # noqa: E501
        message = (
            "## Error\n{text}\n"
            "Press enter to continue\n"
        )
        text = req.get("text")
        sys.stdout.write(message.format(text=text))
        input()
        return
189 190

    @public
191
    def showInfo(self, req):
192
        """
193
        Example request:
194

195 196 197 198 199 200 201 202 203 204 205 206 207
        {"jsonrpc":"2.0","method":"ui_showInfo","params":[{"text":"If you see this message, enter 'yes' to next question"}]}

        :param message: to display
        :return:nothing
        """  # noqa: E501
        message = (
            "## Info\n{text}\n"
            "Press enter to continue\n"
        )
        text = req.get("text")
        sys.stdout.write(message.format(text=text))
        input()
        return
208 209

    @public
210
    def onSignerStartup(self, req):
211 212 213
        """
        Example request:

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
        {"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"}}]}
        """  # noqa: E501
        message = (
            "\n"
            "\t\tExt api url: {extapi_http}\n"
            "\t\tInt api ipc: {extapi_ipc}\n"
            "\t\tExt api ver: {extapi_version}\n"
            "\t\tInt api ver: {intapi_version}\n"
        )
        info = req.get("info")
        sys.stdout.write(
            message.format(
                extapi_http=info.get("extapi_http"),
                extapi_ipc=info.get("extapi_ipc"),
                extapi_version=info.get("extapi_version"),
                intapi_version=info.get("intapi_version"),
            )
        )
232

233 234
    @public
    def approveListing(self, req):
235
        """
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
        Example request:

        {"jsonrpc":"2.0","id":23,"method":"ui_approveListing","params":[{"accounts":[{"address":...
        """  # noqa: E501
        message = (
            "\n"
            "## Account listing request\n"
            "\t{meta_string}\n"
            "\tDo you want to allow listing the following accounts?\n"
            "\t-{addrs}\n"
            "\n"
            "->Auto-answering No\n"
        )
        meta = req.get("meta", {})
        accounts = req.get("accounts", [])
        addrs = [x.get("address") for x in accounts]
        sys.stdout.write(
            message.format(
                addrs="\n\t-".join(addrs),
                meta_string=metaString(meta)
            )
        )
        return {}
259 260

    @public
261
    def onInputRequired(self, req):
262
        """
263 264 265
        Example request:

        {"jsonrpc":"2.0","id":1,"method":"ui_onInputRequired","params":[{"title":"Master Password","prompt":"Please enter the password to decrypt the master seed","isPassword":true}]}
266 267 268

        :param message: to display
        :return:nothing
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
        """  # noqa: E501
        message = (
            "\n"
            "## {title}\n"
            "\t{prompt}\n"
            "\n"
            "> "
        )
        sys.stdout.write(
            message.format(
                title=req.get("title"),
                prompt=req.get("prompt")
            )
        )
        isPassword = req.get("isPassword")
        if not isPassword:
            return {"text": input()}

        return ""
288 289 290


def main(args):
291
    cmd = ["clef", "--stdio-ui"]
292 293 294
    if len(args) > 0 and args[0] == "test":
        cmd.extend(["--stdio-ui-test"])
    print("cmd: {}".format(" ".join(cmd)))
295

296
    dispatcher = RPCDispatcher()
297 298
    dispatcher.register_instance(StdIOHandler(), "ui_")

299
    # line buffered
300 301 302 303 304 305 306
    p = subprocess.Popen(
        cmd,
        bufsize=1,
        universal_newlines=True,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
    )
307 308

    rpc_server = RPCServer(
309
        PipeTransport(p.stdout, p.stdin), JSONRPCProtocol(), dispatcher
310 311 312
    )
    rpc_server.serve_forever()

313 314

if __name__ == "__main__":
315
    main(sys.argv[1:])