AegixPass 算法核心设计

AegixPass 是一种确定性派生密码生成算法,其输出只依赖于输入的主密码、区分密钥和预设配置。该算法不使用任何外部随机源,因此只要输入相同,输出密码总是相同的。

其主要特点和设计原则如下:

  • 确定性 (Deterministic):算法设计目的为可复现的密码派生过程,其算法仅使用可固定复现的随机队列完成密码生成操作,不涉及任何不依赖任何外部或硬件随机源。
  • 不可逆性 (Irreversible):通过强大的单向加密哈希函数来处理输入。即使攻击者获得了最终生成的密码和所用的预设配置,也绝对无法反向推导出您的主密码。
  • 复杂度保证 (Complexity Guarantee):算法确保最终生成的密码中,至少包含一个来自每个指定字符集(charsets)的字符。
  • 抗碰撞性 (Collision Resistance):主密码、区分密钥或预设配置中任何一个微小的变化,都会通过哈希雪崩效应,导致最终生成的密码发生巨大改变。

算法核心输入

算法的运作依赖于以下三个核心输入:

主密码 (password_source)

用户持有的核心秘密,程序永不存储。

区分密钥 (distinguish_key)

用于为不同服务(如网站域名或应用名称)生成不同密码的变量,作为区分生成结果的盐值。

预设 (preset)

一个 JSON 对象,定义了密码生成的所有参数。其结构如下:

{
  "name": "AegixPass - Default",
  "version": 1,
  "hashAlgorithm": "argon2id",
  "rngAlgorithm": "chaCha20",
  "shuffleAlgorithm": "fisherYates",
  "length": 16,
  "platformId": "aegixpass.takuron.com",
  "charsets": [
    "0123456789",
    "abcdefghijklmnopqrstuvwxyz",
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    "!@#$%^&*_+-="
  ]
}
  • length: 密码总长度。
  • version: 算法的版本。
  • charsets: 一个字符串数组,定义了密码应包含的字符分组。例如,数字、小写字母、大写字母和符号。
  • hashAlgorithm: 用于生成主种子的哈希算法,如 sha256, blake3, sha3_256, argon2id, scrypt
  • rngAlgorithm: 确定性随机数生成器算法,目前实现为 chaCha20
  • shuffleAlgorithm: 洗牌算法,固定为 fisherYates
  • platformId: 平台ID,作为一个额外可以变动的盐值用于算法使用者做区分。

详细算法流程

密码的生成过程严格遵循以下几个关键阶段:

阶段 A: 输入验证

在开始计算前,程序会进行严格的输入检查,以避免产生不安全或无效的结果。

  • 非空验证:确保主密码和区分密钥均不为空。
  • 长度验证:确保请求的密码长度 (length) 必须大于或等于字符集分组的数量 (charsets.length)。这是因为后续步骤需要为每个字符集分组至少选择一个字符。
  • 字符集验证:确保每个字符集分组内都至少包含一个字符。

阶段 B: 生成主种子 (Master Seed)

这是整个确定性算法的基石。一个 32 字节的主种子是后续所有伪随机操作的唯一来源。

  1. 拼接输入数据:将所有影响密码生成的参数和用户输入拼接成一个唯一的、格式固定的长字符串。
    • 格式: "AegixPass_V{version}:{platform_id}:{length}:{password_source}:{distinguish_key}:{charsets_json}"
    • 示例:
      AegixPass_V1:aegixpass.takuron.com:16:MySecretPassword123!:example.com:["0123456789","abc...","ABC...","!@#..."]
      
    • 这种设计确保了预设中的任何一个参数(甚至是 charsets 的顺序)发生变化,都会生成一个完全不同的种子。
  2. 哈希计算:根据 hashAlgorithm 的类型,使用不同的方法处理上一步的输入数据:
    • 如果使用快哈希 (sha256, blake3, sha3_256): 直接对上一步拼接好的 UTF-8 编码字符串进行哈希计算,得到 32 字节的主种子。
    • 如果使用慢哈希 (argon2id, scrypt): 慢哈希是内存困难型函数,需要额外的盐(Salt)和计算参数来增加破解难度。 a. 生成确定性盐:为了保证整个流程的确定性,盐值由 platformId 通过 SHA-256 哈希生成:salt = sha256(platformId)。 b. 执行密钥派生: 对于 argon2id:使用 Argon2id 算法,结合预设的参数(内存成本: 19 MiB, 迭代次数: 2, 并行度: 1),处理输入数据和盐,派生出 32 字节的主种子。 * 对于 scrypt:使用 Scrypt 算法,结合预设的参数(N=2^15, r=8, p=1),处理输入数据和盐,派生出 32 字节的主种子。
  3. 获取种子:将哈希结果作为 32 字节的主种子。

阶段 C: 保证每个字符集至少出现一次 (字符集保证)

为了满足现代密码的复杂度要求,算法会确定性地从每个 charsets 分组中挑选一个字符,放入最终的密码中。

  1. 分割种子:将 32 字节的主种子按顺序分割成多个 4 字节(32位无符号整数)的块。
  2. 选择字符:遍历 charsets 数组,对于第 i 个字符集分组:
    • 取出主种子中的第 i 个 4 字节块。
    • 将这个块解释为一个无符号 32 位整数(小端序)。
    • 使用这个整数对当前字符集的长度进行取模运算 (%),得到一个索引。
    • 将该索引对应的字符添加到初始密码数组中。

阶段 D: 填充密码剩余长度

此时,密码数组中已经包含了满足基本复杂度的字符,接下来需要用更多“随机”字符填充至用户指定的 length

  1. 创建确定性 RNG:使用整个 32 字节主种子来初始化一个确定性的随机数生成器(ChaCha20)。

    • 为了保证跨平台(如 Rust 和 JavaScript)实现的一致性,RNG 的初始化参数被严格固定。例如,在使用 ChaCha20 时,nonce 固定为一个 12 字节的全零数组。
  2. 合并字符集:将 charsets 数组中的所有字符合并成一个大的字符池。

  3. 填充字符:循环填充剩余长度的每一个位置:

    • 从 RNG 中获取一个随机数。
    • 为了保证公平性,这里使用了一个无偏的范围随机数生成逻辑 (secure_random_range_u32)。它通过“拒绝采样”方法避免了简单取模运算带来的偏差,确保大字符池中的每个字符被选中的概率完全相等。
    • 将从字符池中选出的字符添加到密码数组中。

阶段 E: 最终整体洗牌

为了消除阶段 C 中引入的、保证性字符位置的任何可预测性,需要对整个密码数组进行最后一次确定性的洗牌。

  1. 使用同一 RNG 流:继续使用阶段 D 创建的 RNG 实例(或其字节流)进行操作,确保洗牌操作本身也是完全确定和可复现的。
  2. Fisher-Yates 洗牌:从后向前遍历密码数组,对于每个位置 i,使用 RNG 生成一个 [0, i] 范围内的随机索引 j,然后交换位置 ij 的字符。同样,这里也会使用无偏的范围随机数生成逻辑。

阶段 F: 组合并返回

将最终洗牌后的字符数组组合成一个字符串,并返回给用户。


通过以上步骤,AegixPass 算法确保了在任何兼容的实现上,只要输入完全一致,输出的密码也必然完全相同,同时保证了密码的强度和安全性。