Zero-Knowledge Proof Authentication
Server never sees user passwords. Authentication uses cryptographic proofs instead of password comparison.
Imports
Required Node.js modules for cryptographic operations.
import crypto from "crypto";
import util from "util";Configuration
Server-side constants for PBKDF2-SHA256 hashing.
- ITERATIONS: 700,000 (exceeds OWASP minimum of 600,000 for PBKDF2-SHA256)
export const CRYPTO_CONFIG = {
HASH_ALGORITHM: "sha256",
KEY_LENGTH: 32,
ITERATIONS: 700000,
};Derived Key Function
Generates cryptographic key using PBKDF2 with configurable iterations.
const deriveKey = async (password, salt) => {
const pbkdf2 = util.promisify(crypto.pbkdf2);
return pbkdf2(
password,
salt,
CRYPTO_CONFIG.ITERATIONS,
CRYPTO_CONFIG.KEY_LENGTH,
CRYPTO_CONFIG.HASH_ALGORITHM,
);
};Password Hashing
Deterministic password hashing for authentication. Always produces same hash for same password.
const hashPasswordDeterministic = async (password) => {
const authSalt = crypto
.createHash(CRYPTO_CONFIG.HASH_ALGORITHM)
.digest("hex");
const key = await deriveKey(password, authSalt);
return key.toString("hex");
};ZK Proof Generation
Generates zero-knowledge proof from password hash and EnvLink ID. Used by both client and server.
export const generateZKProof = async (passwordHash, envlinkId) => {
const serverSalt = crypto
.createHash(CRYPTO_CONFIG.HASH_ALGORITHM)
.update(`envlink:${envlinkId}`)
.digest("hex");
const input = passwordHash + serverSalt;
const key = await deriveKey(input, serverSalt);
return key.toString("base64");
};Client Authentication
Client generates proof and sends to server for verification.
const passwordHash = await hashPasswordDeterministic(userPassword);
const passwordProof = await generateZKProof(passwordHash, envlinkId);
fetch("/api/envlink/auth", {
method: "POST",
body: JSON.stringify({ envlinkId, passwordProof }),
});Server Verification & Authentication
Middleware verifies zero-knowledge proof using timing-safe comparison.
export const verifyEnvLinkPassword = catchAsync(async (req, res, next) => {
const { passwordProof } = req.body || {};
const envlinkId = req.params.id;
if (!isValidEnvLinkId(envlinkId)) {
throw new AppError(HTTP_CODE.BAD_REQUEST, "Invalid EnvLink ID format");
}
const envLink = await EnvLinkModel.findOne({ id: envlinkId });
if (!envLink || isExpired(envLink.expiresAt)) {
throw new AppError(
HTTP_CODE.NOT_FOUND,
"EnvLink not found or access denied",
);
}
if (!envLink.passwordHash) {
return next();
}
if (!passwordProof) {
throw new AppError(HTTP_CODE.BAD_REQUEST, "Password proof is required");
}
const storedProof = await generateZKProof(envLink.passwordHash, envlinkId);
let isProved = false;
try {
const passwordProofBuffer = Buffer.from(passwordProof, "utf8");
const storedPasswordProofBuffer = Buffer.from(storedProof, "utf8");
if (passwordProofBuffer.length === storedPasswordProofBuffer.length) {
isProved = timingSafeEqual(
passwordProofBuffer,
storedPasswordProofBuffer,
);
}
} catch {
isProved = false;
}
if (!isProved) {
throw new AppError(
HTTP_CODE.UNAUTHORIZED,
"EnvLink not found or access denied",
);
}
return next();
});Security Benefits
- Server never sees plaintext passwords
- Encrypted data remains secure even if server is compromised
- Timing-safe comparison prevents side-channel attacks
- EnvLink-specific salts prevent cross-resource attacks