116

I'm making a Windows application, which you need to log into first.
The account details consist of username and password, and they need to be saved locally.
It's just a matter of security, so other people using the same computer can't see everyone's personal data.
What is the best/most secure way to save this data?

I don't want to use a database, so I tried some things with Resource files.
But since I'm kind of new with this, I'm not entirely sure of what I'm doing and where I should be looking for a solution.

Community
  • 1
  • 1
Robin
  • 1,307
  • 2
  • 10
  • 9

5 Answers5

172

If you are just going to verify/validate the entered user name and password, use the Rfc2898DerivedBytes class (also known as Password Based Key Derivation Function 2 or PBKDF2). This is more secure than using encryption like Triple DES or AES because there is no practical way to go from the result of RFC2898DerivedBytes back to the password. You can only go from a password to the result. See Is it ok to use SHA1 hash of password as a salt when deriving encryption key and IV from password string? for an example and discussion for .Net or String encrypt / decrypt with password c# Metro Style for WinRT/Metro.

If you are storing the password for reuse, such as supplying it to a third party, use the Windows Data Protection API (DPAPI). This uses operating system generated and protected keys and the Triple DES encryption algorithm to encrypt and decrypt information. This means your application does not have to worry about generating and protecting the encryption keys, a major concern when using cryptography.

In C#, use the System.Security.Cryptography.ProtectedData class. For example, to encrypt a piece of data, use ProtectedData.Protect():

// Data to protect. Convert a string to a byte[] using Encoding.UTF8.GetBytes().
byte[] plaintext; 

// Generate additional entropy (will be used as the Initialization vector)
byte[] entropy = new byte[20];
using(RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
    rng.GetBytes(entropy);
}

byte[] ciphertext = ProtectedData.Protect(plaintext, entropy,
    DataProtectionScope.CurrentUser);

Store the entropy and ciphertext securely, such as in a file or registry key with permissions set so only the current user can read it. To get access to the original data, use ProtectedData.Unprotect():

byte[] plaintext= ProtectedData.Unprotect(ciphertext, entropy,
    DataProtectionScope.CurrentUser);

Note that there are additional security considerations. For example, avoid storing secrets like passwords as a string. Strings are immutable, being they cannot be notified in memory so someone looking at the application's memory or a memory dump may see the password. Use SecureString or a byte[] instead and remember to dispose or zero them as soon as the password is no longer needed.

Community
  • 1
  • 1
akton
  • 13,492
  • 3
  • 42
  • 45
  • Hi, I tried this, but I got an error at entropy = rng.GetBytes(20) saying: Cannot convert from int to byte[] – Robin Oct 01 '12 at 11:16
  • @CrispyGMR I have fixed that piece of code in the answer. Good catch. – akton Oct 01 '12 at 11:19
  • Thanks a lot. I used md5 for hashing at first, but I was kind of sceptical about it. This seems way more secure. One more question though. I want to save quite some data like this in a text file. I see that it's just a bunch of random characters when I open my file, but is it safe enough to do this? Or do you recommend another way of storing the data? – Robin Oct 01 '12 at 11:46
  • If you are just validating an entered password, use RFC2898DerivedBytes instead (question updated with link and short explanation). Storing encrypted data in a text file is sufficient assuming only trusted users and the application have access to the file. If a malicious person gets access to the file, they could attempt to brute force the values stored there. Make sure to keep the keys safe (or use DPAPI as mentioned above). – akton Oct 01 '12 at 12:02
  • 2
    Seems that the class is now known as Rfc2898DeriveBytes (small letters, .net 4.5 and 4.6) and can be found here: Namespace: System.Security.Cryptography Assembly: mscorlib (in mscorlib.dll) – Dashu Jun 13 '15 at 13:41
  • @akton when I try this code I see this error: `'RNGCryptoServiceProvider': type used in a using statement must be implicitly convertible to 'System.IDisposable'` - this is after copying your code exactly - do you have any advice? thank you! – Bassie Mar 09 '16 at 17:05
  • @Bassie `RNGCryptoServiceProvider` inherits from `RandomNumberGenerator`, which implements `IDisposable`. I confirmed this code works under Visual Studio 2015 with .Net 4.5 and .Net 4.6. Are you sure you are using the version in the `System.Security.Cryptography` namespace? – akton Mar 09 '16 at 22:41
  • @akton the issue was that my solution was targeting .Net-3.5 rather than 4.5 - thanks for the pointer – Bassie Mar 10 '16 at 12:24
  • @Bassie According to the documentation (https://msdn.microsoft.com/en-us/library/system.security.cryptography.randomnumbergenerator(v=vs.90).aspx), `RandomNumberGenerator` does not implement `IDisposable` in .Net 3.5. Remove the using block and you should be fine. – akton Mar 10 '16 at 20:12
  • 4
    Very informative, however I think the whole point of using `ProtectedData` is so that I don't need worry about *Store the entropy and ciphertext securely, ... so only the current user can read it*. I think it offers simplicity in that I can store them however is convenient and still only the CurrentUser can decrypt it. The `entropy` parameter is also optional and appears similar to an IV where uniqueness matters more than secrecy. As such, the value could probably omitted or hard coded into the program in situations where the variation and update of plaintext is infrequent. – antak Feb 26 '17 at 07:43
  • 1
    Things changes, now SecureString is deprecated from a Security perspective: https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md – mCasamento Feb 04 '21 at 15:35
  • 1
    @mCasamento - true, but for non-critical apps, it may still be a practical trade-off. Because the replacement is not always convenient (from that link): *"The general approach of dealing with credentials is to avoid them and instead rely on other means to authenticate, such as certificates or Windows authentication."* – ToolmakerSteve Apr 09 '21 at 19:13
  • Can I protect data by current application scope? – Rougher Apr 14 '21 at 06:27
9

I have used this before and I think in order to make sure credential persist and in a best secure way is

  1. you can write them to the app config file using the ConfigurationManager class
  2. securing the password using the SecureString class
  3. then encrypting it using tools in the Cryptography namespace.

This link will be of great help I hope : Click here

iliketocode
  • 6,652
  • 4
  • 41
  • 57
Pradip
  • 1,461
  • 11
  • 27
5

I wanted to encrypt and decrypt the string as a readable string.

Here is a very simple quick example in C# Visual Studio 2019 WinForms based on the answer from @Pradip.

Right click project > properties > settings > Create a username and password setting.

enter image description here

Now you can leverage those settings you just created. Here I save the username and password but only encrypt the password in it's respectable value field in the user.config file.

Example of the encrypted string in the user.config file.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <userSettings>
        <secure_password_store.Properties.Settings>
            <setting name="username" serializeAs="String">
                <value>admin</value>
            </setting>
            <setting name="password" serializeAs="String">
                <value>AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAQpgaPYIUq064U3o6xXkQOQAAAAACAAAAAAAQZgAAAAEAACAAAABlQQ8OcONYBr9qUhH7NeKF8bZB6uCJa5uKhk97NdH93AAAAAAOgAAAAAIAACAAAAC7yQicDYV5DiNp0fHXVEDZ7IhOXOrsRUbcY0ziYYTlKSAAAACVDQ+ICHWooDDaUywJeUOV9sRg5c8q6/vizdq8WtPVbkAAAADciZskoSw3g6N9EpX/8FOv+FeExZFxsm03i8vYdDHUVmJvX33K03rqiYF2qzpYCaldQnRxFH9wH2ZEHeSRPeiG</value>
            </setting>
        </secure_password_store.Properties.Settings>
    </userSettings>
</configuration>

enter image description here

Full Code

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace secure_password_store
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Exit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void Login_Click(object sender, EventArgs e)
        {
            if (checkBox1.Checked == true)
            {
                Properties.Settings.Default.username = textBox1.Text;
                Properties.Settings.Default.password = EncryptString(ToSecureString(textBox2.Text));
                Properties.Settings.Default.Save();
            }
            else if (checkBox1.Checked == false)
            {
                Properties.Settings.Default.username = "";
                Properties.Settings.Default.password = "";
                Properties.Settings.Default.Save();
            }
            MessageBox.Show("{\"data\": \"some data\"}","Login Message Alert",MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        private void DecryptString_Click(object sender, EventArgs e)
        {
            SecureString password = DecryptString(Properties.Settings.Default.password);
            string readable = ToInsecureString(password);
            textBox4.AppendText(readable + Environment.NewLine);
        }
        private void Form_Load(object sender, EventArgs e)
        {
            //textBox1.Text = "UserName";
            //textBox2.Text = "Password";
            if (Properties.Settings.Default.username != string.Empty)
            {
                textBox1.Text = Properties.Settings.Default.username;
                checkBox1.Checked = true;
                SecureString password = DecryptString(Properties.Settings.Default.password);
                string readable = ToInsecureString(password);
                textBox2.Text = readable;
            }
            groupBox1.Select();
        }


        static byte[] entropy = Encoding.Unicode.GetBytes("SaLtY bOy 6970 ePiC");

        public static string EncryptString(SecureString input)
        {
            byte[] encryptedData = ProtectedData.Protect(Encoding.Unicode.GetBytes(ToInsecureString(input)),entropy,DataProtectionScope.CurrentUser);
            return Convert.ToBase64String(encryptedData);
        }

        public static SecureString DecryptString(string encryptedData)
        {
            try
            {
                byte[] decryptedData = ProtectedData.Unprotect(Convert.FromBase64String(encryptedData),entropy,DataProtectionScope.CurrentUser);
                return ToSecureString(Encoding.Unicode.GetString(decryptedData));
            }
            catch
            {
                return new SecureString();
            }
        }

        public static SecureString ToSecureString(string input)
        {
            SecureString secure = new SecureString();
            foreach (char c in input)
            {
                secure.AppendChar(c);
            }
            secure.MakeReadOnly();
            return secure;
        }

        public static string ToInsecureString(SecureString input)
        {
            string returnValue = string.Empty;
            IntPtr ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(input);
            try
            {
                returnValue = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr);
            }
            finally
            {
                System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr);
            }
            return returnValue;
        }

        private void EncryptString_Click(object sender, EventArgs e)
        {
            Properties.Settings.Default.password = EncryptString(ToSecureString(textBox2.Text));
            textBox3.AppendText(Properties.Settings.Default.password.ToString() + Environment.NewLine);
        }
    }
}
Jonas
  • 865
  • 1
  • 11
  • 20
  • This is exactly what I needed. My requirements are to save password information to the user.config for a Visio Addin along side being FIPS compliant. I could not find an algorithm that could do both while saving to the user.config. The encoding would mangle it pretty badly. Thanks for this. – Bryan Harrington Oct 12 '20 at 16:36
4

DPAPI is just for this purpose. Use DPAPI to encrypt the password the first time the user enters is, store it in a secure location (User's registry, User's application data directory, are some choices). Whenever the app is launched, check the location to see if your key exists, if it does use DPAPI to decrypt it and allow access, otherwise deny it.

swiftgp
  • 949
  • 1
  • 8
  • 15
4

This only works on Windows, so if you are planning to use dotnet core cross-platform, you'll have to look elsewhere. See https://github.com/dotnet/corefx/blob/master/Documentation/architecture/cross-platform-cryptography.md

Stephen Buck
  • 129
  • 1
  • 3