Recently, we here at the SDF launched the second version of our wallet server, a faster and more secure solution for encrypting and storing your wallets. I want to talk to you about the technical details behind why we have switched to version two (v2) so soon after launch and some of the design decisions we made while developing it. But before we get there, let me first tell you about what the wallet server even is.
Securing your Keys
Stellar, like every cryptocurrency that I am familiar with, is reliant upon public-key cryptography; you post transactions signed by your secret key that others can verify with your public key. The rub is that your private key is long by human standards, too long to remember. In stellar, we use ed25519 to sign transactions, meaning that the private key seed is 32 bytes long.
Since your private key is so long, we would prefer to store it somewhere so that you don’t have to type it into the Stellar client every time you want to make a payment. The wallet server is that storage. Only you should be given access to your secret key, so that means that we need to devise a storage system where the SDF cannot access your secret key — and even more importantly — an attacker (someone who wants to steal your money) cannot access your secret key.
To accomplish this, both versions of our wallet server use this basic security design: encrypt your stellar wallet so that only you can decode it, and furthermore protect access to that encrypted wallet so that attackers cannot perform an offline dictionary attack. In both versions of our wallet server, your wallet is encrypted using AES with a 256-bit key in GCM Mode, so we will leave that aside for now.
The interesting bits of both designs are in how we protect access to your encrypted wallet and in how we derive the AES key to unlock your wallet. In this post, we’ll look at the former. Let’s look at the design of both wallet version one and version two in turn.
Wallet v1: scrypt(u+p, u+p)
Don’t worry if the equation above is gibberish now; we’ll get to that in time. Before we do, I should talk about one curious design goal we had for protecting access to the encrypted wallet in v1 that is absent in wallet v2 (reasons why will be explained below). In v1, we wanted to anonymize encrypted wallets such that only someone who knew both the username and the password for an account could find the encrypted wallet even if they had complete access to our database. This was to prevent an attacker from specifically targeting a high balance account, which is pretty trivial to find since the stellar ledger is public.
To accomplish this, we did not store usernames in the v1 tables. Instead, we stored encrypted wallets in a slot that was looked up by a value derived from both the username and password which we called the “Wallet ID”. Before we can talk about what goes into a wallet id, we should first discuss some basic cryptographic principles.
Hashing, Key Stretching and You
In cryptography, hashing a value is a central and recurring concept. In very simplified terms, hashing (amongst other things) can be used to verify a secret without storing it. This is especially useful when applied to a password: a server can keep a hash of your password which it can use to verify you know the password, but the server itself does not know your password. This is good, because in fact there are numerous instances of passwords being stolen because the service provider stored passwords in plaintext.
So, we want to hash your password so that it cannot be stolen, but it turns out hashing is not enough! As described in Coda Hale’s excellent post, most cryptographic hash functions are designed to be fast, and thusly are vulnerable to a dictionary attack, where the attacker tries billions of passwords to try to find the same hashed value. To prevent this, we use a key stretching function to increase the computational cost of each password attempt. One such function is bcrypt, mentioned in Coda’s article that (namedrop) is co-authored by our Chief Scientist, David Mazières.
In the case of Stellar, we rely upon scrypt for our key stretching function.
Back to Wallet v1
Okay, so let’s get back to how Stellar protects access to your encrypted wallet. As mentioned, we stored encrypted wallets on the server in slots identified by a Wallet ID. If you presented a correct Wallet ID to a server, we would give you back the encrypted wallet for that ID. Simple as that. The equation in the section heading above (scrypt(u+p,u+p)) describes how we derived the Wallet ID: We used a concatenation of the username and password as both the passphrase and the salt passed into scrypt. This Wallet ID is a one-way transformation and you cannot go from a Wallet ID back to the original username and password; this is the way we achieved anonymization on the server side.
Unfortunately, this anonymization comes with a several drawbacks that we had not anticipated at the original time of development. Since the login process of our wallet server is only presented with a single value (the Wallet ID), we cannot restrict the number of login attempts against a single user account: We don’t even know the username at that point! The only option we have is to restrict the number of login attempts by IP address. Additionally, not knowing the username at the time of login would prevent us from providing our improved implementation of two-factor authentication, which I describe below.
Now that we’ve looked some at Wallet v1, lets take a look at v2.
Wallet v2: hmac-sha256(“WALLET_ID”, scrypt(p,V+s+u))
Now that we’ve looked at the design for wallet v1, we can discuss v2 and why it’s an improvement. In the context of protecting access to your encrypted wallet, v2 has a veritable plethora of improvements: More expected use of cryptographic primitives, greater efficiency on the server side, better support for two-factor authentication, better future-proofing, and better protection against online dictionary attacks. Quite the list! Let’s go through each of them, but first let me describe the algorithm at a high level.
With wallet v2, login is now a two step process. First, you enter the username you would like to login with, and then you enter the password (and optionally the TOTP code) for your account. After the step where you enter your username, we communicate with the server to retrieve your “login parameters”: what salt to use, what scrypt parameters to use, and whether or not the provided user has enabled two factor authentication. After we’ve retrieved your login parameters, we can collect your password and go about deriving the keys used to retrieve your encrypted wallet.
We must derive two values:
K_m, the master key and
W the wallet ID.
K_m does not get used directly; instead, we use it to derive child keys (such as
W) that do get used directly within the protocol. To create
K_m, we use your password
p as the “passphrase” argument to scrypt, and we use a combination of a version byte
V, the random salt
s and your username
u as the “salt” argument to scrypt. As with v1,
W is used to authenticate access to your encrypted blob. We derive
W by using
K_w as the key for the HMAC-SHA256 computed over the constant string “WALLET_ID”.
As an aside, the Wallet ID used in v2 has no uniqueness constraint in our database like it does in v1. This is to prevent a very specific attack, which I may write about in the future.
If you recall from v1, we feed the username+password pair into scrypt as both the passphrase and the salt. Some of you who are more skilled in cryptography than I may have raised your eyebrows in curiosity when reading that; I know Prof. Mazières did when I initially explained the design to him. The reason? This family of functions was not designed to have the same input for both passphrase and salt. That’s not to say that it is specifically insecure in the case of scrypt, but in cryptography it’s better to be safe than sorry and it’s better to use these functions in the ways they were intended.
When it comes to server efficiency, v2 is orders of magnitude faster. In our tests on my laptop, an average login attempt in v1 takes 130ms. In v2? 6ms. The chief reason for this gain in efficiency comes from the removal of the server-side scrypt operation. Instead, we use a simple SHA-256 function to hash
W for storage in the database. To compensate for this your browser does more work in the initial scrypt operation to derive
K_m. By reducing the work that needs to be done on the server, we will be better able to survive denial of service attacks and ensure that the CPU cycles investing in login directly contribute to frustrating brute-force attackers.
Two Factor Authentication (2FA) is a big deal and when properly set up it greatly increases the security of your account. If you haven’t done so on your stellar account already, and you own a smartphone, I highly recommend you go set it up now. It’s all right, I’ll wait… Okay, now that we have that out of the way, let me talk about how we do it differently. In many applications, your second factor of authentication happens after you’ve correctly entered your password, but in our implementation you actually enter your password and your 2FA code at the same time. That may not seem like a big change, but the benefit is: when 2FA is enabled with us, an attacker cannot use our wallet server to confirm that they know your password.
Say you use the same password for your stellar wallet as you do with another website. If that website is compromised and an attacker gets hold of your credentials, it’s simple for them to steal your funds, provided you don’t have 2FA enabled. If, however, we had a typical 2FA implementation the attacker wouldn’t be able to steal your funds but would be able to confirm that yes, you use the same password on both sites. Their next step would be to work on getting your 2FA authentication disabled (usually using some form of social engineering). You can read about one recent high-profile example here. With our implementation of two-factor authentication, an attacker would need to make one million HTTP requests in a 60 second window to confirm a single password. That is practically impossible. The downside to verifying the password and 2FA simultaneously is that attackers can now tell which accounts have enabled 2FA. So all the more reason for you to go an enable 2FA now!
Computers continue to get faster and faster every year. With that increase in power, scripts that attempt to brute force passwords or reverse a hash get faster as well. As argued in A Future-Adaptable Password Scheme (pdf link), it is vastly preferred that the cost of our method to secure passwords grows along with the increases in computational capacity. To accommodate this, scrypt has a work factor that we can tune to increase the cost of every operation. In v2, we have built in the ability to increase that work factor over time, so that as computers get faster we stay secure. In v1, we were prevented from adding this capability into the system due to the way we anonymized wallets.
The last area of improvement I’ll talk about today concerns online dictionary attacks, and how the second version of our wallet server will let us better protect against them. While there are many ways that someone can steal your password, one often used method is to simply try to login with many different passwords. As mentioned in the v1 section, the only tool we have available to us is to restrict login attempts by IP Address. In v2, the situation is improved. We can:
- fight attacks by restricting login attempts by IP address
- track when an attack is occurring on a specific account
- fight attacks on a specific account by restricting login attempts by username
- prevent denial of service attacks to a single account by adding whitelisted IP addresses on a per account basis
Hopefully you have a better understanding about the design of our second wallet server. It’s better on a number of fronts: more featureful, more future-proof, more efficient, and more secure. By sacrificing anonymity of encrypted wallets, we enabled two factor authentication and upgradeable security. For the future, the launch of wallet v2 will enable us to add many more security features to help keep your funds safe.
On the next episode of “Scott rambles about technical things”
For part two of this series on the technical designs of wallet v2, we’ll delve into how we decrypt your wallet in the browser. Following shortly afterwards, we will also be publishing a whitepaper that provides a clear, concise specification for this entire protocol.