import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as crypto from 'node:crypto'; import { RadiusAuthenticator } from '../../ts_server/index.js'; tap.test('should calculate CHAP response per RFC', async () => { // CHAP-Response = MD5(CHAP-ID + Password + Challenge) const chapId = 42; const password = 'mypassword'; const challenge = crypto.randomBytes(16); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); // Verify manually const md5 = crypto.createHash('md5'); md5.update(Buffer.from([chapId])); md5.update(Buffer.from(password, 'utf8')); md5.update(challenge); const expected = md5.digest(); expect(response.length).toEqual(16); expect(response.equals(expected)).toBeTruthy(); }); tap.test('should verify valid CHAP response', async () => { const chapId = 1; const password = 'correctpassword'; const challenge = crypto.randomBytes(16); // Calculate the expected response const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); // Build CHAP-Password attribute: CHAP Ident (1 byte) + Response (16 bytes) const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); // Should verify with correct password const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); }); tap.test('should reject CHAP response with wrong password', async () => { const chapId = 1; const correctPassword = 'correctpassword'; const wrongPassword = 'wrongpassword'; const challenge = crypto.randomBytes(16); // Calculate response with correct password const response = RadiusAuthenticator.calculateChapResponse(chapId, correctPassword, challenge); // Build CHAP-Password attribute const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); // Should NOT verify with wrong password const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, wrongPassword); expect(isValid).toBeFalsy(); }); tap.test('should reject CHAP response with wrong challenge', async () => { const chapId = 1; const password = 'mypassword'; const correctChallenge = crypto.randomBytes(16); const wrongChallenge = crypto.randomBytes(16); // Calculate response with correct challenge const response = RadiusAuthenticator.calculateChapResponse(chapId, password, correctChallenge); // Build CHAP-Password attribute const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); // Should NOT verify with wrong challenge const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, wrongChallenge, password); expect(isValid).toBeFalsy(); }); tap.test('should reject CHAP response with wrong identifier', async () => { const correctChapId = 1; const wrongChapId = 2; const password = 'mypassword'; const challenge = crypto.randomBytes(16); // Calculate response with correct CHAP ID const response = RadiusAuthenticator.calculateChapResponse(correctChapId, password, challenge); // Build CHAP-Password with WRONG CHAP ID const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(wrongChapId, 0); // Wrong ID response.copy(chapPassword, 1); // Should NOT verify const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeFalsy(); }); tap.test('should reject invalid CHAP-Password length', async () => { const challenge = crypto.randomBytes(16); const password = 'mypassword'; // CHAP-Password must be exactly 17 bytes (1 + 16) const invalidChapPassword = Buffer.alloc(16); // Too short const isValid = RadiusAuthenticator.verifyChapResponse(invalidChapPassword, challenge, password); expect(isValid).toBeFalsy(); const tooLongChapPassword = Buffer.alloc(18); // Too long const isValid2 = RadiusAuthenticator.verifyChapResponse(tooLongChapPassword, challenge, password); expect(isValid2).toBeFalsy(); }); tap.test('should handle all CHAP ID values (0-255)', async () => { const password = 'testpassword'; const challenge = crypto.randomBytes(16); for (const chapId of [0, 1, 127, 128, 254, 255]) { const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); } }); tap.test('should handle special characters in password', async () => { const chapId = 1; const password = '!@#$%^&*()_+-=[]{}|;:,.<>?~`'; const challenge = crypto.randomBytes(16); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); }); tap.test('should handle unicode password', async () => { const chapId = 1; const password = '密码パスワード'; // Chinese + Japanese const challenge = crypto.randomBytes(16); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); }); tap.test('should handle empty password', async () => { const chapId = 1; const password = ''; const challenge = crypto.randomBytes(16); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); }); tap.test('should handle very long password', async () => { const chapId = 1; const password = 'a'.repeat(1000); // Very long password const challenge = crypto.randomBytes(16); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); }); tap.test('should handle different challenge lengths', async () => { const chapId = 1; const password = 'testpassword'; // CHAP challenge can be various lengths for (const length of [8, 16, 24, 32]) { const challenge = crypto.randomBytes(length); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); } }); export default tap.start();