Say no to source-controlled credentials: .NET + RSA encryption

Let’s start with an axiom: you should never store any credentials in your source code in plain text, especially if the codebase is source-controlled. If you are doing it, there’s something seriously wrong with your architecture.1

Here’s one possible solution to this problem. Presumably you already have an OpenSSH pair of public/private keys. It will serve us nicely in this case:

  1. Generate a PEM version of your OpenSSH public key:

    $ openssl rsa -in ~/.ssh/id_rsa -pubout > ~/.ssh/id_rsa.pub.pem
    Enter pass phrase for ~/.ssh/id_rsa:
    writing RSA key
    

  2. Write your password in a plaintext file (say, admin_pass), and encrypt it (say, into admin_pass.enc):2

    cat admin_pass | openssl rsautl -encrypt -pubin -inkey ~/.ssh/id_rsa.pub.pem > admin_pass.enc
    

  3. Delete admin_pass from your hard drive.3

The resulting admin_pass.enc can be safely checked into the repository: one needs access to your private key in order to decrypt it:

cat admin_pass.enc | openssl rsautl -decrypt -inkey ~/.ssh/id_rsa

However, you probably don’t want to do even that. The best policy is to decrypt the credentials directly in your app, securely and in-memory. Presumably your favorite programming language/platform has some API for securely-stored memory objects (usually backed by the OS/hardware cryptography system). I am going to show an implementation for .NET below.

First, in order to use the OpenSSH keys in .NET infrastructure, we need to convert them both to PEM, and also generate the corresponding certificate and the PFX file. Here’s the process using my website and email as an example for the generated certificate (the exact values are not really important for our intended purpose):

$ openssl rsa -in ~/.ssh/id_rsa > ~/.ssh/id_rsa.pem
Enter pass phrase for ~/.ssh/id_rsa:
writing RSA key

$ openssl req -nodes -x509 -days 3650 -subj '/CN=www.alexpolozov.com/emailAddress=polozov@cs.washington.edu' -new -key ~/.ssh/id_rsa.pem -out ~/.ssh/certificate.crt

$ openssl pkcs12 -export -out ~/.ssh/certificate.pfx -inkey ~/.ssh/id_rsa.pem -in ~/.ssh/certificate.crt

Now let’s use this certificate to decrypt the password in C#:

public static SecureString DecryptCredentials(string certificateFile, SecureString pkPassphrase,
                                              string encryptedFile) {
    string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
    string sshDir = Path.Combine(home, ".ssh");
    var cert = new X509Certificate2(Path.Combine(sshDir, certificateFile), pkPassphrase, 
                                    X509KeyStorageFlags.Exportable);
    using (var rsa = cert.PrivateKey as RSACryptoServiceProvider)
    using (var credentialStream = new FileStream(encryptedFile, FileMode.Open))
    using (var encrypted = new MemoryStream()) {
        credentialStream.CopyTo(encrypted);
        // Unfortunately, RSACryptoServiceProvider does not have a Decrypt() overload that
        // returns a SecureString (no idea why). Because of that, we have to store the
        // decrypted data briefly in a garbage-collected memory, and manually convert it
        // into a secure representation as soon as possible.
        byte[] decrypted = rsa.Decrypt(encrypted.ToArray(), false);
        var result = new SecureString();
        foreach (char c in Encoding.ASCII.GetChars(decrypted))
            result.AppendChar(c);
        GC.Collect();  // get rid of the decrypted bytes
        return result;
    }
}

DecryptCredentials("certificate.pfx", Utils.SecurePrompt(), "admin_pass.enc");

The code uses a simple SecurePrompt function, which prompts the user for the SSH passphrase, encrypting one character at a time in a SecureString:

public static SecureString SecurePrompt(string prompt = "> ") {
    Console.Write(prompt);
    var password = new SecureString();
    while (true) {
        var input = Console.ReadKey(true);
        switch (input.Key) {
            case ConsoleKey.Enter:
                Console.WriteLine();
                return password;
            case ConsoleKey.Backspace:
                if (password.Length > 0) {
                    password.RemoveAt(password.Length - 1);
                    Console.Write("\b \b");
                }
                break;
            default:
                password.AppendChar(input.KeyChar);
                Console.Write("*");
                break;
        }
    }
}

  1. And with your undergraduate CS Security class, but I digress.
  2. rsautl has a maximum size limit on the data being encrypted, since it applies the RSA algorithm directly (as opposed to, say, AES). The limit is roughly equal to “[your key size] - [padding]”, which for common key sizes gives 200-400 bytes. Hopefully your password is pretty long, but it probably is not that long ☺
  3. Alternatively, you could avoid creating it in the first place, and simply cat the password from stdin.