如何安全地保存用户名和密码(本地)?

137

我正在制作一个Windows应用程序,首先需要登录。
账户信息包括用户名和密码,需要本地保存。
这只是为了安全起见,让使用同一台计算机的其他人无法看到所有人的个人数据。
最好/最安全的保存这些数据的方法是什么?

我不想使用数据库,所以我尝试了一些Resource文件的方法。
但由于我在这方面还比较新手,我不确定我正在做什么以及应该在哪里寻找解决方案。


9
首先,不要保存密码。对密码进行哈希处理(可能需要加盐),然后保存哈希值。 - carlosfigueira
“用户”是指普通的Windows用户还是其他什么?(我认为你指的是你自己的“用户”,因为普通的Windows用户已经看不到彼此的数据了...) - Alexei Levenkov
3
有完整源代码的最终解决方案吗? - Kiquenet
@Kiquenet 对不起,我没有代码了。 - Robin
4
如果您保存密码用于登录外部数据库,而不必每次都重新输入凭据,则必须保存密码本身,而不是其散列值。问题在于如何本地安全地保存凭据(即以一种只能由当前用户帐户解密的方式进行加密)。 - Triynko
显示剩余2条评论
6个回答

190
如果您只是要验证输入的用户名和密码,请使用Rfc2898DerivedBytes类(也称为基于密码的密钥派生函数2或PBKDF2)。这比使用Triple DES或AES等加密更安全,因为没有实际的方法可以从RFC2898DerivedBytes的结果返回到密码。您只能从密码获得结果。请参见Is it ok to use SHA1 hash of password as a salt when deriving encryption key and IV from password string?以获取有关.Net的示例和讨论或String encrypt / decrypt with password c# Metro Style以获取WinRT/Metro的信息。
如果您需要存储密码以供重复使用,例如提供给第三方,请使用Windows Data Protection API (DPAPI)。它使用操作系统生成和保护的密钥以及Triple DES加密算法来加密和解密信息。这意味着您的应用程序无需担心生成和保护加密密钥,这是使用密码学时的主要问题。
在C#中,使用System.Security.Cryptography.ProtectedData类。例如,要加密一段数据,请使用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);

将熵和密文安全地存储,例如在文件或注册表键中,并设置权限,以便只有当前用户可以读取它。要访问原始数据,请使用ProtectedData.Unprotect()

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

注意,还有其他的安全考虑。例如,避免将密码等机密信息存储为string。因为字符串是不可变的,所以它们在内存中不能被通知,因此查看应用程序的内存或内存转储时可能会看到密码。相反,使用SecureString或byte[],并在密码不再需要时立即处理或清零它们。

2
似乎该类现在被称为Rfc2898DeriveBytes(小写字母,.net 4.5和4.6),可以在以下位置找到: 命名空间:System.Security.Cryptography 程序集:mscorlib(位于mscorlib.dll中) - Dashu
@akton 当我尝试使用这段代码时,我看到了这个错误:'RNGCryptoServiceProvider': type used in a using statement must be implicitly convertible to 'System.IDisposable' - 这是在完全复制您的代码之后出现的 - 您有什么建议吗?谢谢! - Bassie
5
非常详细的解释,但我认为使用ProtectedData的整个意义在于,我不必担心如何“安全存储熵和密文,...以便只有当前用户才能读取它”。这样,使用它可以使存储简化,并且仍然只有当前用户可以解密。 entropy参数也是可选的,类似于IV,其中唯一性比机密性更重要。因此,在平文本的变化和更新不频繁的情况下,该值可能可以省略或硬编码到程序中。 - antak
2
事情在变化,从安全角度来看,现在SecureString已经被弃用了:https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md - mCasamento
2
@mCasamento - 是的,但对于非关键应用程序来说,这仍然可能是一种实际的权衡。因为替换并不总是方便的(来自该链接):“处理凭据的一般方法是避免使用它们,而是依赖其他身份验证手段,如证书或Windows身份验证。” - ToolmakerSteve
显示剩余8条评论

13

我想要将字符串加密并解密成可读的字符串。

这里是一个基于@Pradip回答的非常简单的C# Visual Studio 2019 WinForms快速示例。

右键项目>属性>设置>创建用户名密码设置。

enter image description here

现在您可以利用刚刚创建的那些设置。这里我保存了用户名密码,但只加密了密码user.config文件中的相应值字段。

user.config文件中加密字符串的示例。

<?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

完整代码

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);
        }
    }
}

这正是我所需要的。我的要求是将密码信息保存到用户配置文件中,以便与Visio Addin一起符合FIPS标准。我找不到一个既能够在保存到用户配置文件时满足FIPS标准,又不会使编码混乱的算法。感谢您提供了这个解决方案。 - Bryan Harrington
这太棒了,正是我在寻找的。然而,值得一提的是,user.config 保存在“C:\Users<user>\AppData\Local<program><program>.exe_....<version>”中。 - Mecanik

10

我以前用过这个技术,我认为为了确保凭据持久存在且以最安全的方式,可以:

  1. 使用ConfigurationManager类将它们写入应用程序配置文件
  2. 使用SecureString类保护密码
  3. 然后使用Cryptography命名空间中的工具进行加密。

希望这个链接能够帮到你:点击这里


5

DPAPI专门用于此目的。第一次用户输入密码时,请使用DPAPI进行加密,将其存储在安全位置(例如用户注册表、用户应用程序数据目录等)。每次启动应用程序时,请检查位置以查看密钥是否存在,如果存在,请使用DPAPI解密并允许访问,否则拒绝访问。


4

此链接已失效(404)。 - KarloX
更新后的链接:https://learn.microsoft.com/zh-cn/dotnet/standard/security/cross-platform-cryptography - Triynko

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接