哈希

哈希

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

哈希后的值以 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

哈希算法的版本。支持的值为 9798。建议使用最新版本,即 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
}),
}
})