How we create and handle the non-custodial in-app 2key Wallet

E L
2key
Published in
4 min readOct 7, 2019

Users on 2key have both 2nd layer keys and 1st layer keys, which allow them to access and perform transactions on the public 1st layer blockchain (Ethereum), as well as the 2nd layer 2key Protocol. The 1st layer keys are non-custodial, and users can choose between linking their existing MM keys, Ledger keys or generating a new 2key in-app keypair.

Who’s holding your money? (Photo by Sabine Peters on Unsplash)

For the 2key in-app option only, it’s a keypair created using a Hierarchical Deterministic (HD) wallet (https://medium.com/bitcraft/hd-wallets-explained-from-high-level-to-nuts-and-bolts-9a41545f5b0). This allows us to avoid storing any sensitive data for this wallet in our centralized persistence layers (e.g. DBs, indexes etc..). The HD wallet’s main instance is a privateKey, which allows the user to operate with a unique web3 public address and sign transactions on this address’s behalf.

To make wallet iterations more friendly we encrypt the private key with AES-256 (256 bit encryption key), using https://cryptojs.gitbook.io/docs/#ciphers, with the user’s non-custodial passphrase used for normalising and sealing the 256 bit encryption key. We use PBKDF2 , a password-based key derivation function. In many applications of cryptography, user security is ultimately dependent on a password, and because a password usually can’t be used directly as a cryptographic key, some processing is required. A salt provides a large set of keys for any given password, and an iteration count increases the cost of producing keys from a password, thereby also increasing the difficulty of attack. So long story short we use the hash of the user’s passphrase salted by a super phrase, and utilising this hash along with a random AES IV vector we generate per encryption event we encrypt the private key with AES 256 bit encryption. This enables the user to safely encrypt and decrypt his private key with a relatively short and memorable passphrase (it’s important for the non-custodial aspects that this is memorable, allowing the user to store it only in his head).

This encrypted pk thus cannot be decrypted by anyone but the user as only the user holds the passphrase in his head, which makes this encryptedPK effectively a special kind of public key. This enables us to store this encryption as a service for the user and to allow the user to use his privateKey in different browsers/computers without copying it or sending it online between entities, and without requiring to copy it to and from flash or other storage, and without exposing it in the browser. The decrypted private key is only stored in the specific browser session hot memory that the user is actively participating in, and purged immediately as the session closes, or under some attack vectors/constraints.

To add some fallback for cases where the user might lose his passphrase we generate our 2key wallet with a master 12 words mnemonic — we might up it to 24, but that’s a UX burden.. This mnemonic is also not stored anywhere except by the user himself — in this regard we ask, as well as for the passphrase, that the user copy and write it down somewhere on paper or somewhere safe, and warn that this is the user’s responsibility to save this recovery words.To generate the HD mnemonic and to validate the mnemonic and to generate the seed from mnemonic we use bip39 lib (https://www.npmjs.com/package/bip39)
To create the wallet instance from the mnemonic and get privateKey we use ethereumjs-wallet lib (https://www.npmjs.com/package/ethereumjs-wallet)
To encrypt/decrypt privateKey for cross-device cross-browser interoperability we use crypto-js library (https://www.npmjs.com/package/crypto-js)After we get on the frontend (the client) a safely decrypted privateKey we create a custom hookedProvider based on https://www.npmjs.com/package/web3-provider-engine lib that exposes methods for signing messages and transactions. Then we create a new instance of provider engine (provider) that accepts custom subproviders. In our case we use our hookedProvider as a first (this is important) subprovider (the main idea that this hookedProvider catches and handles all operations related to signing). After hookedProvider we add a regular RPC or WS (websockets) provider to communicate with our nodes (our in-house developed dynamically scalable, kubernetes based ethereum node clusters). Also this provider engine allows us to add another custom providers (like nonce, filter, debug etc). And finally when we have our custom provider engine we create web3js v0.20.7 (https://www.npmjs.com/package/web3/v/0.20.7) instance and pass our provider as an argument. We’ve slated to migrate to web3v1.2.1, which will hopefully occur next month.

As for the 2nd layer keypair, the constraints there are a bit different, as well as the methodologies. But the core principles for the keypair creation are similar. In the 2nd layer case, there are a lot more complex bindings that require multi-sigs between the humanisation moderators, social id moderators, non-custodial master (the user), and the custodial 2nd layer key-keeper that occur, but that would take another few pages to describe in words, and is out of scope of this explanation.. In any case, hope this sheds some light on how we handle the creation of 2key in-app non-custodial wallets.

--

--