Files

283 lines
7.3 KiB
TypeScript
Raw Permalink Normal View History

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();