@dapperlabs/dappauth
Advanced tools
Comparing version 1.0.1 to 2.0.0
65
index.js
const Web3 = require('web3'); | ||
const ethUtil = require('ethereumjs-util'); | ||
const erc165 = require('./ABIs/ERC165'); | ||
const erc725Core = require('./ABIs/ERC725Core'); | ||
const ERC1271 = require('./ABIs/ERC1271'); | ||
const erc725CoreInterfaceID = '0xd202158d'; | ||
const erc725InterfaceID = '0xdc3d2a7b'; | ||
const erc725ActionPurpose = 2; | ||
// bytes4(keccak256("isValidSignature(bytes32,bytes)") | ||
const ERC1271_MAGIC_VALUE = '0x1626ba7e'; | ||
@@ -15,3 +13,3 @@ module.exports = class DappAuth { | ||
async isSignerActionableOnAddress(challenge, signature, address) { | ||
async isAuthorizedSigner(challenge, signature, address) { | ||
const challengeHash = ethUtil.hashPersonalMessage( | ||
@@ -22,9 +20,4 @@ ethUtil.toBuffer(challenge), | ||
// Get the address of whoever signed this message | ||
const signatureParams = ethUtil.fromRpcSig(signature); | ||
const recoveredKey = ethUtil.ecrecover( | ||
challengeHash, | ||
signatureParams.v, | ||
signatureParams.r, | ||
signatureParams.s, | ||
); | ||
const { v, r, s } = ethUtil.fromRpcSig(signature); | ||
const recoveredKey = ethUtil.ecrecover(challengeHash, v, r, s); | ||
const recoveredAddress = ethUtil.publicToAddress(recoveredKey); | ||
@@ -41,48 +34,10 @@ | ||
// try smart-contract wallet | ||
const isSupportedContract = await this.isSupportedContract(address); | ||
if (!isSupportedContract) { | ||
return false; | ||
} | ||
const erc1271CoreContract = new this.web3.eth.Contract(ERC1271, address); | ||
const keyHasActionPurpose = await this.keyHasActionPurpose( | ||
address, | ||
recoveredKey, | ||
); | ||
return keyHasActionPurpose; | ||
} | ||
async keyHasActionPurpose(contractAddr, key) { | ||
const erc725CoreContract = new this.web3.eth.Contract( | ||
erc725Core, | ||
contractAddr, | ||
); | ||
const keyHash = ethUtil.keccak(key); | ||
return erc725CoreContract.methods | ||
.keyHasPurpose(keyHash, erc725ActionPurpose) | ||
const magicValue = await erc1271CoreContract.methods | ||
.isValidSignature(ethUtil.keccak(challenge), signature) // we send just a regular hash, which then the smart contract hashes ontop to an erc191 hash | ||
.call(); | ||
} | ||
async isSupportedContract(contractAddr) { | ||
const erc165Contract = new this.web3.eth.Contract(erc165, contractAddr); | ||
const isSupportsERC725CoreInterface = await erc165Contract.methods | ||
.supportsInterface(erc725CoreInterfaceID) | ||
.call(); | ||
if (isSupportsERC725CoreInterface) { | ||
return true; | ||
} | ||
const isSupportsERC725Interface = await erc165Contract.methods | ||
.supportsInterface(erc725InterfaceID) | ||
.call(); | ||
if (isSupportsERC725Interface) { | ||
return true; | ||
} | ||
return false; | ||
return magicValue === ERC1271_MAGIC_VALUE; | ||
} | ||
}; |
{ | ||
"name": "@dapperlabs/dappauth", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"description": "A util to prove actionable control ('ownership') over a public Ethereum address using eth_sign", | ||
@@ -23,3 +23,5 @@ "keywords": [ | ||
"ethereumjs-util": "^6.0.0", | ||
"web3": "^1.0.0-beta.36" | ||
"web3": "^1.0.0-beta.36", | ||
"ethereumjs-abi": "^0.6.6", | ||
"safe-buffer": "^5.1.2" | ||
}, | ||
@@ -26,0 +28,0 @@ "devDependencies": { |
const ethUtil = require('ethereumjs-util'); | ||
const ethAbi = require('ethereumjs-abi'); | ||
const Buffer = require('safe-buffer').Buffer; | ||
const utils = require('./utils'); | ||
const erc725CoreInterfaceID = '0xd202158d'; | ||
const erc725InterfaceID = '0xdc3d2a7b'; | ||
// bytes4(keccak256("isValidSignature(bytes32,bytes)") | ||
const ERC1271_METHOD_SIG = '1626ba7e'; | ||
module.exports = class MockContract { | ||
constructor(options) { | ||
this.isSupportsERC725CoreInterface = options.isSupportsERC725CoreInterface; | ||
this.isSupportsERC725Interface = options.isSupportsERC725Interface; | ||
this.actionableKey = options.actionableKey; | ||
this.errorOnIsSupportedContract = options.errorOnIsSupportedContract; | ||
this.errorOnKeyHasPurpose = options.errorOnKeyHasPurpose; | ||
this.authorizedKey = options.authorizedKey; | ||
this.address = options.address; | ||
this.errorIsValidSignature = options.errorIsValidSignature; | ||
} | ||
static _true() { | ||
return '0x0000000000000000000000000000000000000000000000000000000000000001'; | ||
return `0x${ERC1271_METHOD_SIG}00000000000000000000000000000000000000000000000000000000`; // a.k.a the "magic value". | ||
} | ||
@@ -23,8 +24,14 @@ | ||
// @param {String} methodCall | ||
// @param {String} methodParams | ||
// @return {String} | ||
run(methodCall, methodParams) { | ||
switch (methodCall) { | ||
case '01ffc9a7': | ||
return this._01ffc9a7(`0x${methodParams.substring(0, 4 * 2)}`); | ||
case 'd202158d': | ||
return this._d202158d(`0x${methodParams.substring(0, 32 * 2)}`); | ||
case ERC1271_METHOD_SIG: | ||
const [hash, signature] = ethAbi.rawDecode( | ||
['bytes32', 'bytes'], | ||
Buffer.from(methodParams, 'hex'), | ||
); | ||
return this._1626ba7e(hash, signature); | ||
default: | ||
@@ -35,29 +42,20 @@ throw new Error(`Unexpected method ${methodCall}`); | ||
// "isSupportedContract" method call | ||
_01ffc9a7(interfaceID) { | ||
if (this.errorOnIsSupportedContract) { | ||
throw new Error('isSupportedContract call returned an error'); | ||
// "isValidSignature" method call | ||
// @param {Buffer} hash | ||
// @param {Buffer} signature | ||
// @return {String} | ||
_1626ba7e(hash, signature) { | ||
if (this.errorIsValidSignature) { | ||
throw new Error('isValidSignature call returned an error'); | ||
} | ||
if ( | ||
this.isSupportsERC725CoreInterface && | ||
interfaceID === erc725CoreInterfaceID | ||
) { | ||
return MockContract._true(); | ||
} | ||
// Get the address of whoever signed this message | ||
const { v, r, s } = ethUtil.fromRpcSig(signature); | ||
const erc191MessageHash = utils.erc191MessageHash(hash, this.address); | ||
const recoveredKey = ethUtil.ecrecover(erc191MessageHash, v, r, s); | ||
const recoveredAddress = ethUtil.publicToAddress(recoveredKey); | ||
if (this.isSupportsERC725Interface && interfaceID === erc725InterfaceID) { | ||
return MockContract._true(); | ||
} | ||
const expectedAddress = ethUtil.publicToAddress(this.authorizedKey); | ||
return MockContract._false(); | ||
} | ||
// "keyHasPurpose" method call | ||
_d202158d(key) { | ||
if (this.errorOnKeyHasPurpose) { | ||
throw new Error('keyHasPurpose call returned an error'); | ||
} | ||
if (key === ethUtil.bufferToHex(ethUtil.keccak(this.actionableKey))) { | ||
if (recoveredAddress.toString() === expectedAddress.toString()) { | ||
return MockContract._true(); | ||
@@ -64,0 +62,0 @@ } |
198
test/test.js
const ethUtil = require('ethereumjs-util'); | ||
const crypto = require('crypto'); | ||
const assert = require('assert'); | ||
@@ -7,25 +6,24 @@ const DappAuth = require('..'); | ||
const ContractMock = require('./contract-mock'); | ||
const utils = require('./utils'); | ||
describe('dappauth', function() { | ||
const keyA = generateRandomKey(); | ||
const keyB = generateRandomKey(); | ||
const keyC = generateRandomKey(); | ||
const keyA = utils.generateRandomKey(); | ||
const keyB = utils.generateRandomKey(); | ||
const keyC = utils.generateRandomKey(); | ||
const testCases = [ | ||
{ | ||
title: | ||
'Direct-keyed wallets should have actionable control over their address', | ||
title: 'External wallets should be authorized signers over their address', | ||
isEOA: true, | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyA, | ||
authAddr: keyToAddress(keyA), | ||
authAddr: utils.keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: false, | ||
isSupportsERC725Interface: false, | ||
actionableKey: null, | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
authorizedKey: null, | ||
address: null, | ||
errorIsValidSignature: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: true, | ||
expectedAuthorizedSignerError: false, | ||
expectedAuthorizedSigner: true, | ||
}, | ||
@@ -35,136 +33,79 @@ | ||
title: | ||
'Direct-keyed wallets should NOT have actionable control when signing the wrong challenge', | ||
'External wallets should NOT be authorized signers when signing the wrong challenge', | ||
isEOA: true, | ||
challenge: 'foo', | ||
challengeSign: 'bar', | ||
signingKey: keyA, | ||
authAddr: keyToAddress(keyA), | ||
authAddr: utils.keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: false, | ||
isSupportsERC725Interface: false, | ||
actionableKey: null, | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
authorizedKey: ethUtil.privateToPublic(keyC), | ||
address: utils.keyToAddress(keyA), | ||
errorIsValidSignature: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: false, | ||
expectedAuthorizedSignerError: false, | ||
expectedAuthorizedSigner: false, | ||
}, | ||
{ | ||
title: | ||
'Direct-keyed wallets should NOT have actionable control over OTHER addresses', | ||
'External wallets should NOT be authorized signers over OTHER addresses', | ||
isEOA: true, | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyA, | ||
authAddr: keyToAddress(keyB), | ||
authAddr: utils.keyToAddress(keyB), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: false, | ||
isSupportsERC725Interface: false, | ||
actionableKey: null, | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
authorizedKey: ethUtil.privateToPublic(keyC), | ||
address: utils.keyToAddress(keyB), | ||
errorIsValidSignature: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: false, | ||
expectedAuthorizedSignerError: false, | ||
expectedAuthorizedSigner: false, | ||
}, | ||
{ | ||
title: | ||
'Smart-contract wallets with support for ERC725Core interface and action key should have actionable control over their address', | ||
'Smart-contract wallets with a 1-of-1 correct internal key should be authorized signers over their address', | ||
isEOA: false, | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyB, | ||
authAddr: keyToAddress(keyA), | ||
authAddr: utils.keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: true, | ||
isSupportsERC725Interface: false, | ||
actionableKey: ethUtil.privateToPublic(keyB), | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
authorizedKey: ethUtil.privateToPublic(keyB), | ||
address: utils.keyToAddress(keyA), | ||
errorIsValidSignature: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: true, | ||
expectedAuthorizedSignerError: false, | ||
expectedAuthorizedSigner: true, | ||
}, | ||
{ | ||
title: | ||
'Smart-contract wallets with support for ERC725 interface and action key should have actionable control over their address', | ||
'Smart-contract wallets with a 1-of-1 incorrect internal key should NOT be authorized signers over their address', | ||
isEOA: false, | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyB, | ||
authAddr: keyToAddress(keyA), | ||
authAddr: utils.keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: false, | ||
isSupportsERC725Interface: true, | ||
actionableKey: ethUtil.privateToPublic(keyB), | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
authorizedKey: ethUtil.privateToPublic(keyC), | ||
address: utils.keyToAddress(keyA), | ||
errorIsValidSignature: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: true, | ||
expectedAuthorizedSignerError: false, | ||
expectedAuthorizedSigner: false, | ||
}, | ||
{ | ||
title: | ||
'Smart-contract wallets WITHOUT support for ERC725/ERC725Core interface and action key should NOT have actionable control over their address', | ||
title: 'isAuthorizedSigner should error when smart-contract call errors', | ||
isEOA: false, | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyB, | ||
authAddr: keyToAddress(keyA), | ||
authAddr: utils.keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: false, | ||
isSupportsERC725Interface: false, | ||
actionableKey: ethUtil.privateToPublic(keyB), | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
authorizedKey: ethUtil.privateToPublic(keyB), | ||
address: utils.keyToAddress(keyA), | ||
errorIsValidSignature: true, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: false, | ||
expectedAuthorizedSignerError: true, | ||
expectedAuthorizedSigner: false, | ||
}, | ||
{ | ||
title: | ||
'Smart-contract wallets with support for ERC725 interface and incorrect action key should have NO actionable control over their address', | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyB, | ||
authAddr: keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: false, | ||
isSupportsERC725Interface: true, | ||
actionableKey: ethUtil.privateToPublic(keyC), | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: false, | ||
expectedIsSignerActionableOnAddress: false, | ||
}, | ||
{ | ||
title: | ||
'isSignerActionableOnAddress should error when smart-contract call errors', | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyB, | ||
authAddr: keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: true, | ||
isSupportsERC725Interface: false, | ||
actionableKey: ethUtil.privateToPublic(keyB), | ||
errorOnIsSupportedContract: true, | ||
errorOnKeyHasPurpose: false, | ||
}, | ||
expectedIsSignerActionableOnAddressError: true, | ||
expectedIsSignerActionableOnAddress: false, | ||
}, | ||
{ | ||
title: | ||
'isSignerActionableOnAddress should error when smart-contract call errors', | ||
challenge: 'foo', | ||
challengeSign: 'foo', | ||
signingKey: keyB, | ||
authAddr: keyToAddress(keyA), | ||
mockContract: { | ||
isSupportsERC725CoreInterface: true, | ||
isSupportsERC725Interface: false, | ||
actionableKey: ethUtil.privateToPublic(keyB), | ||
errorOnIsSupportedContract: false, | ||
errorOnKeyHasPurpose: true, | ||
}, | ||
expectedIsSignerActionableOnAddressError: true, | ||
expectedIsSignerActionableOnAddress: false, | ||
}, | ||
]; | ||
@@ -178,11 +119,16 @@ | ||
const signature = signPersonalMessage( | ||
const signatureFunc = test.isEOA | ||
? utils.signEOAPersonalMessage | ||
: utils.signERC1654PersonalMessage; | ||
const signature = signatureFunc( | ||
test.challengeSign, | ||
test.signingKey, | ||
test.authAddr, | ||
); | ||
let isError = false; | ||
let isSignerActionableOnAddress = false; | ||
let isAuthorizedSigner = false; | ||
try { | ||
isSignerActionableOnAddress = await dappAuth.isSignerActionableOnAddress( | ||
isAuthorizedSigner = await dappAuth.isAuthorizedSigner( | ||
test.challenge, | ||
@@ -196,26 +142,6 @@ signature, | ||
assert.equal(isError, test.expectedIsSignerActionableOnAddressError); | ||
if (!isError) { | ||
assert.equal( | ||
isSignerActionableOnAddress, | ||
test.expectedIsSignerActionableOnAddress, | ||
); | ||
} | ||
assert.equal(isError, test.expectedAuthorizedSignerError); | ||
assert.equal(isAuthorizedSigner, test.expectedAuthorizedSigner); | ||
}), | ||
); | ||
}); | ||
function generateRandomKey() { | ||
return ethUtil.toBuffer(`0x${crypto.randomBytes(32).toString('hex')}`); | ||
} | ||
function signPersonalMessage(message, key) { | ||
const messageHash = ethUtil.hashPersonalMessage(ethUtil.toBuffer(message)); | ||
const signature = ethUtil.ecsign(messageHash, key); | ||
return ethUtil.toRpcSig(signature.v, signature.r, signature.s); | ||
} | ||
function keyToAddress(key) { | ||
return ethUtil.bufferToHex(ethUtil.privateToAddress(key)); | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
163408
4
313
1
+ Addedethereumjs-abi@^0.6.6
+ Addedsafe-buffer@^5.1.2
+ Addedethereumjs-abi@0.6.8(transitive)