dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto'
|
||||
import { promisify } from 'node:util'
|
||||
import { AccessError } from './errors.mjs'
|
||||
|
||||
const scrypt = promisify(scryptCallback)
|
||||
const BIDI_CONTROLS = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u
|
||||
const CONTROL_CHARACTERS = /\p{Cc}/u
|
||||
|
||||
export const SCRYPT_POLICY = Object.freeze({ N: 32768, r: 8, p: 1, keyLength: 32, maxmem: 64 * 1024 * 1024 })
|
||||
|
||||
function codePoints(value) { return [...value].length }
|
||||
|
||||
export function normalizeUsername(input) {
|
||||
if (typeof input !== 'string') throw new AccessError('USERNAME_INVALID', 'username is invalid')
|
||||
const value = input.trim().normalize('NFC')
|
||||
const bytes = Buffer.byteLength(value, 'utf8')
|
||||
if (value.length === 0 || codePoints(value) > 64 || bytes > 256
|
||||
|| CONTROL_CHARACTERS.test(value) || BIDI_CONTROLS.test(value)) {
|
||||
throw new AccessError('USERNAME_INVALID', 'username is invalid')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizePassword(input) {
|
||||
if (typeof input !== 'string') throw new AccessError('PASSWORD_POLICY_VIOLATION', 'password does not meet policy')
|
||||
const value = input.normalize('NFC')
|
||||
const length = codePoints(value)
|
||||
if (length < 8 || length > 1024 || Buffer.byteLength(value, 'utf8') > 1024
|
||||
|| CONTROL_CHARACTERS.test(value) || BIDI_CONTROLS.test(value)) {
|
||||
throw new AccessError('PASSWORD_POLICY_VIOLATION', 'password does not meet policy')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function validParameters(value) {
|
||||
return value !== null && typeof value === 'object'
|
||||
&& Number.isInteger(value.N) && value.N >= 2
|
||||
&& Number.isInteger(value.r) && value.r > 0
|
||||
&& Number.isInteger(value.p) && value.p > 0
|
||||
&& Number.isInteger(value.keyLength) && value.keyLength >= 16
|
||||
&& Number.isInteger(value.maxmem) && value.maxmem >= 1024 * 1024
|
||||
}
|
||||
|
||||
export async function createCredential(password, {
|
||||
version = 1,
|
||||
policy = SCRYPT_POLICY,
|
||||
now = () => new Date(),
|
||||
random = randomBytes,
|
||||
derive = scrypt,
|
||||
} = {}) {
|
||||
const normalized = normalizePassword(password)
|
||||
const salt = random(16)
|
||||
const hash = await derive(normalized, salt, policy.keyLength, {
|
||||
N: policy.N, r: policy.r, p: policy.p, maxmem: policy.maxmem,
|
||||
})
|
||||
return Object.freeze({
|
||||
version,
|
||||
algorithm: 'scrypt',
|
||||
parameters: { ...policy },
|
||||
salt: Buffer.from(salt).toString('base64url'),
|
||||
hash: Buffer.from(hash).toString('base64url'),
|
||||
changedAt: now().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
export function validCredential(value) {
|
||||
if (value === null || typeof value !== 'object' || value.algorithm !== 'scrypt'
|
||||
|| !Number.isInteger(value.version) || value.version < 1 || !validParameters(value.parameters)
|
||||
|| typeof value.salt !== 'string' || typeof value.hash !== 'string'
|
||||
|| typeof value.changedAt !== 'string') return false
|
||||
try {
|
||||
return Buffer.from(value.salt, 'base64url').byteLength >= 16
|
||||
&& Buffer.from(value.hash, 'base64url').byteLength === value.parameters.keyLength
|
||||
&& Number.isFinite(Date.parse(value.changedAt))
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
export async function verifyCredential(password, credential, { derive = scrypt } = {}) {
|
||||
if (!validCredential(credential)) return false
|
||||
let normalized
|
||||
try { normalized = normalizePassword(password) } catch { return false }
|
||||
const expected = Buffer.from(credential.hash, 'base64url')
|
||||
const salt = Buffer.from(credential.salt, 'base64url')
|
||||
const parameters = credential.parameters
|
||||
const actual = Buffer.from(await derive(normalized, salt, parameters.keyLength, {
|
||||
N: parameters.N, r: parameters.r, p: parameters.p, maxmem: parameters.maxmem,
|
||||
}))
|
||||
return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected)
|
||||
}
|
||||
Reference in new issue
Block a user