65.9K
CodeProject 正在变化。 阅读更多。
Home

YAPM(又一个密码管理器)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2018年3月9日

CPOL

6分钟阅读

viewsIcon

23625

downloadIcon

1663

本文描述了创建安全的离线密码管理器所需的安全技术,以及 Libsodium 库如何用于实现这一目标。YAPM 使用 AES 加密存储密码,并使用 Argon2 哈希对用户进行身份验证。

引言

信息安全领域有句话说,我们不应该自己动手。这背后的普遍想法是,在你开发新协议或实现现有协议的过程中,你会在某个地方犯下一个严重的安全性错误,从而严重影响系统的安全性。

然而,本项目仅用于教育目的,旨在了解现有协议、实现问题和一般编程。我知道我在开发 YAPM 时几乎肯定犯了一些严重的错误,因此我不建议将其用作您的个人密码管理器(如果能通过评论指出任何安全问题,将不胜感激!)。

密码管理器非常有用,因为它们不仅解决了忘记密码的问题,而且还鼓励为每个帐户使用更强大、更独特的密码,因为它们不需要记住。然而,这也意味着主密码必须非常安全——如果它被发现,那么攻击者就掌握了“王国之钥”。

安全

一般安全

YAPM 密码使用 AES-GCM-256 位加密存储在名为 main_store 的文件中,并使用密码学随机的 32 字节加密密钥来加密和解密存储的密码。该加密密钥使用 AES-GCM-256 位加密,以主密码的哈希作为密钥,并存储在名为 enc_key 的文件中。用户通过主密码的 Argon2i 哈希(和盐)进行身份验证,该哈希存储在名为 auth 的文件中。Argon2 非常慢,导致登录时有几秒钟的延迟——但这正是我们想要的:用于哈希密码的算法需要很慢,这样暴力破解攻击才能花费很长时间才能找到用于创建哈希的密码。这里的错误是使用不是为密码存储设计的快速哈希算法,例如 MD5 或 SHA-256。

所有文件都存储在 %appdata%/YAPM 中。

身份验证和更多详细信息

YAPM 使用 Libsodium(C# 包装器)作为加密库。

要访问 YAPM 存储的密码,需要用户的主密码(至少 15 个字符)。此主密码根据验证哈希进行验证,验证哈希是用户主密码的 Argon2i 哈希,参数如下:time=6,memory=131072KiB,parallelism=1(这些参数来自 Libsodium 预定义值 PasswordHash.StrengthArgon.Moderate)。我选择 Argon2 是因为它赢得了最近的密码哈希竞赛,并且目前被广泛推荐。

如果主密码已成功针对验证哈希进行验证,则主密码用于解密加密密钥。此 32 字节加密密钥由 Libsodium 中的密码学随机函数 SodiumCore.GetRandomBytes() 生成。然后,此密钥使用 AES-GCM-256 位加密进行加密,密钥是主密码的 32 字节哈希 (GenericHash.Hash())。

然后,此解密的 32 字节加密密钥用于解密密码,这些密码使用 AES-GCM-256 位加密。所有 AES 操作都使用唯一的 12 字节 nonce,对于主存储的加密,nonce 会递增以确保它永远不会被使用两次。

Using the Code

该程序是一个 WinForms 应用程序,可以在 Visual Studio 环境中执行,也可以直接从可执行文件执行。代码注释非常详细,但如果您有任何具体问题,请随时提问,我将提供更多详细信息。

使用了 9 个主要类

  • MainForm - 包含主用户窗体并处理与窗体的交互。
  • AddPasswordForm - 包含添加密码的窗体,处理新密码的创建。
  • EditPasswordEntryForm - 包含编辑密码条目的窗体,处理现有密码条目的修改。
  • SettingsForm - 包含设置窗体,用于更改、查看和读/写设置到存储文件(%appdata%/YAPM/settings)。设置文件的内容遵循严格的 setting=value 格式。
  • Crypto - 借助 Libsodium 处理所有加密操作。此类的目的包括验证用户身份、加密/解密存储的密码、更改用户的主密码。
  • Register - 在存储密码之前创建所需的初始文件(加密密钥、nonce、哈希、默认设置)。
  • PasswordManage - 管理从文件加载的密码(显示密码、编辑密码、添加/删除密码)。
  • Watchdog - 创建一个看门狗计时器,如果用户闲置一段时间,该计时器会导致程序超时并锁定应用程序。当鼠标移动到窗体上时,看门狗计时器会重置。
  • ByteOperation - 递增/递减一个字节。这用于递增/递减用于加密的 12 字节 nonce。

主存储的加密和解密过程如下所示

 // Function to get the encryption key for the main store.
private static byte[] GetKey(string masterPassword) {
    // Get encrypted encryption key and nonce.
    byte[] encryptedKey = Convert.FromBase64String(File.ReadAllText(YAPM_KEY));
    byte[] keyNonce = Convert.FromBase64String(File.ReadAllText(YAPM_KEY_NONCE));
    // Decrypt key with parameters stored in YAPM_PATH.
    byte[] key = SecretAeadAes.Decrypt(encryptedKey, keyNonce, 
                 GenericHash.Hash(masterPassword, (byte[])null, 32));
    return key;
}

// Function to get the contents of the main store file.
private static string[] GetStoreFileContents() {
    // Read each line from file. String array contains base64 encoded strings.
    string[] storeFileContents = File.ReadAllLines(YAPM_STORE);
    return storeFileContents;
}

// Function to get the nonce for the main store.
private static byte[] GetNonce() {
    byte[] nonce = Convert.FromBase64String(File.ReadAllText(YAPM_STORE_NONCE));
    return nonce;
}

// Function to encrypt the main store.
public static void EncryptStoreFile(string masterPassword, string[] dataToEncrypt) {
    // Get required variables.
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    // Clear main store.
    File.WriteAllText(YAPM_STORE, "");
    // Encrypt each password entry.
    for (int i = 0; i < dataToEncrypt.Length; i++) {
        // Increment nonce so every password entry uses a different nonce.
        ByteOperation.Increment(ref nonce);
        byte[] byteDataToEnc = Encoding.ASCII.GetBytes(dataToEncrypt[i]);
        var encrypted = SecretAeadAes.Encrypt(byteDataToEnc, nonce, key);
        File.AppendAllText(YAPM_STORE, Convert.ToBase64String(encrypted) + Environment.NewLine);
    }
    // Write nonce to file.
    File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
}

// Function to decrypt the main store.
public static List<string> DecryptStoreFile(string masterPassword) {
    // Get required variables.
    string[] storeFileContents = GetStoreFileContents();
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    List<string> decryptedList = new List<string>();
    // Decrypt each password entry. Work backwards with last nonce used, as nonce decrements.
    for (int i = storeFileContents.Length - 1; i > -1; i--) {
        byte[] dataToDecrypt = Convert.FromBase64String(storeFileContents[i]);
        var decrypted = SecretAeadAes.Decrypt(dataToDecrypt, nonce, key);
        decryptedList.Add(Encoding.ASCII.GetString(decrypted));
        // Decrement nonce to get each nonce used to encrypt password entry.
        ByteOperation.Decrement(ref nonce);
    }
    // Return list containing all decrypted password entries.
    return decryptedList;
}

正如您在 EncryptStoreFileDecryptStoreFile 中看到的那样,数组中要加密/解密的每个项目都使用相同的密钥,但使用不同的 nonce 进行加密。

虽然所有加密/解密都由 Libsodium 处理,但 nonce 由 ByteOperation.cs 处理。此类有两个简单的方法

ByteOperation.Increment()

public static void Increment(ref byte[] byteArr) {
    for (int i = 0; i < byteArr.Length; i++) {
        byteArr[i]++;
        if (byteArr[i] > 0) {
            return;
        }
    }
    return;
}

ByteOperation.Decrement()

public static void Decrement(ref byte[] byteArr) {
    for (int i = 0; i < byteArr.Length; i++) {
        byteArr[i]--;
        if (byteArr[i] < 255) {
            return;
        }
    }
    return;
}

屏幕截图

我选择了非常基本的设计,因为我不希望用户界面看起来过于杂乱。

登录后的主窗体(可完全调整大小)。右键单击行可显示更多选项

设置菜单

添加新密码

编辑现有密码条目

关注点

在撰写本文时,我正在查看“使用代码”部分中显示的代码,并注意到有些地方不太对劲,具体来说是以下内容

// Increment nonce.
ByteOperation.Increment(ref nonce);
// Clear main store.
File.WriteAllText(YAPM_STORE, "");
// Encrypt each password entry.
for (int i = 0; i < dataToEncrypt.Length; i++) {
    byte[] byteDataToEnc = Encoding.ASCII.GetBytes(dataToEncrypt[i]);
    var encrypted = SecretAeadAes.Encrypt(byteDataToEnc, nonce, key);
    File.AppendAllText(YAPM_STORE, Convert.ToBase64String(encrypted) + Environment.NewLine);
}
// Write nonce to file.
File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));

请注意 nonce 是如何递增的,然后相同的 nonce 用于加密数组中的每个密码条目。这很糟糕,因为您现在有多个使用相同密钥流加密的密文,这是不安全的。我通过在加密时在 for 循环的每次迭代中递增 nonce,并在解密时在 for 循环的每次迭代中递减 nonce 来修复此问题

// Function to encrypt the main store.
public static void EncryptStoreFile(string masterPassword, string[] dataToEncrypt) {
    // Get required variables.
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    // Clear main store.
    File.WriteAllText(YAPM_STORE, "");
    // Encrypt each password entry.
    for (int i = 0; i < dataToEncrypt.Length; i++) {
        // Increment nonce so every password entry uses a different nonce.
        ByteOperation.Increment(ref nonce);
        byte[] byteDataToEnc = Encoding.ASCII.GetBytes(dataToEncrypt[i]);
        var encrypted = SecretAeadAes.Encrypt(byteDataToEnc, nonce, key);
        File.AppendAllText(YAPM_STORE, Convert.ToBase64String(encrypted) + Environment.NewLine);
    }
    // Write nonce to file.
    File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
}

// Function to decrypt the main store.
public static List<string> DecryptStoreFile(string masterPassword) {
    // Get required variables.
    string[] storeFileContents = GetStoreFileContents();
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    List<string> decryptedList = new List<string>();
    // Decrypt each password entry. Work backwards with last nonce used, as nonce decrements.
    for (int i = storeFileContents.Length - 1; i > -1; i--) {
        byte[] dataToDecrypt = Convert.FromBase64String(storeFileContents[i]);
        var decrypted = SecretAeadAes.Decrypt(dataToDecrypt, nonce, key);
        decryptedList.Add(Encoding.ASCII.GetString(decrypted));
        // Decrement nonce to get each nonce used to encrypt password entry.
        ByteOperation.Decrement(ref nonce);
    }
    // Return list containing all decrypted password entries.
    return decryptedList;
}

特点

YAPM 有一个超时功能,这非常重要——如果没有它,如果有人离开他们的电脑,他们几乎就把所有账户的所有权交给了任何有恶意意图的路人。超时功能的工作原理是启动一个看门狗计时器(包含在 Watchdog.cs 中),当鼠标移动到窗体上时,该计时器会重置(“踢看门狗”)。当看门狗计时器达到超时时间时,应用程序通过注销来锁定。超时时间可以在设置中更改,默认为 2 分钟(120000 毫秒)。如果不需要,也可以在设置中禁用超时。

在更改密码时确认当前主密码也很重要——如果没有它,有人可以将账户所有权转移给自己,这也会将用户锁定在他们的账户之外。

使用主密码登录后,密码默认会被混淆(这可以在设置中更改),可以通过右键单击密码所在的行 -> 密码可见性 -> 显示来显示。右键单击还用于编辑/删除密码、从密码条目复制内容和显示任何备注。

历史

  • 2018/03/08 - 初次发布
© . All rights reserved.