Skip to content

Commit

Permalink
Feat(Signing): Message Signing (#903)
Browse files Browse the repository at this point in the history
* feat(Account): Add `signMessage` method to Account stamp

* feat(AEX-2): Add `signMessage` method and new request to RPC

* feat(AEX-2): Fix action arguments propagation

* feat(AEX-2): Fix example app

* feat(AEX-2): Fix broken example app. Fix default option for RpcWallet
Resolve https://github.com/aeternity/tipwallet/issues/87
Resolve #902, #899

* feat(MessageSigning): Replace `æ` with `ae` in message prefix
Make blake2b hash of message before signing
Add `verifyMessage` method to memory account stamp
Fix test's

* feat(MessageSigning): Add `onMessageSign` event to WalletRpc

* feat(MessageSigning): Add test for RPC signing

* docs(API): Generate docs
  • Loading branch information
nduchak authored Feb 19, 2020
1 parent f877ea0 commit bc739b4
Show file tree
Hide file tree
Showing 19 changed files with 14,126 additions and 37 deletions.
2 changes: 2 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
* [@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client](api/utils/aepp-wallet-communication/rpc/rpc-client.md)
* [@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc](api/utils/aepp-wallet-communication/rpc/wallet-rpc.md)
* [@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/wallet-detector](api/utils/aepp-wallet-communication/wallet-detector.md)
* [@aeternity/aepp-sdk/es/utils/amount-formatter](api/utils/amount-formatter.md)
* [@aeternity/aepp-sdk/es/utils/bignumber](api/utils/bignumber.md)
* [@aeternity/aepp-sdk/es/utils/bytes](api/utils/bytes.md)
* [@aeternity/aepp-sdk/es/utils/crypto](api/utils/crypto.md)
* [@aeternity/aepp-sdk/es/utils/keystore](api/utils/keystore.md)
Expand Down
32 changes: 32 additions & 0 deletions docs/api/account.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Account from '@aeternity/aepp-sdk/es/account'
* [@aeternity/aepp-sdk/es/account](#module_@aeternity/aepp-sdk/es/account)
* [Account([options])](#exp_module_@aeternity/aepp-sdk/es/account--Account)`Object`
* [.signTransaction(tx, opt)](#module_@aeternity/aepp-sdk/es/account--Account+signTransaction)`String`
* [.signMessage(message, opt)](#module_@aeternity/aepp-sdk/es/account--Account+signMessage)`String`
* [.verifyMessage(message, signature, opt)](#module_@aeternity/aepp-sdk/es/account--Account+verifyMessage)`Boolean`
* [.getNetworkId()](#module_@aeternity/aepp-sdk/es/account--Account+getNetworkId)`String`
* *[.sign(data)](#module_@aeternity/aepp-sdk/es/account--Account+sign)`String`*
* *[.address()](#module_@aeternity/aepp-sdk/es/account--Account+address)`String`*
Expand Down Expand Up @@ -51,6 +53,36 @@ Sign encoded transaction
| tx | `String` | Transaction to sign |
| opt | `Object` | Options |

<a id="module_@aeternity/aepp-sdk/es/account--Account+signMessage"></a>

#### account.signMessage(message, opt) ⇒ `String`
Sign message

**Kind**: instance method of [`Account`](#exp_module_@aeternity/aepp-sdk/es/account--Account)
**Returns**: `String` - Signature
**Category**: async
**rtype**: `(msg: String) => signature: Promise[String], throws: Error`

| Param | Type | Description |
| --- | --- | --- |
| message | `String` | Message to sign |
| opt | `Object` | Options |

<a id="module_@aeternity/aepp-sdk/es/account--Account+verifyMessage"></a>

#### account.verifyMessage(message, signature, opt) ⇒ `Boolean`
Verify message

**Kind**: instance method of [`Account`](#exp_module_@aeternity/aepp-sdk/es/account--Account)
**Category**: async
**rtype**: `(msg: String, signature: String, publicKey: String) => signature: Promise[String], throws: Error`

| Param | Type | Description |
| --- | --- | --- |
| message | `String` | Message to verify |
| signature | `String` | Signature |
| opt | `Object` | Options |

<a id="module_@aeternity/aepp-sdk/es/account--Account+getNetworkId"></a>

#### account.getNetworkId() ⇒ `String`
Expand Down
2 changes: 1 addition & 1 deletion docs/api/contract/aci.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Generate contract ACI object with predefined js methods for contract usage - can
| [options.aci] | `String` | | Contract ACI |
| [options.contractAddress] | `String` | | Contract address |
| [options.filesystem] | `Object` | | Contact source external namespaces map |
| [options.forceCodeCheck] | `Object` | | Don't check contract code |
| [options.forceCodeCheck] | `Object` | <code>true</code> | Don't check contract code |
| [options.opt] | `Object` | | Contract options |

**Example**
Expand Down
12 changes: 11 additions & 1 deletion docs/api/utils/aepp-wallet-communication/rpc/aepp-rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ContentScriptBridge from '@aeternity/aepp-sdk/es/utils/aepp-wallet-commun
* [.askAddresses()](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+askAddresses)`Promise`
* [.subscribeAddress(type, value)](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+subscribeAddress)`Promise`
* [.signTransaction()](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+signTransaction)`Promise.&lt;String&gt;`
* [.signMessage()](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+signMessage)`Promise.&lt;String&gt;`
* [.sendConnectRequest()](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+sendConnectRequest)`Promise`
* [.send(tx, [options])](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+send)`Promise.&lt;Object&gt;`

Expand Down Expand Up @@ -90,6 +91,15 @@ All sdk API which use it will be send notification to wallet and wait for callBa
**Kind**: instance method of [`exports.AeppRpc`](#exp_module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc)
**Returns**: `Promise.&lt;String&gt;` - Signed transaction
**rtype**: `(tx: String, options = {}) => Promise`
<a id="module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+signMessage"></a>

#### exports.AeppRpc.signMessage() ⇒ `Promise.&lt;String&gt;`
Overwriting of `signMessage` AE method
All sdk API which use it will be send notification to wallet and wait for callBack

**Kind**: instance method of [`exports.AeppRpc`](#exp_module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc)
**Returns**: `Promise.&lt;String&gt;` - Signed transaction
**rtype**: `(msg: String, options = {}) => Promise`
<a id="module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc--exports.AeppRpc+sendConnectRequest"></a>

#### exports.AeppRpc.sendConnectRequest() ⇒ `Promise`
Expand All @@ -113,5 +123,5 @@ This method will sign, broadcast and wait until transaction will be accepted usi
| --- | --- | --- |
| tx | `String` | |
| [options] | `Object` | <code>{}</code> |
| [options.walletBroadcast] | `Object` | <code>{}</code> |
| [options.walletBroadcast] | `Object` | <code>true</code> |

5 changes: 3 additions & 2 deletions docs/api/utils/aepp-wallet-communication/rpc/wallet-rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import WalletRpc from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rp
```

* [@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc)
* [exports.WalletRpc(param, onConnection, onSubscription, onSign, onAskAccounts, onDisconnect)](#exp_module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc)`Object`
* [exports.WalletRpc(param, onConnection, onSubscription, onSign, onAskAccounts, onMessageSign, onDisconnect)](#exp_module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc)`Object`
* [.getClients()](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc+getClients)`Object`
* [.addRpcClient(clientConnection)](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc+addRpcClient)`void`
* [.shareWalletInfo(postFn)](#module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc+shareWalletInfo)`void`
Expand All @@ -18,7 +18,7 @@ import WalletRpc from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rp

<a id="exp_module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc"></a>

### exports.WalletRpc(param, onConnection, onSubscription, onSign, onAskAccounts, onDisconnect) ⇒ `Object`
### exports.WalletRpc(param, onConnection, onSubscription, onSign, onAskAccounts, onMessageSign, onDisconnect) ⇒ `Object`
Contain functionality for aepp interaction and managing multiple aepps

**Kind**: Exported function
Expand All @@ -32,6 +32,7 @@ Contain functionality for aepp interaction and managing multiple aepps
| onSubscription | `function` | Call-back function for incoming AEPP account subscription (Second argument contain function for accept/deny request) |
| onSign | `function` | Call-back function for incoming AEPP sign request (Second argument contain function for accept/deny request) |
| onAskAccounts | `function` | Call-back function for incoming AEPP get address request (Second argument contain function for accept/deny request) |
| onMessageSign | `function` | Call-back function for incoming AEPP sign message request (Second argument contain function for accept/deny request) |
| onDisconnect | `function` | Call-back function for disconnect event |

<a id="module_@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc--exports.WalletRpc+getClients"></a>
Expand Down
37 changes: 33 additions & 4 deletions es/account/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

import stampit from '@stamp/it'
import { required } from '@stamp/required'
import * as Crypto from '../utils/crypto'

import { hash, personalMessageToBinary, decodeBase64Check, assertedType, verifyPersonalMessage } from '../utils/crypto'
import { buildTx } from '../tx/builder'
import { decode } from '../tx/builder/helpers'
import { TX_TYPE } from '../tx/builder/schema'

/**
Expand All @@ -39,14 +41,41 @@ import { TX_TYPE } from '../tx/builder/schema'
*/
async function signTransaction (tx, opt = {}) {
const networkId = this.getNetworkId()
const rlpBinaryTx = Crypto.decodeBase64Check(Crypto.assertedType(tx, 'tx'))
const rlpBinaryTx = decodeBase64Check(assertedType(tx, 'tx'))
// Prepend `NETWORK_ID` to begin of data binary
const txWithNetworkId = Buffer.concat([Buffer.from(networkId), rlpBinaryTx])

const signatures = [await this.sign(txWithNetworkId, opt)]
return buildTx({ encodedTx: rlpBinaryTx, signatures }, TX_TYPE.signed).tx
}

/**
* Sign message
* @instance
* @category async
* @rtype (msg: String) => signature: Promise[String], throws: Error
* @param {String} message - Message to sign
* @param {Object} opt - Options
* @return {String} Signature
*/
async function signMessage (message, opt = {}) {
return this.sign(hash(personalMessageToBinary(message)), opt)
}

/**
* Verify message
* @instance
* @category async
* @rtype (msg: String, signature: String, publicKey: String) => signature: Promise[String], throws: Error
* @param {String} message - Message to verify
* @param {String} signature - Signature
* @param {Object} opt - Options
* @return {Boolean}
*/
async function verifyMessage (message, signature, opt = {}) {
return verifyPersonalMessage(message, signature, decode(await this.address(opt)))
}

/**
* Obtain networkId for signing
* @instance
Expand Down Expand Up @@ -81,10 +110,10 @@ const Account = stampit({
this.networkId = networkId
}
},
methods: { signTransaction, getNetworkId },
methods: { signTransaction, getNetworkId, signMessage, verifyMessage },
deepConf: {
Ae: {
methods: ['sign', 'address', 'signTransaction', 'getNetworkId']
methods: ['sign', 'address', 'signTransaction', 'getNetworkId', 'signMessage', 'verifyMessage']
}
}
}, required({
Expand Down
28 changes: 24 additions & 4 deletions es/utils/aepp-wallet-communication/rpc/aepp-rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const RESPONSES = {
[METHODS.aepp.sign]: (instance) =>
(msg) => {
instance.rpcClient.processResponse(msg, ({ id, result }) => [result.signedTransaction || result.transactionHash])
},
[METHODS.aepp.signMessage]: (instance) =>
(msg) => {
instance.rpcClient.processResponse(msg, ({ id, result }) => [result.signature])
}
}

Expand Down Expand Up @@ -98,6 +102,7 @@ export const AeppRpc = Ae.compose({
if (typeof this[event] !== 'function') throw new Error(`Call-back for ${event} must be an function!`)
})
},
deepProps: { Ae: { defaults: { walletBroadcast: true } } },
methods: {
sign () {
},
Expand Down Expand Up @@ -183,6 +188,21 @@ export const AeppRpc = Ae.compose({
this.rpcClient.sendMessage(message(METHODS.aepp.sign, { ...opt, tx, returnSigned: true }))
)
},
/**
* Overwriting of `signMessage` AE method
* All sdk API which use it will be send notification to wallet and wait for callBack
* @function signMessage
* @instance
* @rtype (msg: String, options = {}) => Promise
* @return {Promise<String>} Signed transaction
*/
async signMessage (msg, opt = {}) {
if (!this.rpcClient || !this.rpcClient.connection.isConnected() || !this.rpcClient.isConnected()) throw new Error('You are not connected to Wallet')
if (!this.rpcClient.getCurrentAccount()) throw new Error('You do not subscribed for account.')
return this.rpcClient.addCallback(
this.rpcClient.sendMessage(message(METHODS.aepp.signMessage, { ...opt, message: msg }))
)
},
/**
* Send connection request to wallet
* @function sendConnectRequest
Expand All @@ -208,19 +228,19 @@ export const AeppRpc = Ae.compose({
* @rtype (tx: String, options = {}) => Promise
* @param {String} tx
* @param {Object} [options={}]
* @param {Object} [options.walletBroadcast={}]
* @param {Object} [options.walletBroadcast=true]
* @return {Promise<Object>} Transaction broadcast result
*/
async send (tx, options = { walletBroadcast: true }) {
async send (tx, options = {}) {
if (!this.rpcClient || !this.rpcClient.connection.isConnected() || !this.rpcClient.isConnected()) throw new Error('You are not connected to Wallet')
if (!this.rpcClient.getCurrentAccount()) throw new Error('You do not subscribed for account.')
const opt = R.merge(this.Ae.defaults, options)
if (!opt.walletBroadcast) {
const signed = await this.signTransaction(tx, opt)
const signed = await this.signTransaction(tx, { onAccount: opt.onAccount })
return this.sendTransaction(signed, opt)
}
return this.rpcClient.addCallback(
this.rpcClient.sendMessage(message(METHODS.aepp.sign, { ...opt, tx, returnSigned: false }))
this.rpcClient.sendMessage(message(METHODS.aepp.sign, { onAccount: opt.onAccount, tx, returnSigned: false }))
)
}
}
Expand Down
8 changes: 4 additions & 4 deletions es/utils/aepp-wallet-communication/rpc/rpc-clients.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,13 @@ export const RpcClient = stampit({
if (Object.prototype.hasOwnProperty.call(this.callbacks, action.id)) throw new Error('Action for this request already exist')
this.actions[action.id] = {
...action,
accept () {
accept (...args) {
removeAction(action.id)
r()
r(...args)
},
deny () {
deny (...args) {
removeAction(action.id)
j()
j(...args)
}
}
return this.actions[action.id]
Expand Down
27 changes: 23 additions & 4 deletions es/utils/aepp-wallet-communication/rpc/wallet-rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const REQUESTS = {
} catch (e) {
if (!returnSigned) {
// Validate transaction
const validationResult = await instance.unpackAndVerify(tx)
const validationResult = await instance.unpackAndVerify(rawTx || tx)
if (validationResult.validation.length) return sendResponseMessage(client)(id, method, { error: ERRORS.invalidTransaction(validationResult) })
// Send broadcast failed error to aepp
sendResponseMessage(client)(id, method, { error: ERRORS.broadcastFailde(e.message) })
Expand All @@ -132,6 +132,23 @@ const REQUESTS = {
const deny = (id) => (error) => sendResponseMessage(client)(id, method, { error: ERRORS.rejectedByUser(error) })

instance.onSign(client, client.addAction({ id, method, params: { tx, returnSigned, onAccount } }, [accept(id), deny(id)]))
},
[METHODS.aepp.signMessage]: (instance, { client }) =>
async ({ id, method, params: { message, onAccount } }) => {
// Authorization check
if (!client.isConnected()) return sendResponseMessage(client)(id, method, { error: ERRORS.notAuthorize() })

const accept = (id) => async () => sendResponseMessage(client)(
id,
method,
{
result: { signature: await instance.signMessage(message, { onAccount }) }
}
)

const deny = (id) => (error) => sendResponseMessage(client)(id, method, { error: ERRORS.rejectedByUser(error) })

instance.onMessageSign(client, client.addAction({ id, method, params: { message, onAccount } }, [accept(id), deny(id)]))
}
}

Expand All @@ -158,18 +175,20 @@ const handleMessage = (instance, id) => async (msg) => {
* @param {Function} onSubscription Call-back function for incoming AEPP account subscription (Second argument contain function for accept/deny request)
* @param {Function} onSign Call-back function for incoming AEPP sign request (Second argument contain function for accept/deny request)
* @param {Function} onAskAccounts Call-back function for incoming AEPP get address request (Second argument contain function for accept/deny request)
* @param {Function} onMessageSign Call-back function for incoming AEPP sign message request (Second argument contain function for accept/deny request)
* @param {Function} onDisconnect Call-back function for disconnect event
* @return {Object}
*/
export const WalletRpc = Ae.compose(Accounts, Selector, {
init ({ name, onConnection, onSubscription, onSign, onDisconnect, onAskAccounts }) {
const eventsHandlers = ['onConnection', 'onSubscription', 'onSign', 'onDisconnect']
init ({ name, onConnection, onSubscription, onSign, onDisconnect, onAskAccounts, onMessageSign }) {
const eventsHandlers = ['onConnection', 'onSubscription', 'onSign', 'onDisconnect', 'onMessageSign']
// CallBacks for events
this.onConnection = onConnection
this.onSubscription = onSubscription
this.onSign = onSign
this.onDisconnect = onDisconnect
this.onAskAccounts = onAskAccounts
this.onMessageSign = onMessageSign

eventsHandlers.forEach(event => {
if (typeof this[event] !== 'function') throw new Error(`Call-back for ${event} must be an function!`)
Expand Down Expand Up @@ -273,7 +292,7 @@ export const WalletRpc = Ae.compose(Accounts, Selector, {
getWalletInfo () {
const runtime = getBrowserAPI(true).runtime
return {
id: runtime ? runtime.id : this.id,
id: runtime && runtime.id ? runtime.id : this.id,
name: this.name,
networkId: this.getNetworkId(),
origin: window.location.origin,
Expand Down
4 changes: 3 additions & 1 deletion es/utils/aepp-wallet-communication/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const REQUESTS = asEnum([
'connect',
'subscribeAddress',
'sign',
'address'
'address',
'signMessage'
])

export const SUBSCRIPTION_VALUES = asEnum([
Expand All @@ -44,6 +45,7 @@ export const METHODS = {
[REQUESTS.address]: 'address.get',
[REQUESTS.connect]: 'connection.open',
[REQUESTS.sign]: 'transaction.sign',
[REQUESTS.signMessage]: 'message.sign',
[REQUESTS.subscribeAddress]: 'address.subscribe'
},
[NOTIFICATIONS.updateNetwork]: 'networkId.update',
Expand Down
4 changes: 1 addition & 3 deletions es/utils/amount-formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { BigNumber } from 'bignumber.js'

/**
* AE amount formats
* @type {{AE: string, AETTOS: string}}
*/
export const AE_AMOUNT_FORMATS = {
AE: 'ae',
Expand All @@ -36,8 +35,7 @@ export const AE_AMOUNT_FORMATS = {
}

/**
*
* @type {{[string]: number}}
* DENOMINATION_MAGNITUDE
*/
const DENOMINATION_MAGNITUDE = {
[AE_AMOUNT_FORMATS.AE]: 18,
Expand Down
6 changes: 3 additions & 3 deletions es/utils/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,18 +359,18 @@ export function prepareTx (signature, data) {
}

export function personalMessageToBinary (message) {
const p = Buffer.from('æternity Signed Message:\n', 'utf8')
const p = Buffer.from('aeternity Signed Message:\n', 'utf8')
const msg = Buffer.from(message, 'utf8')
if (msg.length >= 0xFD) throw new Error('message too long')
return Buffer.concat([Buffer.from([p.length]), p, Buffer.from([msg.length]), msg])
}

export function signPersonalMessage (message, privateKey) {
return sign(personalMessageToBinary(message), privateKey)
return sign(hash(personalMessageToBinary(message)), privateKey)
}

export function verifyPersonalMessage (str, signature, publicKey) {
return verify(personalMessageToBinary(str), signature, publicKey)
return verify(hash(personalMessageToBinary(str)), signature, publicKey)
}

/**
Expand Down
Loading

0 comments on commit bc739b4

Please sign in to comment.