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:
hermes committed 2026-10-01 19:21:19 +00:00
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)
}