import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as crypto from 'node:crypto'; import { RadiusAuthenticator } from '../../ts_server/index.js'; tap.test('should encrypt and decrypt short password (< 16 chars)', async () => { const password = 'secret'; const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); // Encrypted length should be multiple of 16 expect(encrypted.length).toEqual(16); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should encrypt and decrypt exactly 16-char password', async () => { const password = '1234567890123456'; // Exactly 16 characters const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); expect(encrypted.length).toEqual(16); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should encrypt and decrypt long password (> 16 chars)', async () => { const password = 'thisisaverylongpasswordthatexceeds16characters'; const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); // Should be padded to next multiple of 16 expect(encrypted.length % 16).toEqual(0); expect(encrypted.length).toBeGreaterThan(16); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should encrypt and decrypt password near max length (128 chars)', async () => { const password = 'a'.repeat(120); // Near max of 128 const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); // Should be limited to 128 bytes expect(encrypted.length).toBeLessThanOrEqual(128); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should truncate password exceeding 128 chars', async () => { const password = 'a'.repeat(150); // Exceeds max const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); // Should be exactly 128 bytes (max) expect(encrypted.length).toEqual(128); // Decrypted will be truncated to 128 chars const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted.length).toBeLessThanOrEqual(128); }); tap.test('should handle empty password', async () => { const password = ''; const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); // Even empty password is padded to 16 bytes expect(encrypted.length).toEqual(16); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should fail to decrypt with wrong secret', async () => { const password = 'mypassword'; const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const wrongSecret = 'wrongsecret'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, wrongSecret ); // Should not match original password expect(decrypted).not.toEqual(password); }); tap.test('should fail to decrypt with wrong authenticator', async () => { const password = 'mypassword'; const requestAuthenticator = crypto.randomBytes(16); const wrongAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, wrongAuthenticator, secret ); // Should not match original password expect(decrypted).not.toEqual(password); }); tap.test('should handle special characters in password', async () => { const password = '!@#$%^&*()_+-=[]{}|;:,.<>?'; const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should handle unicode characters in password', async () => { const password = 'パスワード密码'; // Japanese + Chinese const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; const encrypted = RadiusAuthenticator.encryptPassword( password, requestAuthenticator, secret ); const decrypted = RadiusAuthenticator.decryptPassword( encrypted, requestAuthenticator, secret ); expect(decrypted).toEqual(password); }); tap.test('should reject invalid encrypted password length', async () => { const requestAuthenticator = crypto.randomBytes(16); const secret = 'testing123'; // Not a multiple of 16 const invalidEncrypted = Buffer.alloc(15); let error: Error | undefined; try { RadiusAuthenticator.decryptPassword(invalidEncrypted, requestAuthenticator, secret); } catch (e) { error = e as Error; } expect(error).toBeDefined(); expect(error!.message).toInclude('Invalid'); }); tap.test('PAP encryption matches RFC 2865 algorithm', async () => { // Test that our implementation matches the RFC 2865 algorithm: // b1 = MD5(S + RA) c(1) = p1 xor b1 // b2 = MD5(S + c(1)) c(2) = p2 xor b2 const password = 'testpassword12345678'; // Longer than 16 to test chaining const requestAuth = Buffer.alloc(16, 0x42); // Fixed for deterministic test const secret = 'sharedsecret'; // Our implementation const encrypted = RadiusAuthenticator.encryptPassword(password, requestAuth, secret); // Manual calculation per RFC const paddedLength = Math.ceil(password.length / 16) * 16; const padded = Buffer.alloc(paddedLength, 0); Buffer.from(password, 'utf8').copy(padded); const expected = Buffer.alloc(paddedLength); let previousCipher = requestAuth; for (let i = 0; i < paddedLength; i += 16) { const md5 = crypto.createHash('md5'); md5.update(Buffer.from(secret, 'utf8')); md5.update(previousCipher); const b = md5.digest(); for (let j = 0; j < 16; j++) { expected[i + j] = padded[i + j] ^ b[j]; } previousCipher = expected.subarray(i, i + 16); } expect(encrypted.equals(expected)).toBeTruthy(); }); export default tap.start();