Files
smartradius/ts_server/classes.radiusauthenticator.ts

303 lines
8.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as plugins from './plugins.js';
import { ERadiusCode } from './interfaces.js';
/**
* RADIUS Authenticator handling
* Implements RFC 2865 and RFC 2866 authenticator calculations
*/
export class RadiusAuthenticator {
private static readonly AUTHENTICATOR_LENGTH = 16;
/**
* Generate random Request Authenticator for Access-Request
* RFC 2865: The Request Authenticator value is a 16 octet random number
*/
public static generateRequestAuthenticator(): Buffer {
return plugins.crypto.randomBytes(this.AUTHENTICATOR_LENGTH);
}
/**
* Calculate Response Authenticator for Access-Accept, Access-Reject, Access-Challenge
* RFC 2865: ResponseAuth = MD5(Code+ID+Length+RequestAuth+Attributes+Secret)
*/
public static calculateResponseAuthenticator(
code: ERadiusCode,
identifier: number,
requestAuthenticator: Buffer,
attributes: Buffer,
secret: string
): Buffer {
const length = 20 + attributes.length; // 20 = header size
const md5 = plugins.crypto.createHash('md5');
const header = Buffer.allocUnsafe(4);
header.writeUInt8(code, 0);
header.writeUInt8(identifier, 1);
header.writeUInt16BE(length, 2);
md5.update(header);
md5.update(requestAuthenticator);
md5.update(attributes);
md5.update(Buffer.from(secret, 'utf8'));
return md5.digest();
}
/**
* Calculate Accounting Request Authenticator
* RFC 2866: MD5(Code+ID+Length+16×0x00+Attrs+Secret)
*/
public static calculateAccountingRequestAuthenticator(
code: ERadiusCode,
identifier: number,
attributes: Buffer,
secret: string
): Buffer {
const length = 20 + attributes.length;
const zeroAuth = Buffer.alloc(this.AUTHENTICATOR_LENGTH, 0);
const md5 = plugins.crypto.createHash('md5');
const header = Buffer.allocUnsafe(4);
header.writeUInt8(code, 0);
header.writeUInt8(identifier, 1);
header.writeUInt16BE(length, 2);
md5.update(header);
md5.update(zeroAuth);
md5.update(attributes);
md5.update(Buffer.from(secret, 'utf8'));
return md5.digest();
}
/**
* Verify Accounting Request Authenticator
*/
public static verifyAccountingRequestAuthenticator(
packet: Buffer,
secret: string
): boolean {
if (packet.length < 20) {
return false;
}
const code = packet.readUInt8(0);
const identifier = packet.readUInt8(1);
const length = packet.readUInt16BE(2);
const receivedAuth = packet.subarray(4, 20);
const attributes = packet.subarray(20, length);
const expectedAuth = this.calculateAccountingRequestAuthenticator(
code,
identifier,
attributes,
secret
);
return plugins.crypto.timingSafeEqual(receivedAuth, expectedAuth);
}
/**
* Verify Response Authenticator
*/
public static verifyResponseAuthenticator(
responsePacket: Buffer,
requestAuthenticator: Buffer,
secret: string
): boolean {
if (responsePacket.length < 20) {
return false;
}
const code = responsePacket.readUInt8(0);
const identifier = responsePacket.readUInt8(1);
const length = responsePacket.readUInt16BE(2);
const receivedAuth = responsePacket.subarray(4, 20);
const attributes = responsePacket.subarray(20, length);
const expectedAuth = this.calculateResponseAuthenticator(
code,
identifier,
requestAuthenticator,
attributes,
secret
);
return plugins.crypto.timingSafeEqual(receivedAuth, expectedAuth);
}
/**
* Encrypt User-Password (PAP)
* RFC 2865 Section 5.2:
* b1 = MD5(S + RA) c(1) = p1 xor b1
* b2 = MD5(S + c(1)) c(2) = p2 xor b2
* ...
* bi = MD5(S + c(i-1)) c(i) = pi xor bi
*/
public static encryptPassword(
password: string,
requestAuthenticator: Buffer,
secret: string
): Buffer {
const passwordBuffer = Buffer.from(password, 'utf8');
// Pad password to multiple of 16 bytes
const paddedLength = Math.max(16, Math.ceil(passwordBuffer.length / 16) * 16);
const padded = Buffer.alloc(paddedLength, 0);
passwordBuffer.copy(padded);
// Limit to 128 bytes max
const maxLength = Math.min(paddedLength, 128);
const result = Buffer.allocUnsafe(maxLength);
let previousCipher = requestAuthenticator;
for (let i = 0; i < maxLength; i += 16) {
const md5 = plugins.crypto.createHash('md5');
md5.update(Buffer.from(secret, 'utf8'));
md5.update(previousCipher);
const b = md5.digest();
for (let j = 0; j < 16 && i + j < maxLength; j++) {
result[i + j] = padded[i + j] ^ b[j];
}
previousCipher = result.subarray(i, i + 16);
}
return result;
}
/**
* Decrypt User-Password (PAP)
* Reverse of encryptPassword
*/
public static decryptPassword(
encryptedPassword: Buffer,
requestAuthenticator: Buffer,
secret: string
): string {
if (encryptedPassword.length === 0 || encryptedPassword.length % 16 !== 0) {
throw new Error('Invalid encrypted password length');
}
const result = Buffer.allocUnsafe(encryptedPassword.length);
let previousCipher = requestAuthenticator;
for (let i = 0; i < encryptedPassword.length; i += 16) {
const md5 = plugins.crypto.createHash('md5');
md5.update(Buffer.from(secret, 'utf8'));
md5.update(previousCipher);
const b = md5.digest();
for (let j = 0; j < 16; j++) {
result[i + j] = encryptedPassword[i + j] ^ b[j];
}
previousCipher = encryptedPassword.subarray(i, i + 16);
}
// Remove null padding
let end = result.length;
while (end > 0 && result[end - 1] === 0) {
end--;
}
return result.subarray(0, end).toString('utf8');
}
/**
* Calculate CHAP Response
* RFC 2865: CHAP-Response = MD5(CHAP-ID + Password + Challenge)
*/
public static calculateChapResponse(
chapId: number,
password: string,
challenge: Buffer
): Buffer {
const md5 = plugins.crypto.createHash('md5');
md5.update(Buffer.from([chapId]));
md5.update(Buffer.from(password, 'utf8'));
md5.update(challenge);
return md5.digest();
}
/**
* Verify CHAP Response
* CHAP-Password format: CHAP Ident (1 byte) + String (16 bytes MD5 hash)
*/
public static verifyChapResponse(
chapPassword: Buffer,
challenge: Buffer,
password: string
): boolean {
if (chapPassword.length !== 17) {
return false;
}
const chapId = chapPassword[0];
const receivedResponse = chapPassword.subarray(1);
const expectedResponse = this.calculateChapResponse(chapId, password, challenge);
return plugins.crypto.timingSafeEqual(receivedResponse, expectedResponse);
}
/**
* Calculate Message-Authenticator (HMAC-MD5)
* Used for EAP and other extended authentication methods
* RFC 3579: HMAC-MD5 over entire packet with Message-Authenticator set to 16 zero bytes
*/
public static calculateMessageAuthenticator(
packet: Buffer,
secret: string
): Buffer {
// The packet should have Message-Authenticator attribute with 16 zero bytes
const hmac = plugins.crypto.createHmac('md5', Buffer.from(secret, 'utf8'));
hmac.update(packet);
return hmac.digest();
}
/**
* Verify Message-Authenticator
*/
public static verifyMessageAuthenticator(
packet: Buffer,
messageAuthenticator: Buffer,
messageAuthenticatorOffset: number,
secret: string
): boolean {
if (messageAuthenticator.length !== 16) {
return false;
}
// Create a copy of the packet with Message-Authenticator set to 16 zero bytes
const packetCopy = Buffer.from(packet);
Buffer.alloc(16, 0).copy(packetCopy, messageAuthenticatorOffset);
const expectedAuth = this.calculateMessageAuthenticator(packetCopy, secret);
return plugins.crypto.timingSafeEqual(messageAuthenticator, expectedAuth);
}
/**
* Create packet header
*/
public static createPacketHeader(
code: ERadiusCode,
identifier: number,
authenticator: Buffer,
attributesLength: number
): Buffer {
const header = Buffer.allocUnsafe(20);
const length = 20 + attributesLength;
header.writeUInt8(code, 0);
header.writeUInt8(identifier, 1);
header.writeUInt16BE(length, 2);
authenticator.copy(header, 4);
return header;
}
}
export default RadiusAuthenticator;