【问题标题】:How to securely save username/password (local)?如何安全地保存用户名/密码(本地)?
【发布时间】:2016-02-03 12:24:09
【问题描述】:

我正在制作一个 Windows 应用程序,您需要先登录。
账户信息由用户名和密码组成,需要保存在本地。
这只是安全问题,因此使用同一台计算机的其他人无法看到每个人的个人数据。
保存这些数据的最佳/最安全的方法是什么?

我不想使用数据库,所以我尝试了一些资源文件。
但由于我对这个有点陌生,我不完全确定我在做什么以及我应该在哪里寻找解决方案。

【问题讨论】:

  • 首先,不要保存密码。散列它(可能使用盐值),然后保存它。
  • “用户”你是指普通的 Windows 用户还是别的什么? (我认为你的意思是你们中的一些人拥有“用户”,因为普通 Windows 用户已经看不到彼此的数据......)
  • 我已经编辑了你的标题。请参阅“Should questions include “tags” in their titles?”,其中的共识是“不,他们不应该”。
  • @John Saunders 好吧,原谅我的无知。
  • 有完整源代码的最终解决方案吗?

标签: c# security local


【解决方案1】:

如果您只是要验证/验证输入的用户名和密码,请使用Rfc2898DerivedBytes 类(也称为基于密码的密钥派生函数 2 或 PBKDF2)。这比使用三重 DES 或 AES 等加密更安全,因为没有实用的方法可以从 RFC2898DerivedBytes 的结果返回密码。您只能从密码转到结果。有关 .Net 的示例和讨论,请参阅 Is it ok to use SHA1 hash of password as a salt when deriving encryption key and IV from password string? 或 WinRT/Metro 的 String encrypt / decrypt with password c# Metro Style

如果您要存储密码以供重复使用,例如将其提供给第三方,请使用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[] 并记住在不再需要密码时立即处理或归零它们。

【讨论】:

  • 嗨,我试过了,但在 entropy = rng.GetBytes(20) 处出现错误,提示:无法从 int 转换为 byte[]
  • 似乎该类现在称为 Rfc2898DeriveBytes(小写字母,.net 4.5 和 4.6),可以在此处找到:命名空间:System.Security.Cryptography 程序集:mscorlib(在 mscorlib.dll 中)
  • 信息量很大,但是我认为使用ProtectedData 的全部意义在于我不需要担心 安全地存储熵和密文,......所以只有当前用户可以阅读。我认为它提供了简单性,因为我可以存储它们但是很方便,并且仍然只有 CurrentUser 可以解密它。 entropy 参数也是可选的,看起来类似于 IV,其中唯一性比保密更重要。因此,在明文的变化和更新不频繁的情况下,该值可能会被省略或硬编码到程序中。
  • 事情发生了变化,现在从安全角度来看,SecureString 已被弃用:github.com/dotnet/platform-compat/blob/master/docs/DE0001.md
  • @mCasamento - 是的,但对于非关键应用程序,它可能仍然是一个实际的权衡。因为替换并不总是方便(来自该链接):“处理凭据的一般方法是避免使用它们,而是依靠其他方式进行身份验证,例如证书或 Windows 身份验证。”
【解决方案2】:

我以前用过这个,我认为为了确保凭证持续存在并以最安全的方式是

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

希望这个链接对我有很大帮助:Click here

【讨论】:

    【解决方案3】:

    我想将字符串加密和解密为可读字符串。

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

    右键项目>属性>设置>创建usernamepassword设置。

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

    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>
    

    完整代码

    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 插件的 user.config 中,同时符合 FIPS。在保存到 user.config 时,我找不到可以同时执行这两种操作的算法。编码会严重破坏它。谢谢你。
    【解决方案4】:

    DPAPI 就是为了这个目的。使用 DPAPI 对用户第一次输入的密码进行加密,将其存储在安全的位置(用户的注册表,用户的应用程序数据目录,都是一些选择)。每当应用启动时,检查位置以查看您的密钥是否存在,是否使用 DPAPI 解密并允许访问,否则拒绝它。

    【讨论】:

      【解决方案5】:

      这仅适用于 Windows,因此如果您计划使用 dotnet core 跨平台,则必须寻找其他地方。见https://github.com/dotnet/corefx/blob/master/Documentation/architecture/cross-platform-cryptography.md

      【讨论】:

      • 此链接已失效 (404)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-10-19
      • 2010-12-07
      • 2017-03-23
      • 1970-01-01
      • 2010-12-07
      • 2013-06-02
      相关资源
      最近更新 更多