From 6bb9aab5f58068865ef0af52a06fa13abbe0361b Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Sun, 26 Apr 2026 06:31:19 +0000 Subject: [PATCH 1/2] hashPassword(): fix max-length check * add tests for min & max length, both with ascii chars & emojis * test max-length by bytes instead of characters --- lib/util/crypto.js | 2 +- test/unit/util/crypto.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/util/crypto.js b/lib/util/crypto.js index 6242ce289..9a21232b6 100644 --- a/lib/util/crypto.js +++ b/lib/util/crypto.js @@ -36,7 +36,7 @@ const BCRYPT_COST_FACTOR = process.env.BCRYPT === 'insecure' ? 1 : 12; const hashPassword = (plain) => { if (typeof plain !== 'string') return reject(Problem.user.invalidDataTypeOfParameter({ value: plain, expected: 'string' })); if (plain.length < 10) return reject(Problem.user.passwordTooShort()); - if (plain.length > 72) return reject(Problem.user.passwordTooLong()); + if (Buffer.byteLength(plain) > 72) return reject(Problem.user.passwordTooLong()); return isBlank(plain) ? resolve(null) : bcrypt.hash(plain, BCRYPT_COST_FACTOR); }; const verifyPassword = (plain, hash) => ((typeof plain !== 'string' || isBlank(plain) || isBlank(hash)) diff --git a/test/unit/util/crypto.js b/test/unit/util/crypto.js index f0e0a102c..1f5dcfa2b 100644 --- a/test/unit/util/crypto.js +++ b/test/unit/util/crypto.js @@ -23,6 +23,25 @@ describe('util/crypto', () => { it('should reject given a blank plaintext', () => hashPassword('').should.be.rejectedWith('The password or passphrase provided does not meet the required length.')); + it('should reject given a short plaintext', () => + hashPassword('2short').should.be.rejectedWith('The password or passphrase provided does not meet the required length.')); + + it('should reject given a short plaintext (measured in bytes)', () => + // This emoji is a single char on some devices, but in UTF-8 is 11 bytes. A + // single character is too short to use as a password, even if its byte length + // is above the password length limit. + hashPassword('👩‍💻').should.be.rejectedWith('The password or passphrase provided does not meet the required length.')); + + it('should reject given a long plaintext', () => + hashPassword('longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg').should.be.rejectedWith('The password or passphrase provided exceeds the maximum length.')); + + it('should reject given a long plaintext (measured in bytes)', () => { + const password = '❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️'; + password.length.should.be.lessThan(72); + Buffer.byteLength(password).should.be.greaterThan(72); + return hashPassword(password).should.be.rejectedWith('The password or passphrase provided exceeds the maximum length.'); + }); + it('should not attempt to verify empty plaintext', (done) => { verifyPassword('', '$2a$12$hCRUXz/7Hx2iKPLCduvrWugC5Q/j5e3bX9KvaYvaIvg/uvFYEpzSy').then((result) => { result.should.equal(false); From 89c9f34146340d3eaa0ea4f26cadb3a3899599f9 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Tue, 28 Apr 2026 12:49:09 +0000 Subject: [PATCH 2/2] fix tests --- test/unit/util/crypto.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/unit/util/crypto.js b/test/unit/util/crypto.js index 56dde5933..be9580684 100644 --- a/test/unit/util/crypto.js +++ b/test/unit/util/crypto.js @@ -21,14 +21,6 @@ describe('util/crypto', () => { it('should reject given a blank plaintext', () => hashPassword('').should.be.rejectedWith('The password or passphrase provided does not meet the required length.')); - }); - - describe('verifyPassword()', () => { - const { verifyPassword } = crypto; - - it('should always return a Promise', () => { - verifyPassword('password', 'hashhash').should.be.a.Promise(); - }); it('should reject given a short plaintext', () => hashPassword('2short').should.be.rejectedWith('The password or passphrase provided does not meet the required length.')); @@ -48,6 +40,14 @@ describe('util/crypto', () => { Buffer.byteLength(password).should.be.greaterThan(72); return hashPassword(password).should.be.rejectedWith('The password or passphrase provided exceeds the maximum length.'); }); + }); + + describe('verifyPassword()', () => { + const { verifyPassword } = crypto; + + it('should always return a Promise', () => { + verifyPassword('password', 'hashhash').should.be.a.Promise(); + }); it('should not attempt to verify empty plaintext', (done) => { verifyPassword('', '$2a$12$hCRUXz/7Hx2iKPLCduvrWugC5Q/j5e3bX9KvaYvaIvg/uvFYEpzSy').then((result) => {