283 lines
7.3 KiB
TypeScript
283 lines
7.3 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 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();
|