feat(smartradius): Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation
This commit is contained in:
282
test/server/test.pap.ts
Normal file
282
test/server/test.pap.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as crypto from '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();
|
||||
Reference in New Issue
Block a user