Unverified Commit a0943b89 authored by Péter Szilágyi's avatar Péter Szilágyi Committed by GitHub

cmd/clef, signer: refresh tutorial, fix noticed issues (#19774)

* cmd/clef, signer: refresh tutorial, fix noticed issues

* cmd/clef, signer: support removing stored keys (delpw + rules)

* cmd/clef: polishes + Geth integration in the tutorial
parent 6bf5555c
This diff is collapsed.
...@@ -11,7 +11,7 @@ Example: ...@@ -11,7 +11,7 @@ Example:
"content_type": "text/plain", "content_type": "text/plain",
"address": "0xDEADbEeF000000000000000000000000DeaDbeEf", "address": "0xDEADbEeF000000000000000000000000DeaDbeEf",
"raw_data": "GUV0aGVyZXVtIFNpZ25lZCBNZXNzYWdlOgoxMWhlbGxvIHdvcmxk", "raw_data": "GUV0aGVyZXVtIFNpZ25lZCBNZXNzYWdlOgoxMWhlbGxvIHdvcmxk",
"message": [ "messages": [
{ {
"name": "message", "name": "message",
"value": "\u0019Ethereum Signed Message:\n11hello world", "value": "\u0019Ethereum Signed Message:\n11hello world",
...@@ -133,7 +133,7 @@ This occurs _after_ successful completion of the entire signing procedure, but r ...@@ -133,7 +133,7 @@ This occurs _after_ successful completion of the entire signing procedure, but r
A ruleset that implements a rate limitation needs to know what transactions are sent out to the external interface. By hooking into this methods, the ruleset can maintain track of that count. A ruleset that implements a rate limitation needs to know what transactions are sent out to the external interface. By hooking into this methods, the ruleset can maintain track of that count.
**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. **OBS:** Note that if an attacker can restore your `clef` data to a previous point in time (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content.
The `OnApproved` method cannot be responded to, it's purely informative The `OnApproved` method cannot be responded to, it's purely informative
...@@ -179,7 +179,7 @@ Example: ...@@ -179,7 +179,7 @@ Example:
``` ```
### ListRequest ### ListRequest
Sent when a request has been made to list addresses. The UI is provided with the full `account`s, including local directory names. Note: this information is not passed back to the external caller, who only sees the `address`es. Sent when a request has been made to list addresses. The UI is provided with the full `account`s, including local directory names. Note: this information is not passed back to the external caller, who only sees the `address`es.
Example: Example:
```json ```json
......
### Changelog for external API ## Changelog for external API
The API uses [semantic versioning](https://semver.org/).
TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the:
* MAJOR version when you make incompatible API changes,
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
### 6.0.0 ### 6.0.0
...@@ -14,15 +25,15 @@ The addition of `contentType` makes it possible to use the method for different ...@@ -14,15 +25,15 @@ The addition of `contentType` makes it possible to use the method for different
* signing clique headers, * signing clique headers,
* signing plain personal messages, * signing plain personal messages,
* The external method `account_signTypedData` implements [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and makes it possible to sign typed data. * The external method `account_signTypedData` implements [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and makes it possible to sign typed data.
#### 4.0.0 #### 4.0.0
* The external `account_Ecrecover`-method was removed. * The external `account_Ecrecover`-method was removed.
* The external `account_Import`-method was removed. * The external `account_Import`-method was removed.
#### 3.0.0 #### 3.0.0
* The external `account_List`-method was changed to not expose `url`, which contained info about the local filesystem. It now returns only a list of addresses. * The external `account_List`-method was changed to not expose `url`, which contained info about the local filesystem. It now returns only a list of addresses.
#### 2.0.0 #### 2.0.0
...@@ -33,15 +44,3 @@ makes the `accounts_signTransaction` identical to the old `eth_signTransaction`. ...@@ -33,15 +44,3 @@ makes the `accounts_signTransaction` identical to the old `eth_signTransaction`.
#### 1.0.0 #### 1.0.0
Initial release. Initial release.
### Versioning
The API uses [semantic versioning](https://semver.org/).
TLDR; Given a version number MAJOR.MINOR.PATCH, increment the:
* MAJOR version when you make incompatible API changes,
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
### Changelog for internal API (ui-api) ## Changelog for internal API (ui-api)
### 6.0.0 The API uses [semantic versioning](https://semver.org/).
TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the:
* MAJOR version when you make incompatible API changes,
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
### 7.0.0
- The `message` field was renamed to `messages` in all data signing request methods to better reflect that it's a list, not a value.
- The `storage.Put` and `storage.Get` methods in the rule execution engine were lower-cased to `storage.put` and `storage.get` to be consistent with JavaScript call conventions.
### 6.0.0
Removed `password` from responses to operations which require them. This is for two reasons, Removed `password` from responses to operations which require them. This is for two reasons,
- Consistency between how rulesets operate and how manual processing works. A rule can `Approve` but require the actual password to be stored in the clef storage. - Consistency between how rulesets operate and how manual processing works. A rule can `Approve` but require the actual password to be stored in the clef storage.
With this change, the same stored password can be used even if rulesets are not enabled, but storage is. With this change, the same stored password can be used even if rulesets are not enabled, but storage is.
- It also removes the usability-shortcut that a UI might otherwise want to implement; remembering passwords. Since we now will not require the - It also removes the usability-shortcut that a UI might otherwise want to implement; remembering passwords. Since we now will not require the
password on every `Approve`, there's no need for the UI to cache it locally. password on every `Approve`, there's no need for the UI to cache it locally.
- In a future update, we'll likely add `clef_storePassword` to the internal API, so the user can store it via his UI (currently only CLI works). - In a future update, we'll likely add `clef_storePassword` to the internal API, so the user can store it via his UI (currently only CLI works).
Affected datatypes: Affected datatypes:
- `SignTxResponse` - `SignTxResponse`
- `SignDataResponse` - `SignDataResponse`
- `NewAccountResponse` - `NewAccountResponse`
If `clef` requires a password, the `OnInputRequired` will be used to collect it. If `clef` requires a password, the `OnInputRequired` will be used to collect it.
### 5.0.0 ### 5.0.0
Changed the namespace format to adhere to the legacy ethereum format: `name_methodName`. Changes: Changed the namespace format to adhere to the legacy ethereum format: `name_methodName`. Changes:
...@@ -38,7 +53,7 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth ...@@ -38,7 +53,7 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth
### 4.0.0 ### 4.0.0
* Bidirectional communication implemented, so the UI can query `clef` via the stdin/stdout RPC channel. Methods implemented are: * Bidirectional communication implemented, so the UI can query `clef` via the stdin/stdout RPC channel. Methods implemented are:
- `clef_listWallets` - `clef_listWallets`
- `clef_listAccounts` - `clef_listAccounts`
- `clef_listWallets` - `clef_listWallets`
- `clef_deriveAccount` - `clef_deriveAccount`
...@@ -48,10 +63,10 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth ...@@ -48,10 +63,10 @@ Changed the namespace format to adhere to the legacy ethereum format: `name_meth
- `clef_setChainId` - `clef_setChainId`
- `clef_export` - `clef_export`
- `clef_import` - `clef_import`
* The type `Account` was modified (the json-field `type` was removed), to consist of
```golang * The type `Account` was modified (the json-field `type` was removed), to consist of
```go
type Account struct { type Account struct {
Address common.Address `json:"address"` // Ethereum account address derived from the key Address common.Address `json:"address"` // Ethereum account address derived from the key
URL URL `json:"url"` // Optional resource locator within a backend URL URL `json:"url"` // Optional resource locator within a backend
...@@ -64,7 +79,7 @@ type Account struct { ...@@ -64,7 +79,7 @@ type Account struct {
* Make `ShowError`, `OnApprovedTx`, `OnSignerStartup` be json-rpc [notifications](https://www.jsonrpc.org/specification#notification): * Make `ShowError`, `OnApprovedTx`, `OnSignerStartup` be json-rpc [notifications](https://www.jsonrpc.org/specification#notification):
> A Notification is a Request object without an "id" member. A Request object that is a Notification signifies the Client's lack of interest in the corresponding Response object, and as such no Response object needs to be returned to the client. The Server MUST NOT reply to a Notification, including those that are within a batch request. > A Notification is a Request object without an "id" member. A Request object that is a Notification signifies the Client's lack of interest in the corresponding Response object, and as such no Response object needs to be returned to the client. The Server MUST NOT reply to a Notification, including those that are within a batch request.
> >
> Notifications are not confirmable by definition, since they do not have a Response object to be returned. As such, the Client would not be aware of any errors (like e.g. "Invalid params","Internal error" > Notifications are not confirmable by definition, since they do not have a Response object to be returned. As such, the Client would not be aware of any errors (like e.g. "Invalid params","Internal error"
### 3.1.0 ### 3.1.0
...@@ -79,15 +94,17 @@ type Account struct { ...@@ -79,15 +94,17 @@ type Account struct {
* Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords. * Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords.
The following structures are used: The following structures are used:
```golang
UserInputRequest struct { ```go
Prompt string `json:"prompt"` UserInputRequest struct {
Title string `json:"title"` Prompt string `json:"prompt"`
IsPassword bool `json:"isPassword"` Title string `json:"title"`
} IsPassword bool `json:"isPassword"`
UserInputResponse struct { }
Text string `json:"text"` UserInputResponse struct {
} Text string `json:"text"`
}
```
### 2.0.0 ### 2.0.0
...@@ -161,15 +178,3 @@ Example call: ...@@ -161,15 +178,3 @@ Example call:
#### 1.0.0 #### 1.0.0
Initial release. Initial release.
### Versioning
The API uses [semantic versioning](https://semver.org/).
TLDR; Given a version number MAJOR.MINOR.PATCH, increment the:
* MAJOR version when you make incompatible API changes,
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
This diff is collapsed.
...@@ -42,7 +42,6 @@ class PipeTransport(ServerTransport): ...@@ -42,7 +42,6 @@ class PipeTransport(ServerTransport):
self.output.write("\n") self.output.write("\n")
class StdIOHandler(): class StdIOHandler():
def __init__(self): def __init__(self):
pass pass
...@@ -76,7 +75,7 @@ class StdIOHandler(): ...@@ -76,7 +75,7 @@ class StdIOHandler():
:param transaction: transaction info :param transaction: transaction info
:param call_info: info abou the call, e.g. if ABI info could not be :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 :param meta: metadata about the request, e.g. where the call comes from
:return: :return:
""" """
transaction = req.get('transaction') transaction = req.get('transaction')
_from = req.get('from') _from = req.get('from')
...@@ -158,8 +157,7 @@ class StdIOHandler(): ...@@ -158,8 +157,7 @@ class StdIOHandler():
return return
def main(args): def main(args):
cmd = ["clef", "--stdio-ui"]
cmd = ["./clef", "--stdio-ui"]
if len(args) > 0 and args[0] == "test": if len(args) > 0 and args[0] == "test":
cmd.extend(["--stdio-ui-test"]) cmd.extend(["--stdio-ui-test"])
print("cmd: {}".format(" ".join(cmd))) print("cmd: {}".format(" ".join(cmd)))
......
...@@ -19,32 +19,30 @@ The section below deals with both of them ...@@ -19,32 +19,30 @@ The section below deals with both of them
A ruleset file is implemented as a `js` file. Under the hood, the ruleset-engine is a `SignerUI`, implementing the same methods as the `json-rpc` methods A ruleset file is implemented as a `js` file. Under the hood, the ruleset-engine is a `SignerUI`, implementing the same methods as the `json-rpc` methods
defined in the UI protocol. Example: defined in the UI protocol. Example:
```javascript ```js
function asBig(str) {
function asBig(str){ if (str.slice(0, 2) == "0x") {
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} return new BigNumber(str.slice(2), 16)
return new BigNumber(str) }
return new BigNumber(str)
} }
// Approve transactions to a certain contract if value is below a certain limit // Approve transactions to a certain contract if value is below a certain limit
function ApproveTx(req){ function ApproveTx(req) {
var limit = big.Newint("0xb1a2bc2ec50000")
var limit = big.Newint("0xb1a2bc2ec50000")
var value = asBig(req.transaction.value); var value = asBig(req.transaction.value);
if(req.transaction.to.toLowerCase()=="0xae967917c465db8578ca9024c205720b1a3651a9") if (req.transaction.to.toLowerCase() == "0xae967917c465db8578ca9024c205720b1a3651a9") && value.lt(limit)) {
&& value.lt(limit) ){ return "Approve"
return "Approve" }
} // If we return "Reject", it will be rejected.
// If we return "Reject", it will be rejected. // By not returning anything, it will be passed to the next UI, for manual processing
// By not returning anything, it will be passed to the next UI, for manual processing
} }
//Approve listings if request made from IPC // Approve listings if request made from IPC
function ApproveListing(req){ function ApproveListing(req){
if (req.metadata.scheme == "ipc"){ return "Approve"} if (req.metadata.scheme == "ipc"){ return "Approve"}
} }
``` ```
Whenever the external API is called (and the ruleset is enabled), the `signer` calls the UI, which is an instance of a ruleset-engine. The ruleset-engine Whenever the external API is called (and the ruleset is enabled), the `signer` calls the UI, which is an instance of a ruleset-engine. The ruleset-engine
...@@ -140,97 +138,97 @@ This is now implemented (with ephemeral non-encrypted storage for now, so not ye ...@@ -140,97 +138,97 @@ This is now implemented (with ephemeral non-encrypted storage for now, so not ye
## Example 1: ruleset for a rate-limited window ## Example 1: ruleset for a rate-limited window
```javascript ```js
function big(str) {
function big(str){ if (str.slice(0, 2) == "0x") {
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} return new BigNumber(str.slice(2), 16)
return new BigNumber(str)
} }
return new BigNumber(str)
}
// Time window: 1 week // Time window: 1 week
var window = 1000* 3600*24*7; var window = 1000* 3600*24*7;
// Limit : 1 ether // Limit : 1 ether
var limit = new BigNumber("1e18"); var limit = new BigNumber("1e18");
function isLimitOk(transaction){ function isLimitOk(transaction) {
var value = big(transaction.value) var value = big(transaction.value)
// Start of our window function // Start of our window function
var windowstart = new Date().getTime() - window; var windowstart = new Date().getTime() - window;
var txs = []; var txs = [];
var stored = storage.Get('txs'); var stored = storage.get('txs');
if(stored != ""){ if (stored != "") {
txs = JSON.parse(stored) txs = JSON.parse(stored)
} }
// First, remove all that have passed out of the time-window // First, remove all that have passed out of the time-window
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart}); var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
console.log(txs, newtxs.length); console.log(txs, newtxs.length);
// Secondly, aggregate the current sum // Secondly, aggregate the current sum
sum = new BigNumber(0) sum = new BigNumber(0)
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
console.log("ApproveTx > Sum so far", sum); console.log("ApproveTx > Sum so far", sum);
console.log("ApproveTx > Requested", value.toNumber()); console.log("ApproveTx > Requested", value.toNumber());
// Would we exceed weekly limit ? // Would we exceed weekly limit ?
return sum.plus(value).lt(limit) return sum.plus(value).lt(limit)
}
function ApproveTx(r) {
if (isLimitOk(r.transaction)) {
return "Approve"
} }
function ApproveTx(r){ return "Nope"
if (isLimitOk(r.transaction)){ }
return "Approve"
}
return "Nope"
}
/** /**
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
* 'response_str' contains the return value that will be sent to the external caller. * 'response_str' contains the return value that will be sent to the external caller.
* The return value from this method is ignore - the reason for having this callback is to allow the * The return value from this method is ignore - the reason for having this callback is to allow the
* ruleset to keep track of approved transactions. * ruleset to keep track of approved transactions.
* *
* When implementing rate-limited rules, this callback should be used. * When implementing rate-limited rules, this callback should be used.
* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
* then accepts the transaction, this method will be called. * then accepts the transaction, this method will be called.
* *
* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
*/ */
function OnApprovedTx(resp){ function OnApprovedTx(resp) {
var value = big(resp.tx.value) var value = big(resp.tx.value)
var txs = [] var txs = []
// Load stored transactions // Load stored transactions
var stored = storage.Get('txs'); var stored = storage.get('txs');
if(stored != ""){ if (stored != "") {
txs = JSON.parse(stored) txs = JSON.parse(stored)
}
// Add this to the storage
txs.push({tstamp: new Date().getTime(), value: value});
storage.Put("txs", JSON.stringify(txs));
} }
// Add this to the storage
txs.push({tstamp: new Date().getTime(), value: value});
storage.put("txs", JSON.stringify(txs));
}
``` ```
## Example 2: allow destination ## Example 2: allow destination
```javascript ```js
function ApproveTx(r) {
function ApproveTx(r){ if (r.transaction.from.toLowerCase() == "0x0000000000000000000000000000000000001337") {
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} return "Approve"
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
// Otherwise goes to manual processing
} }
if (r.transaction.from.toLowerCase() == "0x000000000000000000000000000000000000dead") {
return "Reject"
}
// Otherwise goes to manual processing
}
``` ```
## Example 3: Allow listing ## Example 3: Allow listing
```javascript ```js
function ApproveListing() {
function ApproveListing(){ return "Approve"
return "Approve" }
} ```
```
\ No newline at end of file
This diff is collapsed.
...@@ -24,7 +24,6 @@ import ( ...@@ -24,7 +24,6 @@ import (
"math/big" "math/big"
"os" "os"
"reflect" "reflect"
"strings"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
...@@ -44,7 +43,7 @@ const ( ...@@ -44,7 +43,7 @@ const (
// ExternalAPIVersion -- see extapi_changelog.md // ExternalAPIVersion -- see extapi_changelog.md
ExternalAPIVersion = "6.0.0" ExternalAPIVersion = "6.0.0"
// InternalAPIVersion -- see intapi_changelog.md // InternalAPIVersion -- see intapi_changelog.md
InternalAPIVersion = "6.0.0" InternalAPIVersion = "7.0.0"
) )
// ExternalAPI defines the external API through which signing requests are made. // ExternalAPI defines the external API through which signing requests are made.
...@@ -234,7 +233,7 @@ type ( ...@@ -234,7 +233,7 @@ type (
ContentType string `json:"content_type"` ContentType string `json:"content_type"`
Address common.MixedcaseAddress `json:"address"` Address common.MixedcaseAddress `json:"address"`
Rawdata []byte `json:"raw_data"` Rawdata []byte `json:"raw_data"`
Message []*NameValueType `json:"message"` Messages []*NameValueType `json:"messages"`
Hash hexutil.Bytes `json:"hash"` Hash hexutil.Bytes `json:"hash"`
Meta Metadata `json:"meta"` Meta Metadata `json:"meta"`
} }
...@@ -477,22 +476,24 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool { ...@@ -477,22 +476,24 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
return modified return modified
} }
func (api *SignerAPI) lookupPassword(address common.Address) string { func (api *SignerAPI) lookupPassword(address common.Address) (string, error) {
return api.credentials.Get(strings.ToLower(address.String())) return api.credentials.Get(address.Hex())
} }
func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) { func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) {
if pw := api.lookupPassword(address); pw != "" { // Look up the password and return if available
if pw, err := api.lookupPassword(address); err == nil {
return pw, nil return pw, nil
} else {
pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true})
if err != nil {
log.Warn("error obtaining password", "error", err)
// We'll not forward the error here, in case the error contains info about the response from the UI,
// which could leak the password if it was malformed json or something
return "", errors.New("internal error")
}
return pwResp.Text, nil
} }
// Password unavailable, request it from the user
pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true})
if err != nil {
log.Warn("error obtaining password", "error", err)
// We'll not forward the error here, in case the error contains info about the response from the UI,
// which could leak the password if it was malformed json or something
return "", errors.New("internal error")
}
return pwResp.Text, nil
} }
// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form // SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
......
...@@ -169,13 +169,12 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp ...@@ -169,13 +169,12 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
fmt.Printf("-------- Sign data request--------------\n") fmt.Printf("-------- Sign data request--------------\n")
fmt.Printf("Account: %s\n", request.Address.String()) fmt.Printf("Account: %s\n", request.Address.String())
fmt.Printf("message:\n") fmt.Printf("messages:\n")
for _, nvt := range request.Message { for _, nvt := range request.Messages {
fmt.Printf("%v\n", nvt.Pprint(1)) fmt.Printf("%v\n", nvt.Pprint(1))
} }
//fmt.Printf("message: \n%v\n", request.Message)
fmt.Printf("raw data: \n%q\n", request.Rawdata) fmt.Printf("raw data: \n%q\n", request.Rawdata)
fmt.Printf("message hash: %v\n", request.Hash) fmt.Printf("data hash: %v\n", request.Hash)
fmt.Printf("-------------------------------------------\n") fmt.Printf("-------------------------------------------\n")
showMetadata(request.Meta) showMetadata(request.Meta)
if !ui.confirm() { if !ui.confirm() {
...@@ -187,7 +186,6 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp ...@@ -187,7 +186,6 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
// ApproveListing prompt the user for confirmation to list accounts // ApproveListing prompt the user for confirmation to list accounts
// the list of accounts to list can be modified by the UI // the list of accounts to list can be modified by the UI
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) { func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
......
...@@ -123,11 +123,10 @@ type TypedDataDomain struct { ...@@ -123,11 +123,10 @@ type TypedDataDomain struct {
var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`) var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`)
// sign receives a request and produces a signature // sign receives a request and produces a signature
//
// Note, the produced signature conforms to the secp256k1 curve R, S and V values, // Note, the produced signature conforms to the secp256k1 curve R, S and V values,
// where the V value will be 27 or 28 for legacy reasons, if legacyV==true. // where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) {
// We make the request prior to looking up if we actually have the account, to prevent // We make the request prior to looking up if we actually have the account, to prevent
// account-enumeration via the API // account-enumeration via the API
res, err := api.UI.ApproveSignData(req) res, err := api.UI.ApproveSignData(req)
...@@ -169,7 +168,6 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com ...@@ -169,7 +168,6 @@ func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr com
if err != nil { if err != nil {
return nil, err return nil, err
} }
signature, err := api.sign(addr, req, transformV) signature, err := api.sign(addr, req, transformV)
if err != nil { if err != nil {
api.UI.ShowError(err.Error()) api.UI.ShowError(err.Error())
...@@ -202,7 +200,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType ...@@ -202,7 +200,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
return nil, useEthereumV, err return nil, useEthereumV, err
} }
sighash, msg := SignTextValidator(validatorData) sighash, msg := SignTextValidator(validatorData)
message := []*NameValueType{ messages := []*NameValueType{
{ {
Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)",
Typ: "description", Typ: "description",
...@@ -224,7 +222,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType ...@@ -224,7 +222,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
Value: fmt.Sprintf("0x%x", msg), Value: fmt.Sprintf("0x%x", msg),
}, },
} }
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
case ApplicationClique.Mime: case ApplicationClique.Mime:
// Clique is the Ethereum PoA standard // Clique is the Ethereum PoA standard
stringData, ok := data.(string) stringData, ok := data.(string)
...@@ -251,7 +249,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType ...@@ -251,7 +249,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
if err != nil { if err != nil {
return nil, useEthereumV, err return nil, useEthereumV, err
} }
message := []*NameValueType{ messages := []*NameValueType{
{ {
Name: "Clique header", Name: "Clique header",
Typ: "clique", Typ: "clique",
...@@ -260,7 +258,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType ...@@ -260,7 +258,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
} }
// Clique uses V on the form 0 or 1 // Clique uses V on the form 0 or 1
useEthereumV = false useEthereumV = false
req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Message: message, Hash: sighash} req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash}
default: // also case TextPlain.Mime: default: // also case TextPlain.Mime:
// Calculates an Ethereum ECDSA signature for: // Calculates an Ethereum ECDSA signature for:
// hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}") // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}")
...@@ -272,21 +270,20 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType ...@@ -272,21 +270,20 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
return nil, useEthereumV, err return nil, useEthereumV, err
} else { } else {
sighash, msg := accounts.TextAndHash(textData) sighash, msg := accounts.TextAndHash(textData)
message := []*NameValueType{ messages := []*NameValueType{
{ {
Name: "message", Name: "message",
Typ: accounts.MimetypeTextPlain, Typ: accounts.MimetypeTextPlain,
Value: msg, Value: msg,
}, },
} }
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
} }
} }
} }
req.Address = addr req.Address = addr
req.Meta = MetadataFromContext(ctx) req.Meta = MetadataFromContext(ctx)
return req, useEthereumV, nil return req, useEthereumV, nil
} }
// SignTextWithValidator signs the given message which can be further recovered // SignTextWithValidator signs the given message which can be further recovered
...@@ -327,11 +324,11 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd ...@@ -327,11 +324,11 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd
} }
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
sighash := crypto.Keccak256(rawData) sighash := crypto.Keccak256(rawData)
message, err := typedData.Format() messages, err := typedData.Format()
if err != nil { if err != nil {
return nil, err return nil, err
} }
req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Message: message, Hash: sighash} req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Messages: messages, Hash: sighash}
signature, err := api.sign(addr, req, true) signature, err := api.sign(addr, req, true)
if err != nil { if err != nil {
api.UI.ShowError(err.Error()) api.UI.ShowError(err.Error())
......
...@@ -74,12 +74,28 @@ func (r *rulesetUI) execute(jsfunc string, jsarg interface{}) (otto.Value, error ...@@ -74,12 +74,28 @@ func (r *rulesetUI) execute(jsfunc string, jsarg interface{}) (otto.Value, error
// Instantiate a fresh vm engine every time // Instantiate a fresh vm engine every time
vm := otto.New() vm := otto.New()
// Set the native callbacks // Set the native callbacks
consoleObj, _ := vm.Get("console") consoleObj, _ := vm.Get("console")
consoleObj.Object().Set("log", consoleOutput) consoleObj.Object().Set("log", consoleOutput)
consoleObj.Object().Set("error", consoleOutput) consoleObj.Object().Set("error", consoleOutput)
vm.Set("storage", r.storage)
vm.Set("storage", struct{}{})
storageObj, _ := vm.Get("storage")
storageObj.Object().Set("put", func(call otto.FunctionCall) otto.Value {
key, val := call.Argument(0).String(), call.Argument(1).String()
if val == "" {
r.storage.Del(key)
} else {
r.storage.Put(key, val)
}
return otto.NullValue()
})
storageObj.Object().Set("get", func(call otto.FunctionCall) otto.Value {
goval, _ := r.storage.Get(call.Argument(0).String())
jsval, _ := otto.ToValue(goval)
return jsval
})
// Load bootstrap libraries // Load bootstrap libraries
script, err := vm.Compile("bignumber.js", BigNumber_JS) script, err := vm.Compile("bignumber.js", BigNumber_JS)
if err != nil { if err != nil {
......
...@@ -301,23 +301,23 @@ func TestStorage(t *testing.T) { ...@@ -301,23 +301,23 @@ func TestStorage(t *testing.T) {
js := ` js := `
function testStorage(){ function testStorage(){
storage.Put("mykey", "myvalue") storage.put("mykey", "myvalue")
a = storage.Get("mykey") a = storage.get("mykey")
storage.Put("mykey", ["a", "list"]) // Should result in "a,list" storage.put("mykey", ["a", "list"]) // Should result in "a,list"
a += storage.Get("mykey") a += storage.get("mykey")
storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]" storage.put("mykey", {"an": "object"}) // Should result in "[object Object]"
a += storage.Get("mykey") a += storage.get("mykey")
storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}' storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
a += storage.Get("mykey") a += storage.get("mykey")
a += storage.Get("missingkey") //Missing keys should result in empty string a += storage.get("missingkey") //Missing keys should result in empty string
storage.Put("","missing key==noop") // Can't store with 0-length key storage.put("","missing key==noop") // Can't store with 0-length key
a += storage.Get("") // Should result in '' a += storage.get("") // Should result in ''
var b = new BigNumber(2) var b = new BigNumber(2)
var c = new BigNumber(16)//"0xf0",16) var c = new BigNumber(16)//"0xf0",16)
...@@ -337,7 +337,6 @@ func TestStorage(t *testing.T) { ...@@ -337,7 +337,6 @@ func TestStorage(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Unexpected error %v", err) t.Errorf("Unexpected error %v", err)
} }
retval, err := v.ToString() retval, err := v.ToString()
if err != nil { if err != nil {
...@@ -369,7 +368,7 @@ const ExampleTxWindow = ` ...@@ -369,7 +368,7 @@ const ExampleTxWindow = `
var windowstart = new Date().getTime() - window; var windowstart = new Date().getTime() - window;
var txs = []; var txs = [];
var stored = storage.Get('txs'); var stored = storage.get('txs');
if(stored != ""){ if(stored != ""){
txs = JSON.parse(stored) txs = JSON.parse(stored)
...@@ -414,19 +413,18 @@ const ExampleTxWindow = ` ...@@ -414,19 +413,18 @@ const ExampleTxWindow = `
var value = big(resp.tx.value) var value = big(resp.tx.value)
var txs = [] var txs = []
// Load stored transactions // Load stored transactions
var stored = storage.Get('txs'); var stored = storage.get('txs');
if(stored != ""){ if(stored != ""){
txs = JSON.parse(stored) txs = JSON.parse(stored)
} }
// Add this to the storage // Add this to the storage
txs.push({tstamp: new Date().getTime(), value: value}); txs.push({tstamp: new Date().getTime(), value: value});
storage.Put("txs", JSON.stringify(txs)); storage.put("txs", JSON.stringify(txs));
} }
` `
func dummyTx(value hexutil.Big) *core.SignTxRequest { func dummyTx(value hexutil.Big) *core.SignTxRequest {
to, _ := mixAddr("000000000000000000000000000000000000dead") to, _ := mixAddr("000000000000000000000000000000000000dead")
from, _ := mixAddr("000000000000000000000000000000000000dead") from, _ := mixAddr("000000000000000000000000000000000000dead")
n := hexutil.Uint64(3) n := hexutil.Uint64(3)
...@@ -448,28 +446,27 @@ func dummyTx(value hexutil.Big) *core.SignTxRequest { ...@@ -448,28 +446,27 @@ func dummyTx(value hexutil.Big) *core.SignTxRequest {
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
} }
} }
func dummyTxWithV(value uint64) *core.SignTxRequest {
func dummyTxWithV(value uint64) *core.SignTxRequest {
v := big.NewInt(0).SetUint64(value) v := big.NewInt(0).SetUint64(value)
h := hexutil.Big(*v) h := hexutil.Big(*v)
return dummyTx(h) return dummyTx(h)
} }
func dummySigned(value *big.Int) *types.Transaction { func dummySigned(value *big.Int) *types.Transaction {
to := common.HexToAddress("000000000000000000000000000000000000dead") to := common.HexToAddress("000000000000000000000000000000000000dead")
gas := uint64(21000) gas := uint64(21000)
gasPrice := big.NewInt(2000000) gasPrice := big.NewInt(2000000)
data := make([]byte, 0) data := make([]byte, 0)
return types.NewTransaction(3, to, value, gas, gasPrice, data) return types.NewTransaction(3, to, value, gas, gasPrice, data)
} }
func TestLimitWindow(t *testing.T) {
func TestLimitWindow(t *testing.T) {
r, err := initRuleEngine(ExampleTxWindow) r, err := initRuleEngine(ExampleTxWindow)
if err != nil { if err != nil {
t.Errorf("Couldn't create evaluator %v", err) t.Errorf("Couldn't create evaluator %v", err)
return return
} }
// 0.3 ether: 429D069189E0000 wei // 0.3 ether: 429D069189E0000 wei
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000")) v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
h := hexutil.Big(*v) h := hexutil.Big(*v)
...@@ -496,7 +493,6 @@ func TestLimitWindow(t *testing.T) { ...@@ -496,7 +493,6 @@ func TestLimitWindow(t *testing.T) {
if resp.Approved { if resp.Approved {
t.Errorf("Expected check to resolve to 'Reject'") t.Errorf("Expected check to resolve to 'Reject'")
} }
} }
// dontCallMe is used as a next-handler that does not want to be called - it invokes test failure // dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
...@@ -508,6 +504,7 @@ func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInput ...@@ -508,6 +504,7 @@ func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInput
d.t.Fatalf("Did not expect next-handler to be called") d.t.Fatalf("Did not expect next-handler to be called")
return core.UserInputResponse{}, nil return core.UserInputResponse{}, nil
} }
func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) { func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) {
} }
...@@ -589,7 +586,7 @@ func TestSignData(t *testing.T) { ...@@ -589,7 +586,7 @@ func TestSignData(t *testing.T) {
function ApproveSignData(r){ function ApproveSignData(r){
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
{ {
if(r.message[0].value.indexOf("bazonk") >= 0){ if(r.messages[0].value.indexOf("bazonk") >= 0){
return "Approve" return "Approve"
} }
return "Reject" return "Reject"
...@@ -615,11 +612,11 @@ function ApproveSignData(r){ ...@@ -615,11 +612,11 @@ function ApproveSignData(r){
}, },
} }
resp, err := r.ApproveSignData(&core.SignDataRequest{ resp, err := r.ApproveSignData(&core.SignDataRequest{
Address: *addr, Address: *addr,
Message: nvt, Messages: nvt,
Hash: hash, Hash: hash,
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
Rawdata: []byte(rawdata), Rawdata: []byte(rawdata),
}) })
if err != nil { if err != nil {
t.Fatalf("Unexpected error %v", err) t.Fatalf("Unexpected error %v", err)
......
...@@ -53,7 +53,7 @@ func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage { ...@@ -53,7 +53,7 @@ func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage {
} }
} }
// Put stores a value by key. 0-length keys results in no-op // Put stores a value by key. 0-length keys results in noop.
func (s *AESEncryptedStorage) Put(key, value string) { func (s *AESEncryptedStorage) Put(key, value string) {
if len(key) == 0 { if len(key) == 0 {
return return
...@@ -75,27 +75,41 @@ func (s *AESEncryptedStorage) Put(key, value string) { ...@@ -75,27 +75,41 @@ func (s *AESEncryptedStorage) Put(key, value string) {
} }
} }
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length // Get returns the previously stored value, or an error if it does not exist or
func (s *AESEncryptedStorage) Get(key string) string { // key is of 0-length.
func (s *AESEncryptedStorage) Get(key string) (string, error) {
if len(key) == 0 { if len(key) == 0 {
return "" return "", ErrZeroKey
} }
data, err := s.readEncryptedStorage() data, err := s.readEncryptedStorage()
if err != nil { if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename) log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return "" return "", err
} }
encrypted, exist := data[key] encrypted, exist := data[key]
if !exist { if !exist {
log.Warn("Key does not exist", "key", key) log.Warn("Key does not exist", "key", key)
return "" return "", ErrNotFound
} }
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key)) entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key))
if err != nil { if err != nil {
log.Warn("Failed to decrypt key", "key", key) log.Warn("Failed to decrypt key", "key", key)
return "" return "", err
}
return string(entry), nil
}
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
func (s *AESEncryptedStorage) Del(key string) {
data, err := s.readEncryptedStorage()
if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return
}
delete(data, key)
if err = s.writeEncryptedStorage(data); err != nil {
log.Warn("Failed to write entry", "err", err)
} }
return string(entry)
} }
// readEncryptedStorage reads the file with encrypted creds // readEncryptedStorage reads the file with encrypted creds
......
...@@ -110,8 +110,8 @@ func TestEnd2End(t *testing.T) { ...@@ -110,8 +110,8 @@ func TestEnd2End(t *testing.T) {
} }
s1.Put("bazonk", "foobar") s1.Put("bazonk", "foobar")
if v := s2.Get("bazonk"); v != "foobar" { if v, err := s2.Get("bazonk"); v != "foobar" || err != nil {
t.Errorf("Expected bazonk->foobar, got '%v'", v) t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err)
} }
} }
...@@ -154,11 +154,11 @@ func TestSwappedKeys(t *testing.T) { ...@@ -154,11 +154,11 @@ func TestSwappedKeys(t *testing.T) {
} }
} }
swap() swap()
if v := s1.Get("k1"); v != "" { if v, _ := s1.Get("k1"); v != "" {
t.Errorf("swapped value should return empty") t.Errorf("swapped value should return empty")
} }
swap() swap()
if v := s1.Get("k1"); v != "v1" { if v, _ := s1.Get("k1"); v != "v1" {
t.Errorf("double-swapped value should work fine") t.Errorf("double-swapped value should work fine")
} }
} }
...@@ -17,11 +17,26 @@ ...@@ -17,11 +17,26 @@
package storage package storage
import "errors"
var (
// ErrZeroKey is returned if an attempt was made to inset a 0-length key.
ErrZeroKey = errors.New("0-length key")
// ErrNotFound is returned if an unknown key is attempted to be retrieved.
ErrNotFound = errors.New("not found")
)
type Storage interface { type Storage interface {
// Put stores a value by key. 0-length keys results in no-op // Put stores a value by key. 0-length keys results in noop.
Put(key, value string) Put(key, value string)
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
Get(key string) string // Get returns the previously stored value, or an error if the key is 0-length
// or unknown.
Get(key string) (string, error)
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
Del(key string)
} }
// EphemeralStorage is an in-memory storage that does // EphemeralStorage is an in-memory storage that does
...@@ -31,23 +46,29 @@ type EphemeralStorage struct { ...@@ -31,23 +46,29 @@ type EphemeralStorage struct {
namespace string namespace string
} }
// Put stores a value by key. 0-length keys results in noop.
func (s *EphemeralStorage) Put(key, value string) { func (s *EphemeralStorage) Put(key, value string) {
if len(key) == 0 { if len(key) == 0 {
return return
} }
//fmt.Printf("storage: put %v -> %v\n", key, value)
s.data[key] = value s.data[key] = value
} }
func (s *EphemeralStorage) Get(key string) string { // Get returns the previously stored value, or an error if the key is 0-length
// or unknown.
func (s *EphemeralStorage) Get(key string) (string, error) {
if len(key) == 0 { if len(key) == 0 {
return "" return "", ErrZeroKey
} }
//fmt.Printf("storage: get %v\n", key) if v, ok := s.data[key]; ok {
if v, exist := s.data[key]; exist { return v, nil
return v
} }
return "" return "", ErrNotFound
}
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
func (s *EphemeralStorage) Del(key string) {
delete(s.data, key)
} }
func NewEphemeralStorage() Storage { func NewEphemeralStorage() Storage {
...@@ -61,6 +82,7 @@ func NewEphemeralStorage() Storage { ...@@ -61,6 +82,7 @@ func NewEphemeralStorage() Storage {
type NoStorage struct{} type NoStorage struct{}
func (s *NoStorage) Put(key, value string) {} func (s *NoStorage) Put(key, value string) {}
func (s *NoStorage) Get(key string) string { func (s *NoStorage) Del(key string) {}
return "" func (s *NoStorage) Get(key string) (string, error) {
return "", errors.New("I forgot")
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment