import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as crypto from 'node:crypto'; import { RadiusAuthenticator, RadiusPacket, ERadiusCode, ERadiusAttributeType, } from '../../ts_server/index.js'; tap.test('should generate 16-byte random request authenticator', async () => { const auth1 = RadiusAuthenticator.generateRequestAuthenticator(); const auth2 = RadiusAuthenticator.generateRequestAuthenticator(); expect(auth1.length).toEqual(16); expect(auth2.length).toEqual(16); // Should be random (different each time) expect(auth1.equals(auth2)).toBeFalsy(); }); tap.test('should calculate response authenticator correctly', async () => { const code = ERadiusCode.AccessAccept; const identifier = 1; const requestAuthenticator = crypto.randomBytes(16); const attributes = Buffer.from([]); const secret = 'testing123'; const responseAuth = RadiusAuthenticator.calculateResponseAuthenticator( code, identifier, requestAuthenticator, attributes, secret ); expect(responseAuth.length).toEqual(16); // Verify by recalculating const verified = RadiusAuthenticator.calculateResponseAuthenticator( code, identifier, requestAuthenticator, attributes, secret ); expect(responseAuth.equals(verified)).toBeTruthy(); }); tap.test('should calculate accounting request authenticator', async () => { const code = ERadiusCode.AccountingRequest; const identifier = 1; const attributes = Buffer.from([]); const secret = 'testing123'; const acctAuth = RadiusAuthenticator.calculateAccountingRequestAuthenticator( code, identifier, attributes, secret ); expect(acctAuth.length).toEqual(16); }); tap.test('should verify accounting request authenticator', async () => { const secret = 'testing123'; // Create an accounting request packet const packet = RadiusPacket.createAccountingRequest(1, secret, [ { type: ERadiusAttributeType.AcctStatusType, value: 1 }, // Start { type: ERadiusAttributeType.AcctSessionId, value: 'session-123' }, ]); // Verify the authenticator const isValid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, secret); expect(isValid).toBeTruthy(); // Should fail with wrong secret const isInvalid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, 'wrongsecret'); expect(isInvalid).toBeFalsy(); }); tap.test('should verify response authenticator', async () => { const identifier = 1; const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; // Create a response packet const responsePacket = RadiusPacket.createAccessAccept( identifier, requestAuthenticator, secret, [{ type: ERadiusAttributeType.ReplyMessage, value: 'Welcome' }] ); // Verify the authenticator const isValid = RadiusAuthenticator.verifyResponseAuthenticator( responsePacket, requestAuthenticator, secret ); expect(isValid).toBeTruthy(); // Should fail with wrong request authenticator const wrongRequestAuth = crypto.randomBytes(16); const isInvalid = RadiusAuthenticator.verifyResponseAuthenticator( responsePacket, wrongRequestAuth, secret ); expect(isInvalid).toBeFalsy(); // Should fail with wrong secret const isInvalid2 = RadiusAuthenticator.verifyResponseAuthenticator( responsePacket, requestAuthenticator, 'wrongsecret' ); expect(isInvalid2).toBeFalsy(); }); tap.test('should create packet header', async () => { const code = ERadiusCode.AccessRequest; const identifier = 42; const authenticator = crypto.randomBytes(16); const attributesLength = 50; const header = RadiusAuthenticator.createPacketHeader( code, identifier, authenticator, attributesLength ); expect(header.length).toEqual(20); expect(header[0]).toEqual(code); expect(header[1]).toEqual(identifier); expect(header.readUInt16BE(2)).toEqual(20 + attributesLength); expect(header.subarray(4, 20).equals(authenticator)).toBeTruthy(); }); tap.test('should calculate CHAP response', async () => { const chapId = 1; const password = 'testpassword'; const challenge = crypto.randomBytes(16); const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); expect(response.length).toEqual(16); // Verify the calculation: MD5(CHAP-ID + Password + Challenge) 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.equals(expected)).toBeTruthy(); }); tap.test('should verify CHAP response', async () => { const chapId = 1; const password = 'testpassword'; const challenge = crypto.randomBytes(16); // Calculate the response const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge); // Create CHAP-Password attribute format: CHAP-ID (1 byte) + Response (16 bytes) const chapPassword = Buffer.allocUnsafe(17); chapPassword.writeUInt8(chapId, 0); response.copy(chapPassword, 1); // Verify with correct password const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password); expect(isValid).toBeTruthy(); // Verify with wrong password const isInvalid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, 'wrongpassword'); expect(isInvalid).toBeFalsy(); }); tap.test('should calculate Message-Authenticator (HMAC-MD5)', async () => { const packet = Buffer.alloc(50); packet[0] = ERadiusCode.AccessRequest; packet[1] = 1; packet.writeUInt16BE(50, 2); crypto.randomBytes(16).copy(packet, 4); const secret = 'testing123'; const msgAuth = RadiusAuthenticator.calculateMessageAuthenticator(packet, secret); expect(msgAuth.length).toEqual(16); // Verify it's HMAC-MD5 const hmac = crypto.createHmac('md5', Buffer.from(secret, 'utf8')); hmac.update(packet); const expected = hmac.digest(); expect(msgAuth.equals(expected)).toBeTruthy(); }); export default tap.start();