哈希

哈希

你可以使用 hash 服务为应用中的用户密码进行哈希。AdonisJS 对 bcrypt、scrypt 和 argon2 哈希算法提供了一流的支持,并且具备添加自定义驱动的能力。

哈希后的值以 PHC 字符串格式 存储。PHC 是一种用于格式化哈希值的确定性编码规范。

用法

hash.make 方法接受一个明文值(用户输入的密码)并返回哈希输出。

import hash from '@adonisjs/core/services/hash'
const hash = await hash.make('user_password')
// $scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ

你无法将哈希值转换回明文,哈希是一个单向过程,生成哈希后无法取回原始值。

但是,哈希提供了一种方式来校验给定的明文值是否与现有的哈希匹配,你可以使用 hash.verify 方法执行此检查。

import hash from '@adonisjs/core/services/hash'
if (await hash.verify(existingHash, plainTextValue)) {
// password is correct
}

配置

哈希的配置存储在 config/hash.ts 文件中。默认驱动设置为 scrypt,因为 scrypt 使用 Node.js 原生 crypto 模块,不需要任何第三方包。

config/hash.ts
import { defineConfig, drivers } from '@adonisjs/core/hash'
export default defineConfig({
default: 'scrypt',
list: {
scrypt: drivers.scrypt(),
/**
* Uncomment when using argon2
argon: drivers.argon2(),
*/
/**
* Uncomment when using bcrypt
bcrypt: drivers.bcrypt(),
*/
}
})

Argon

Argon 是哈希用户密码时推荐的哈希算法。要在 AdonisJS 哈希服务中使用 argon,你必须安装 argon2 npm 包。

npm i argon2

我们为 argon 驱动配置了安全的默认值,但你可以根据应用需求随意调整配置选项。以下是可用选项的列表。

export default defineConfig({
// Make sure to update the default driver to argon
default: 'argon',
list: {
argon: drivers.argon2({
version: 0x13, // hex code for 19
variant: 'id',
iterations: 3,
memory: 65536,
parallelism: 4,
saltSize: 16,
hashLength: 32,
})
}
})

variant

要使用的 argon 哈希变体。

  • d 更快,且对 GPU 攻击有极强的抵抗力,对加密货币很有用
  • i 较慢,且对折中攻击有抵抗力,更适合密码哈希和密钥派生。
  • id (默认) 是上述两者的混合,对 GPU 和折中攻击均有抵抗力。

version

要使用的 argon 版本。可用选项为 0x10 (1.0) 和 0x13 (1.3)。默认应使用最新版本。

iterations

iterations 计数会增加哈希强度,但也需要更长的计算时间。

默认值为 3。

memory

用于哈希值的内存量。每个并行线程都会拥有此大小的记忆池。

默认值为 65536 (KiB)。

parallelism

用于计算哈希的线程数。

默认值为 4。

saltSize

盐(salt)的长度(以字节为单位)。Argon 在计算哈希时会生成此大小的加密安全随机盐。

密码哈希的默认和推荐值为 16。

hashLength

原始哈希的最大长度(以字节为单位)。输出值会比提到的哈希长度更长,因为原始哈希输出会被进一步编码为 PHC 格式。

默认值为 32

Bcrypt

要在 AdonisJS 哈希服务中使用 Bcrypt,你必须安装 bcrypt npm 包。

npm i bcrypt

以下是可用配置选项的列表。

export default defineConfig({
// Make sure to update the default driver to bcrypt
default: 'bcrypt',
list: {
bcrypt: drivers.bcrypt({
rounds: 10,
saltSize: 16,
version: 98
})
}
})

rounds

计算哈希的开销。我们建议阅读 Bcrypt 文档中的 A Note on Rounds 章节,以了解 rounds 值如何影响计算哈希所需的时间。

默认值为 10。

saltSize

盐(salt)的长度(以字节为单位)。在计算哈希时,我们会生成此大小的加密安全随机盐。

默认值为 16。

version

哈希算法的版本。支持的值为 97 和 98。建议使用最新版本,即 98。

Scrypt

scrypt 驱动使用 Node.js crypto 模块来计算密码哈希。配置选项与 Node.js scrypt 方法 接受的选项相同。

export default defineConfig({
// Make sure to update the default driver to scrypt
default: 'scrypt',
list: {
scrypt: drivers.scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
saltSize: 16,
maxMemory: 33554432,
keyLength: 64
})
}
})

使用模型钩子对密码进行哈希

由于你将使用 hash 服务对用户的密码进行哈希,你可能会发现将逻辑放在 beforeSave 模型钩子中会很有帮助。

如果你正在使用 @adonisjs/auth 模块,则无需在模型中对密码进行哈希。AuthFinder 会自动处理密码哈希,确保你的用户凭据被安全地处理。在此处了解更多相关信息 here。

import { BaseModel, beforeSave } from '@adonisjs/lucid'
import hash from '@adonisjs/core/services/hash'
export default class User extends BaseModel {
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}

在驱动之间切换

如果你的应用使用多个哈希驱动,可以使用 hash.use 方法在它们之间切换。

hash.use 方法接受配置文件中的映射名称,并返回匹配驱动的实例。

import hash from '@adonisjs/core/services/hash'
// uses "list.scrypt" mapping from the config file
await hash.use('scrypt').make('secret')
// uses "list.bcrypt" mapping from the config file
await hash.use('bcrypt').make('secret')
// uses "list.argon" mapping from the config file
await hash.use('argon').make('secret')

检查密码是否需要重新哈希

建议使用最新的配置选项来保证密码安全,尤其是在旧版本哈希算法报告存在漏洞时。

在使用最新选项更新配置后,你可以使用 hash.needsReHash 方法检查一个密码哈希是否使用了旧的选项,并执行重新哈希。

该检查必须在用户登录期间执行,因为这是你唯一可以访问明文密码的时机。

import hash from '@adonisjs/core/services/hash'
if (await hash.needsReHash(user.password)) {
user.password = await hash.make(plainTextPassword)
await user.save()
}

如果你使用模型钩子来计算哈希,可以将明文值赋给 user.password。

if (await hash.needsReHash(user.password)) {
// Let the model hook rehash the password
user.password = plainTextPassword
await user.save()
}

在测试期间伪造哈希服务

哈希值通常是一个缓慢的过程,会让你的测试变慢。因此,你可能希望使用 hash.fake 方法伪造哈希服务以禁用密码哈希。

在下面的示例中,我们使用 UserFactory 创建 20 个用户。由于你使用的是模型钩子来对密码进行哈希,这可能需要 5-7 秒(取决于配置)。

import hash from '@adonisjs/core/services/hash'
test('get users list', async ({ client }) => {
await UserFactory().createMany(20)
const response = await client.get('users')
})

但是,一旦你伪造了哈希服务,同样的测试运行速度将快一个数量级。

import hash from '@adonisjs/core/services/hash'
test('get users list', async ({ client }) => {
hash.fake()
await UserFactory().createMany(20)
const response = await client.get('users')
hash.restore()
})

创建自定义哈希驱动

哈希驱动必须实现 HashDriverContract 接口。此外,官方的 Hash 驱动使用 PHC 格式 来序列化哈希输出以便存储。你可以查看现有驱动的实现,了解它们如何使用 PHC 格式化器 来生成和校验哈希。

import {
HashDriverContract,
ManagerDriverFactory
} from '@adonisjs/core/types/hash'
/**
* Config accepted by the hash driver
*/
export type PbkdfConfig = {
}
/**
* Driver implementation
*/
export class Pbkdf2Driver implements HashDriverContract {
constructor(public config: PbkdfConfig) {
}
/**
* Check if the hash value is formatted as per
* the hashing algorithm.
*/
isValidHash(value: string): boolean {
}
/**
* Convert raw value to Hash
*/
async make(value: string): Promise<string> {
}
/**
* Verify if the plain value matches the provided
* hash
*/
async verify(
hashedValue: string,
plainValue: string
): Promise<boolean> {
}
/**
* Check if the hash needs to be re-hashed because
* the config parameters have changed
*/
needsReHash(value: string): boolean {
}
}
/**
* Factory function to reference the driver
* inside the config file.
*/
export function pbkdf2Driver (config: PbkdfConfig): ManagerDriverFactory {
return () => {
return new Pbkdf2Driver(config)
}
}

在上面的代码示例中,我们导出了以下值。

  • PbkdfConfig:你想要接受的配置的 TypeScript 类型。

  • Pbkdf2Driver:驱动的实现。它必须遵守 HashDriverContract 接口。

  • pbkdf2Driver:最后,一个用于惰性创建驱动实例的工厂函数。

使用该驱动

实现完成后,你可以使用 pbkdf2Driver 工厂函数在配置文件中引用该驱动。

config/hash.ts
import { defineConfig } from '@adonisjs/core/hash'
import { pbkdf2Driver } from 'my-custom-package'
export default defineConfig({
list: {
pbkdf2: pbkdf2Driver({
// config goes here
}),
}
})