← 中文资源

简体中文版本

口令加盐

盐是在哈希前混入每个口令的唯一随机数据,使相同口令产生不同摘要并让预计算攻击失效,是凭据存储的基线要求。

阅读英文原文

概念概述

口令加盐(Password Salting)指的是:盐是在哈希前混入每个口令的唯一随机数据,使相同口令产生不同摘要并让预计算攻击失效,是凭据存储的基线要求。

从更严格的定义看,A password salt is a random value stored alongside a hash and combined with the password input so that equal passwords yield unequal stored representations. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。

为什么需要它

It is a baseline requirement for credential storage and is built into modern password hashing libraries. 它与 Authenticated Encryption (AEAD)、Forward Secrecy、Key Derivation Functions 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。

常见误解

  • a salt is a secret that must be protected like a key
  • salting alone makes a fast hash suitable for passwords

需要避免的反模式

  • reusing one global salt for every account
  • implementing salting by hand instead of using a reviewed library

实践要点

  1. Because the salt is unique and stored with the hash, an attacker cannot reuse a table computed for one user against another, and equal passwords no longer reveal shared credentials.
  2. The salt does not need to be secret, only unique and unpredictable; its purpose is to remove precomputation, not to add a second secret.
  3. Salting increases the cost per guess only marginally, so it must be combined with a slow algorithm tuned so that verification stays responsive for legitimate users.

参考来源

https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html

代码示例

const COMMON_PATTERNS = [/^123456/, /password/i, /qwerty/i, /(.)\1{2,}/];

// Password strength estimation based on character-pool entropy.
export function estimatePasswordStrength(password) {
  const value = String(password);
  const pools = [
    [/[a-z]/, 26],
    [/[A-Z]/, 26],
    [/[0-9]/, 10],
    [/[^A-Za-z0-9]/, 33],
  ];
  const poolSize = pools.reduce((total, [pattern, size]) => {
    return total + (pattern.test(value) ? size : 0);
  }, 0);
  const unique = new Set(value).size;
  const effectiveLength = Math.max(unique, Math.ceil(value.length * 0.75));
  let entropyBits = poolSize > 0 ? effectiveLength * Math.log2(poolSize) : 0;
  for (const pattern of COMMON_PATTERNS) {
    if (pattern.test(value)) entropyBits *= 0.5;
  }
  if (value.length < 8) entropyBits *= 0.6;
  const thresholds = [20, 40, 60, 80];
  let score = thresholds.filter((threshold) => entropyBits >= threshold).length;
  if (value.length === 0) score = 0;
  const labels = ["very weak", "weak", "fair", "strong", "very strong"];
  return {
    entropyBits: Math.round(entropyBits),
    score,
    label: labels[score],
    poolSize,
    length: value.length,
    uniqueCharacters: unique,
  };
}
import hashlib
import hmac
import os

N = 2 ** 14
R = 8
P = 1
KEY_LEN = 32
SALT_LEN = 16

def hash_password(password: str) -> str:
    salt = os.urandom(SALT_LEN)
    digest = hashlib.scrypt(password.encode('utf-8'), salt=salt, n=N, r=R, p=P, dklen=KEY_LEN)
    return f'scrypt${N}${R}${P}${salt.hex()}${digest.hex()}'

def verify_password(password: str, encoded: str) -> bool:
    parts = encoded.split('$')
    if len(parts) != 5 or parts[0] != 'scrypt':
        return False
    _, n, r, p, rest = parts
    salt_hex, digest_hex = rest[:SALT_LEN * 2], rest[SALT_LEN * 2:]
    expected = bytes.fromhex(digest_hex)
    digest = hashlib.scrypt(password.encode('utf-8'), salt=bytes.fromhex(salt_hex),
                            n=int(n), r=int(r), p=int(p), dklen=len(expected))
    return hmac.compare_digest(digest, expected)