210 lines
7.2 KiB
TypeScript
210 lines
7.2 KiB
TypeScript
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
|
import * as crypto from '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();
|