Hedgehog is alternative to Metamask that manages a user's private key and wallet on the browser. It exposes a simple API to allow you to create an authentication scheme to let users sign up and login to their wallet across multiple browsers and devices. hedgehog.audius.co.
Why Use This? Not All Transactions Are Created Equal! Currently available wallets treat every transaction as if it were moving around your life’s savings. Hedgehog was built for use-cases involving low-to-no financial value.
Installation
Hedgehog is available as an npm package.
npm install --save @audius/hedgehog
Overview
The following sections are a technical overview of how Hedgehog works, for coded examples, skip to the How To section.
How It Works
Hedgehog is a package that lives in your front end application to create and manage a user's entropy (from which a private key is derived). It allows your application to interact with a REST API on a server and database of your choice to securely persist and retrieve auth artifacts. Hedgehog relies on username and password to create auth artifacts, so it's able to simulate a familiar authentication system that allows users to sign up or login from multiple browsers or devices and retrieve their entropy.
Since Hedgehog interacts with a REST API, it requires that you run a server or database, or use a managed solution, and conform to the API specified in the How To section below.
Hedgehog generates a set of artifacts similar to a MyEtherWallet keystore file. Those artifacts can then be persisted to a database of your choice and retrieved with a hash computed from the username, password and an initialization vector. The private key is only computed and available client side and is never transmitted or stored anywhere besides the user's browser.
Wallet Creation
Wallets are created by first generating a wallet seed and entropy as per the BIP-39 spec. The entropy can then be used to derive a hierarchical deterministic wallet given a path, as stated in the BIP-32 spec. This entropy is stored in the browser's localStorage to allow user state to persist across multiple sessions without any external dependency. Using this entropy, a wallet object from ethereumjs-wallet
is generated and stored in the wallet
property within the Hedgehog class on initialization, enabling state persistence.
In addition to the entropy, Hedgehog generates an initialization vector(iv
), lookupKey
and cipherText
. These three values can be securely stored in a database and retrieved from a server to authenticate a user. The iv
is a random hex string generated for each user to secure authentication. The lookupKey
is the username and password combined with a pre-defined, constant, initialization vector(not the same iv
that's stored in the database). This lookupKey
acts as the primary key in the database to retrieve the cipherText
and iv
values. The cipherText
is generated using an aes-256-cbc cipher with the iv
and a key derived from a combination of the user's password and the iv using scrypt and stores the entropy.
Since entropy is stored in the cipherText
, it can be derived from there if we know the iv
and key(scrypt of user's password and iv
). After the entropy is decrypted, it's stored in the browser on a local ethereumjs-wallet
object as well as in localStorage. The encryption and decryption process happens exclusively on the client side with the user's password or entropy never leaving the browser without first being encrypted.
For API of functions to access and modify wallet state, please see the API section
Wallet Persistence
The wallet information can be persisted on the backend of your choice. You, as the developer, have the choice to pick which language and frameworks to use, write the endpoints to suit any custom logic necessary and select a hosting provider (if any).
The database schema for persisting data should resemble the following example. There two tables, one for storing authentication information, and the other for storing username and walletAddress. It's important that the username is not stored in the Authentications table because the lookupKey
is a scrypt hash of a predefined iv with an username and password combination. If the data in these tables were ever exposed, susceptibility of a rainbow table attack could increase because the password is the only unknown property. These tables can be named anything since Hedgehog only interacts with REST API endpoints that will perform CRUD on these tables.
Security Considerations
All third party javascript should be audited for localStorage access. One possible attack vector is a script that loops through all localStorage keys and sends them to a third party server from where those keys could be used to sign transactions on behalf of malicious actors. To mitigate this, all third party javascript should be audited and stored locally to serve, instead of being loaded dynamically through scripts.
Username should be stored separately from auth artifacts in different tables. The table containing the authentication values should be independent with no relation to the table storing username
If the application developers’ server is seized, breached, or controlled by bad actors, the resources required to brute-force decrypt the auth artifacts stored there would be immense. It would only make sense to expend those resources if there were enough value to be gained by breaking a given account, which is why we only recommend using Hedgehog in cases where the stakes are lower. This is also why we recommend a bridge approach for certain use-cases, where one could start users on Hedgehog and suggest migrating to a more secure wallet if their stored value increases beyond a certain threshold. We are working on fallback mechanisms to enable key sharing between devices in the absence of this server component, eg. QR codes.
For more deployment best practices please see this section
IMPORTANT: Lost Passwords
If a user loses their password, the account is no longer recoverable. There's no way to reset a password because the entropy is encrypted client side before it's sent to the database. And since the old password is required to decrypt the entropy and re-encrypt with a new password, if the password used to encrypt the entropy has been lost or forgotten, the account is not recoverable.
How To
The code below shows code snippets to integrate Hedgehog into your own application. For more information about setting up a database schema, see the example schema section, and for a fully working end-to-end demo with a custom backend (Firebase or Express), see the demo repo.
Client-side setup
In this example, we assume you have already a backend/database set up to handle the following scenarios:
POST /authentication
param | type |
---|---|
iv |
string |
cipherText |
string |
lookupKey |
string |
Inserts iv
, cipherText
, and lookupKey
into the authentication table.
GET /authentication
param | type |
---|---|
lookupKey |
string |
Retrieve one record with lookupKey
from the authentication table.
POST /users
param | type |
---|---|
username |
string |
walletAddress |
string |
Inserts username
and walletAddress
into a users table.
const { Hedgehog, /*WalletManager, Authentication */ } = require('@audius/hedgehog')
const axios = require('axios')
const makeRequestToService = async (axiosRequestObj) => {
axiosRequestObj.baseURL = 'http://hedgehog.base-url.com'
try {
const resp = await axios(axiosRequestObj)
if (resp.status === 200) {
return resp.data
} else {
throw new Error(`Server returned error: ${resp.status.toString()} ${resp.data['error']}`)
}
} catch (e) {
console.error(e)
throw new Error(`Server returned error: ${e.response.status.toString()} ${e.response.data['error']}`)
}
}
Next,
Import Hedgehog and necessary dependencies. Hedgehog is the package export that should be used by most users. WalletManager and Authentication imports are possible but not recommended and should only be used by advanced users.
makeRequestToService
This is a helper function that makes XHR requests to a server of your choosing and parses the response status code. This is simply a helper to make defining our setters and getters easier.
The Hedgehog constructor requires 3 parameters to set and retrieve data from your backend. These are:
setAuthFn
Responsible for setting values into the authentication table on the backend.
On the backend, the /authentication
route will need to insert an entry into the authentication table containing the iv, cipherText, and lookupKey.
/**
* @param {Object} obj contains {iv, cipherText, lookupKey}
*/
const setAuthFn = async (obj) => {
await makeRequestToService({
url: '/authentication',
method: 'post',
data: obj
})
}
setUserFn
Similarly to setAuthFn, the setUserFn is used to relay user information to our custom backend users table.
On the backend, the /user
route will need to insert an entry into the users table containing the walletAddress and username.
/**
* @param {Object} obj contains {walletAddress, username}
*/
const setUserFn = async (obj) => {
await makeRequestToService({
url: '/user',
method: 'post',
data: obj
})
}
getFn
The getFn used to retrieve a record from the authentication table.
/**
* @param {Object} obj contains {lookupKey}
*/
const getFn = async (obj) => {
return makeRequestToService({
url: '/authentication',
method: 'get',
params: obj
})
}
After setting up these methods, we're good to go and ready let users create accounts & sign in!
Usage
const hedgehog = new Hedgehog(getFn, setAuthFn, setUserFn)
// wallet is an `ethereumjs-wallet` object that can be used to sign transactions
let wallet = null
try {
if (hedgehog.isLoggedIn()) {
wallet = hedgehog.getWallet()
}
else {
// Prompt user for username/password input for login or signup
wallet = await hedgehog.login('username', 'password')
// or
wallet = await hedgehog.signUp('username', 'password')
}
}
catch(e) {
console.error(e)
}
Here, we:
- Construct a new Hedgehog instance using our getFn, setAuthFn, and setUserFn.
- Create a variable to store a wallet
- Check if a user is logged in and set their wallet accordingly
- If not, we can either log in a user with their credentials or sign up for a new account
Example: SQL Schema
There are two tables that should be used to persist hedgehog authentication and user information. The names of the tables and columns can be customized. For a full working example of a server and SQL schema, see the Hedgehog demo repo.
Authentications
This table stores auth information like iv
and cipherText
and also the lookupKey
, which should serve as the primary key for this table since it's sent from the browser to request an auth record.
The values and explanation for fields in the Authentications table (iv
, cipherText
and lookupKey
) are given in the Wallet creation section.
An example of the Authentications table with example data:
iv | cipherText | lookupKey |
---|---|---|
c9b3...48 | 07...e561 | 0e...2a8 |
d6...355 | 059f...561 | 15e...3c0 |
99...6e | f4...07 | 18...10 |
Users
This table can store information about users. The the two default fields hedgehog returns in the setUserFn
call are username
and walletAddress
. username
should serve as the primary key for the table.
Next Steps
After setting up Hedgehog, in order to fund wallets so that transactions can be waived on behalf of the user, see Funding Hedgehog Accounts as well as other best practices.
Funding Hedgehog Accounts
Since Hedgehog creates and manages wallets client side, just like Metamask, the problem of funding a wallet still exists. When performing only reads from a blockchain, there's usually no transaction fee. However, write transactions typically require fees, and the onus is on the transaction sender to pay these fees.
This is less than ideal for an end user facing product since users will be required to pay when submitting transactions - without a technology or cryptocurrency background, self-funding wallets is an unrealistic requirement.
There are two ways to try to solve this problem: funding user wallets or using EIP-712 relay transactions.
Fund User Wallets
As part of the endpoint which persists the walletAddress, you can fund any new walletAddress
's created. When a new wallet is created, you could send a small amount of tokens to that address so the user can sign and send transactions to the chain browser side. The downside is there could be potential for abuse where someone farms accounts to collect tokens because these accounts would be funded directly.
EIP-712 Relay Transactions
Another option is to have users sign their transactions browser side, but relay their transaction through an EIP-712 relayer which submits their transaction to chain. Any transaction costs incurred would be paid by the relayer instead of the user, however the original user transaction data is preserved and submitted.
For more information about EIP-712, please see the following links:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
https://medium.com/metamask/eip712-is-coming-what-to-expect-and-how-to-use-it-bb92fd1a7a26
Best practices
Password Strength
It's recommended to enforce password standards client side to reject any insecure passwords. Two recommendations to increase password strength are to enforce a minimum character limit and use a bloom filter to reject commonly used passwords like this npm module from Mozilla
Rate limiting
The server endpoint that is called by getFn should be rate limited to prevent brute force attacks
Javascript security
All client side code should be audited for localStorage since the entropy resides in localStorage. Please see the security considerations section for more information
API
The functions exposed via hedgehog are:
signUp
/**
* @param {String} username username
* @param {String} password user password
* @returns {Object} ethereumjs-wallet wallet object
*/
async signUp (username, password)
Example
const wallet = await hedgehog.signUp('username', 'password')
Given user credentials, create a client side wallet and all other authentication artifacts, call setFn to persist the artifacts to a server and return the wallet object
login
/**
* @param {String} username username
* @param {String} password user password
* @returns {Object} ethereumjs-wallet wallet object
*/
async login (username, password)
Example
const wallet = await hedgehog.login('username', 'password')
Given user credentials, attempt to get authentication artifacts from server using getFn, create the private key using the artifacts and the user password
logout
logout ()
Example
hedgehog.logout()
Deletes the local client side wallet including entropy and all associated authentication artifacts
isLoggedIn
/**
* @returns {Boolean} true if the user has a client side wallet, false otherwise
*/
isLoggedIn ()
Example
if (hedgehog.isLoggedIn()) { /* show user account */ }
Returns is the user has a client side wallet. If they do, calls can be made against that wallet, if they don't the user has to login or signup.
getWallet
/**
* @returns {Object} ethereumjs-wallet wallet object if a wallet exists, otherwise null
*/
getWallet ()
Example
const wallet = hedgehog.getWallet()
Returns the current user wallet.
restoreLocalWallet
/**
* @returns {Object/null} If the user has a wallet client side, the wallet object is returned,
* otherwise null is returned
*/
restoreLocalWallet ()
If a user refreshes or navigates away from the page and comes back later, this attempts to restore the client side wallet from localStorage, if it exists.
createWallet
/**
* @param {String} password user password
* @returns {Object} ethereumjs-wallet wallet object
*/
async createWallet (password)
Create a new client side wallet object without going through the signup flow. This is useful if you need a temporary, read-only wallet that is ephemeral and does not need to be persisted.
Code Organization
The Hedgehog package is organized into several files with varying levels of control.
- index.js - default exports for the npm module, exports each of the src/ modules below
- src/hedgehog.js - main constructor with primary consumable public facing API and high level functions
- src/walletManager.js - wallet management logic including localStorage, and end to end authentication functionality
- src/authentication.js - low level authentication functions (eg create iv, encrypt hash etc)