Building Trust, and how we never see your Cryptocurrency Private Keys

Building trust with new products and services is difficult, but I'm hoping as the Product Manager for Signata I can help build that trust with you by trying to be as open as possible in how our product works. You might simply ask why we don't just make our code base open source and be done with it? Well, making a product open source isn't as simple as flipping the switch to "public" for our source code - there are several larger considerations that have to take place around access controls to the repositories, ensuring we're in a state that we can actually make it public, test and release processes, legal requirements, intellectual property, and more.

We aren't discounting the idea of open sourcing our products (or at least some parts of them), but for now we're just a start-up and we just don't have the resources available to make that transition safely without sacrificing effort on actually building our products.

So, for now, I'll at least give some in-depth information on how we provide assurance to you, dear reader, that we never see your cryptocurrency private keys.

So, what's a Private Key?

Most people who've dabbled in cryptocurrency will run into the idea of addresses - these are effectively the names of our wallets that we can share with one-another to send coins around.

An example of a LItecoin address

What that address actually is though is just a unique way of sharing a public key. When you start looking into the world of cryptography, you'll probably come across the concept of key pairs - these are public and private keys that are mathematically related to each other. A cryptocurrency address is just a form of Public Key, and if it's your own public key then you've likely got its corresponding Private Key as well. It's got the public part in it's name because you can safely share it around with the general public. The private part though is something you just keep to yourself - should you ever give anyone your private key, then they have the power to send your cryptocurrency wherever they want as if it was their own.

You can also think of it like internet banking - your address/public key is like your bank account number that you give out to people for them to send you money. The username and password you log in to your internet banking website with is like your private key. If you gave someone else your login details for your bank, then they'll be able to pretend to be you, and send your money wherever they like.

Just as a side note: a cryptographic "key" is just a really large number - basically a number that is so large and complex that it's next to impossible to guess.

Now that we've defined private keys, let's look at your YubiKeys.

How we set up your YubiKey

One of the core design decisions we made with Signata was to remain zero-knowledge about our customer's private keys. If we were to have any knowledge of them, then that would just open us up to becoming a huge target for hackers. Being zero-knowledge comes with a price though - should you, dear reader, ever forget your passwords for Signata, then we don't have any way to recover your data for you.

So before we look at how we protect your cryptocurrency Private Keys, we first need to look at how exactly we set up your YubiKeys with our system, as they're integral to how we protect your cryptocurrency Private Keys.

When you first add a YubiKey to Signata, you'll get asked for your Recovery Passphrase and PIN:

Your Recovery Passphrase is a 12 word mnemonic, which is converted into a "seed" - you set this up the first time you run the desktop application. We then convert that seed into a byte buffer, and then we make two ten thousand round PBKDF2 hashes of those bytes. The first hash is without any salt, and the second hash is with some salt that's saved in your account:

const seed = mnemonicToSeedSync(recoveryPassphrase).toString('hex');
const seedBuf = Buffer.from(seed, 'hex');
const h1 = forge.util.bytesToHex(forge.pkcs5.pbkdf2(seed, '', 10000, 512));
const h2 = forge.util.bytesToHex(forge.pkcs5.pbkdf2(h1, recoveryPassphraseSalt, 10000, 512));

The 2-step hashing process is designed to serve the purpose of letting us validate the first hash with some locally cached data that doesn't need to know the salt assigned to your account, and the second hash is stored in your Signata account so we can verify your hash on our back-end servers without you actually needing to send us your mnemonic. If someone managed to compromise our back-end servers and happened to be intercepting a check of your passphrase, then they couldn't actually see your mnemonic as they'd only see a PBKDF2 hash of it.

If the PBKDF2 hashes of your seed are correct for your account, then we move to the next step. We generate a new RSA2048 Encryption Key Pair for your YubiKey, and then we use AES128 encryption to encrypt it with the "seed" that we generated before. We then store that encrypted RSA Encryption Key Pair onto your account as a secure backup, and the RSA Key Pair is injected into the Encryption Key slot of your YubiKey.

We use RSA2048 because that's the maximum most smartcard like devices can handle, including YubiKeys. We also use AES128 as there are no known weaknesses with that algorithm, and it doesn't have export restrictions like AES256 has in some countries.

With your recovery passphrase you can, at any time, set up additional YubiKeys with the same Encryption Key installed in them if you want spare devices or you happen to lose your YubiKey.

Now, onto the cryptocurrency addresses themselves.

How we protect your Private Keys

With your YubiKey set up as above, we're then ready to start protecting your cryptocurrency addresses. Adding or Importing addresses works in exactly the same way, with the Import just relying on you providing the private key of the address instead of us creating a new one for you.

When an address is created in Signata, the very first thing that happens is we generate a session key:

const sessionKey = await generateSessionKey();
const sessionKeyBuf = Buffer.from(sessionKey.randomString, 'ascii');

Then we RSA encrypt that session key using your YubiKey:

const encryptedSessionKey = await newDevice.encryptData(sessionKey.randomString);

And then we create your new address and AES encrypt the WIF and Private Key using that session key:

const encryptedWif = await aesEncrypt(
    sessionKeyBuf,
    Buffer.from(privateKey.toWIF(), 'utf-8'),
);

So why don't we just RSA encrypt the WIF and Private Key with the YubiKey and skip that session key step? Well encrypting data with the RSA algorithm is not designed for encrypting large amounts of data. In fact if you try to, a lot of libraries and devices will actually just refuse to process it if it's too big, because it's just too slow and inefficient.

Instead, RSA is designed to work in conjunction with symmetric algorithms like AES - you use AES to encrypt data, and then you use RSA to encrypt the much smaller AES encryption key. You end up with the ability to encrypt large amounts of data quickly and securely, but also the added usefulness of asymmetric encryption with RSA.

If you're wondering how those session keys are generated - we actually do that in Python, generating them using your underlying Operating System random number generators.

So in the end we store in the Signata database 3 values. Your session key encrypted by the YubiKey, the cipher text encrypted by the session key, and some salt that was generated for the encryption process.

wif = {
    sessionKey: encryptedSessionKey.encryptedData,
    cipherText: encryptedWif.encryptedData,
    salt: encryptedWif.salt,
};

To get access to your Private Key again, we just reverse the process - we decrypt the session key using your YubiKey, and then we decrypt the cipher text with the session key and salt together.

How it all fits together

At a higher level, you can think of our zero-knowledge feature like this:

  1. Your recovery passphrase protects your YubiKeys. We have zero knowledge of your recovery passphrase.
  2. Your YubiKeys protect your cryptocurrency addresses.
  3. The Signata database never stores anything that isn't encrypted by you.

I apologise if this got a little too deep into the weeds - a lot of terminology and concepts were thrown in without explanation, but I wanted to make sure anyone who already understands cryptography would at least get the gist of what we're talking about. If you have any questions, or want anything better explained, just let me know in the comments below :)

Start protecting your cryptocurrency today by downloading Signata for free.

Timothy Quinn

Timothy Quinn

Managing Director of Congruent Labs