From 97e4f0f8f5e4482575700e1f2afea82ba4cb964f Mon Sep 17 00:00:00 2001 From: lwvemike Date: Mon, 15 Apr 2024 17:16:15 +0300 Subject: [PATCH] feat: add chap and pap methods of authentication --- packages/playground/server/tac_plus.conf | 178 ++-------------- packages/playground/src/index.ts | 86 ++++---- .../src/authentication/login/ascii.ts | 10 + .../src/authentication/login/chap.ts | 26 +++ .../src/authentication/login/pap.ts | 14 ++ packages/tacacs-plus/src/client/index.ts | 196 ++++++++++-------- packages/tacacs-plus/src/client/types.ts | 22 +- packages/tacacs-plus/src/types.ts | 16 ++ 8 files changed, 248 insertions(+), 300 deletions(-) create mode 100644 packages/tacacs-plus/src/authentication/login/ascii.ts create mode 100644 packages/tacacs-plus/src/authentication/login/chap.ts create mode 100644 packages/tacacs-plus/src/authentication/login/pap.ts diff --git a/packages/playground/server/tac_plus.conf b/packages/playground/server/tac_plus.conf index 55fa583..2525004 100644 --- a/packages/playground/server/tac_plus.conf +++ b/packages/playground/server/tac_plus.conf @@ -5,172 +5,24 @@ default authentication = file /etc/passwd accounting syslog; accounting file = /var/log/tac_plus/tac_plus.acct -# ACL for network_admin group - -acl = network_admin { - # allow access from all sources - permit = .* - # implicit deny (ie: anything else) -} - -# ACL for sys_admin group - -acl = sys_admin { - # allow access from 10.10.10.250 only - permit = .* - # permit = ^10\.10\.10\.2$ - # implicit deny (ie: anything else) -} - -# network_admin group, full access to network devices - - group = network_admin { - default service = permit - acl = network_admin - service = exec { - priv-lvl = 15 - } -} -# sys_admin group, only has read access to the network devices and can change the access vlan on an interface - - group = sys_admin { - default service = deny - expires = "Jan 1 2015" - acl = sys_admin - service = exec { - priv-lvl = 15 - } - cmd = enable { - permit .* - } - cmd = show { - permit .* - } - cmd = exit { - permit .* - } - cmd = configure { - permit .* - } - cmd = interface { - permit Ethernet.* - permit FastEthernet.* - permit GigabitEthernet.* - } - cmd = switchport { - permit "access vlan.*" - permit "trunk encapsulation.*" - permit "mode.*" - permit "trunk allowed vlan.*" - } - cmd = description { - permit .* - } -} - -#user1 -user = user1 { - default service = permit - pap = cleartext user1 - service = exec { - priv-lvl = 15 - security-role = security-admin - } -} - -#user2 -user = user2 { - pap = cleartext user2 - service = exec { - priv-lvl = 15 - } -} - -user = user_tac1 { - default service = permit - pap = cleartext tac - service = exec { - priv-lvl = 15 - security-role = security-admin - } -} -#user2 -user = user_tac2 { - pap = cleartext tac - service = exec { - priv-lvl = 15 - } -} - -user = user_command { - pap = cleartext com - service = exec { - priv-lvl = 15 - } +user = test_chap { + chap = cleartext test_chap_password + + service = management { + priv-lvl=15 + role="admin" + } } +user = test_login { + login = cleartext test_login_password -#user3 -user = user3 { - chap = cleartext user3 - service = exec { - priv-lvl = 15 - } + service = idk { + priv-lvl=10 + role="user" + } } -#user4 -user = user4 { - chap = cleartext user4 -} - -# User jonathanm using DES password and enable passwords - -user = jonathanm { - member = network_admin - login = des 6/1aYAL9zcCe. - enable = des dBFJQefS4S4Jw -} - -# User bob authenticating from the system /etc/passwd and the default enable password - -user = bob { - login = file /etc/passwd - member = sys_admin - service = exec { - priv-lvl = 11 - } -} - -user = root { - member = network_admin -} - -user = netop { - login = file /etc/passwd - member = network_admin -} - -user = admin { - pap = cleartext admin - member = network_admin -} - -user = sorin { - login = cleartext topcik - - service = nfa { - role = manager - car = audi - } -} - -user = cristi { - login = cleartext f30 - - service = nfa { - role = pussykiller - car = bmw - chubby = yes - } +user = test_pap { + pap = cleartext test_pap_password } - diff --git a/packages/playground/src/index.ts b/packages/playground/src/index.ts index 90f7d7d..8e2a340 100644 --- a/packages/playground/src/index.ts +++ b/packages/playground/src/index.ts @@ -1,11 +1,10 @@ -import { Client, Header, PRIVILEGE_LEVELS } from '@noction/tacacs-plus' +import type { AuthenType } from '@noction/tacacs-plus' +import { AUTHEN_TYPES, Client, PRIVILEGE_LEVELS } from '@noction/tacacs-plus' const client = new Client({ host: '127.0.0.1', port: 49, - // secret: 'testing123', - majorVersion: Header.MAJOR_VERSIONS.TAC_PLUS_MAJOR_VER, - minorVersion: Header.MINOR_VERSIONS.TAC_PLUS_MINOR_VER_DEFAULT, + // secret: 'tac_test', logger: { // TODO(lwvemike): remove before release /* eslint-disable-next-line no-console */ @@ -18,43 +17,52 @@ const client = new Client({ }, }) +const FUNCTION = 'authentication' as 'authentication' | 'authorization' +const SERVICES = ['management', 'idk'] +const CREDENTIALS = { + username: 'test_pap', + password: 'test_pap_password', +} as const +const SELECTED_AUTHEN_TYPE: AuthenType = AUTHEN_TYPES.TAC_PLUS_AUTHEN_TYPE_PAP + ;(async () => { - const CREDENTIALS: Record<'username' | 'password', string> = { - username: 'cristi', - password: 'f30', + if (FUNCTION === 'authentication') { + return await authenticatinon() } - const SERVICE = 'authorization' as 'authorization' | 'authentication' - - if (SERVICE === 'authentication') { - try { - const res = await client.authenticateASCII({ - username: CREDENTIALS.username, - password: CREDENTIALS.password, - privLvl: PRIVILEGE_LEVELS.TAC_PLUS_PRIV_LVL_ROOT, - }) - - // eslint-disable-next-line no-console - console.log(res) - } - catch (err) { - // eslint-disable-next-line no-console - console.log(err) - } + return await authorization() +})() + +async function authorization() { + try { + const res = await client.authorize({ + username: CREDENTIALS.username, + services: SERVICES, + }) + + // eslint-disable-next-line no-console + console.log(res) } - else if (SERVICE === 'authorization') { - try { - const res = await client.authorize({ - username: CREDENTIALS.username, - services: ['nfa'], - }) - - // eslint-disable-next-line no-console - console.log(res) - } - catch (err) { - // eslint-disable-next-line no-console - console.log(err) - } + catch (err) { + // eslint-disable-next-line no-console + console.log(err) } -})() +} + +async function authenticatinon() { + try { + const res = await client.authenticate({ + username: CREDENTIALS.username, + password: CREDENTIALS.password, + privLvl: PRIVILEGE_LEVELS.TAC_PLUS_PRIV_LVL_ROOT, + authenType: SELECTED_AUTHEN_TYPE, + }) + + // eslint-disable-next-line no-console + console.log(res) + } + catch (err) { + // eslint-disable-next-line no-console + console.log(err) + } +} diff --git a/packages/tacacs-plus/src/authentication/login/ascii.ts b/packages/tacacs-plus/src/authentication/login/ascii.ts new file mode 100644 index 0000000..afc9bba --- /dev/null +++ b/packages/tacacs-plus/src/authentication/login/ascii.ts @@ -0,0 +1,10 @@ +import { Buffer } from 'node:buffer' +import { Header } from '../../header' +import type { CreateAuthenticationDataReturn } from '../../types' + +export function createAsciiAuthenticationData(): CreateAuthenticationDataReturn { + return { + data: Buffer.alloc(0), + minorVersion: Header.MINOR_VERSIONS.TAC_PLUS_MINOR_VER_DEFAULT, + } +} diff --git a/packages/tacacs-plus/src/authentication/login/chap.ts b/packages/tacacs-plus/src/authentication/login/chap.ts new file mode 100644 index 0000000..28d204c --- /dev/null +++ b/packages/tacacs-plus/src/authentication/login/chap.ts @@ -0,0 +1,26 @@ +import { Buffer } from 'node:buffer' +import { createHash, randomBytes } from 'node:crypto' +import type { CreateAuthenticationDataReturn } from '../../types' +import { Header } from '../../header' + +export function createChapAuthenticationData(password: string): CreateAuthenticationDataReturn { + const pppId = randomBytes(1) + const challenge = randomBytes(49) + + const data = Buffer.concat([ + pppId, + challenge, + createHash('md5').update( + Buffer.concat([ + pppId, + Buffer.from(password), + challenge, + ]), + ).digest(), + ]) + + return { + data, + minorVersion: Header.MINOR_VERSIONS.TAC_PLUS_MINOR_VER_ONE, + } +} diff --git a/packages/tacacs-plus/src/authentication/login/pap.ts b/packages/tacacs-plus/src/authentication/login/pap.ts new file mode 100644 index 0000000..ec09b48 --- /dev/null +++ b/packages/tacacs-plus/src/authentication/login/pap.ts @@ -0,0 +1,14 @@ +import { Buffer } from 'node:buffer' +import { Header } from '../../header' +import type { CreateAuthenticationDataReturn } from '../../types' + +/** + * @deprecated + * @description In Tacacs+ RFC this is obsolete because of security implications + */ +export function createPapAuthenticationData(password: string): CreateAuthenticationDataReturn { + return { + minorVersion: Header.MINOR_VERSIONS.TAC_PLUS_MINOR_VER_ONE, + data: Buffer.from(password, 'ascii'), + } +} diff --git a/packages/tacacs-plus/src/client/index.ts b/packages/tacacs-plus/src/client/index.ts index 1aa7da2..1c091cf 100644 --- a/packages/tacacs-plus/src/client/index.ts +++ b/packages/tacacs-plus/src/client/index.ts @@ -1,9 +1,9 @@ import type { Socket, TcpNetConnectOpts } from 'node:net' import { createConnection } from 'node:net' import type { Buffer } from 'node:buffer' -import type { HeaderRecord } from '../header/types' import { Header } from '../header' import { Packet } from '../packet' +import type { AuthenType } from '../common' import { AUTHEN_METHODS, AUTHEN_SERVICE, AUTHEN_TYPES, PRIVILEGE_LEVELS } from '../common' import { TacacsError, createClientSequenceNumberGenerator, createServices, getNameFromCollectionValue, notImplemented, randomInt } from '../utils' import { AuthenticationReply } from '../authentication/authentication-reply' @@ -13,14 +13,15 @@ import type { Arg } from '../authorization/authorization-reply/types' import { AuthorizationReply } from '../authorization/authorization-reply' import { DEFAULT_LOGGER } from '../constants' import { AuthenticationContinue } from '../authentication/authentication-continue' +import { createChapAuthenticationData } from '../authentication/login/chap' +import { createAsciiAuthenticationData } from '../authentication/login/ascii' +import { createPapAuthenticationData } from '../authentication/login/pap' import type { AuthenticateArgs, AuthorizeArgs, Logger, Options } from './types' export class Client { readonly #host: Options['host'] readonly #port: Options['port'] readonly #secret: Options['secret'] - readonly #majorVersion: HeaderRecord['majorVersion'] - readonly #minorVersion: HeaderRecord['minorVersion'] readonly #logger: Logger readonly #socketTimeout: number readonly #debug: boolean @@ -29,8 +30,6 @@ export class Client { this.#host = options.host this.#port = options.port this.#secret = options.secret - this.#majorVersion = options.majorVersion - this.#minorVersion = options.minorVersion this.#debug = options.debug ?? false this.#socketTimeout = options.socketTimeout ?? 10_000 this.#logger = options.logger ?? DEFAULT_LOGGER @@ -53,85 +52,6 @@ export class Client { } } - async authenticateASCII({ username, password, privLvl }: AuthenticateArgs): Promise { - return new Promise((resolve, reject) => { - const socket = this.createSocket() - - const getNextSeqNo = createClientSequenceNumberGenerator() - const sessionId = randomInt() - - const body = new AuthenticationStart({ - action: AuthenticationStart.ACTIONS.TAC_PLUS_AUTHEN_LOGIN, - authenType: AUTHEN_TYPES.TAC_PLUS_AUTHEN_TYPE_ASCII, - authenService: AUTHEN_SERVICE.TAC_PLUS_AUTHEN_SVC_NONE, - privLvl, - username, - }).toBuffer() - - const header = this.createGenericASCIIAuthenticationHeader({ bodyLength: body.length, seqNo: getNextSeqNo(), sessionId }) - - const authStartPacket = new Packet(header, body, this.#secret) - - this.#logger.debug(`\nSent:\n${authStartPacket.toHumanReadable()}\n`) - - socket.write(authStartPacket.toBuffer()) - - socket.on('data', (data: Buffer) => { - const decodedPacket = Packet.decodePacket(data, this.#secret) - - this.#logger.debug(`Received:\n${decodedPacket.toHumanReadable()}\n`) - - const authenticationReply = new AuthenticationReply(decodedPacket.body, decodedPacket.header.length) - - switch (authenticationReply.status) { - case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_GETPASS: { - const bodyBuffer = new AuthenticationContinue({ userMsg: password }).toBuffer() - const innerHeader = this.createGenericASCIIAuthenticationHeader({ bodyLength: bodyBuffer.length, seqNo: getNextSeqNo(), sessionId }) - - const authContinuePacket = new Packet(innerHeader, bodyBuffer, this.#secret) - - this.#logger.debug(`\nSent:\n${authContinuePacket.toHumanReadable()}\n`) - - return socket.write(authContinuePacket.toBuffer()) - } - case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_PASS: { - socket.destroy() - this.#logger.log('Authentication PASS') - return resolve(true) - } - case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_FAIL: { - socket.destroy() - const error = new TacacsError('Authentication Failed', getNameFromCollectionValue(authenticationReply.status, AuthenticationReply.STATUSES)) - this.#logger.error(error.message) - return reject(error) - } - case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_ERROR: { - socket.destroy() - const error = new TacacsError('Authentication Errored', getNameFromCollectionValue(authenticationReply.status, AuthenticationReply.STATUSES)) - this.#logger.error(error.message) - return reject(error) - } - default: { - const error = new TacacsError('Unknown', 'Unknown') - reject(error) - } - } - }) - }) - } - - private createGenericASCIIAuthenticationHeader({ bodyLength, seqNo, sessionId }: { sessionId: number, seqNo: number, bodyLength: number }) { - return new Header({ - majorVersion: this.#majorVersion, - minorVersion: this.#minorVersion, - type: Header.TYPES.TAC_PLUS_AUTHEN, - flags: (this.#secret === undefined ? Header.FLAGS.TAC_PLUS_UNENCRYPTED_FLAG : 0), - seqNo, - sessionId, - length: bodyLength, - }) - } - async authorize({ username, services }: AuthorizeArgs): Promise { return new Promise((resolve, reject) => { const socket = this.createSocket() @@ -149,8 +69,8 @@ export class Client { }).toBuffer() const header = new Header({ - majorVersion: this.#majorVersion, - minorVersion: this.#minorVersion, + majorVersion: Header.MAJOR_VERSIONS.TAC_PLUS_MAJOR_VER, + minorVersion: Header.MINOR_VERSIONS.TAC_PLUS_MINOR_VER_DEFAULT, type: Header.TYPES.TAC_PLUS_AUTHOR, flags: (this.#secret === undefined ? Header.FLAGS.TAC_PLUS_UNENCRYPTED_FLAG : 0), seqNo: getNextSeqNo(), @@ -231,4 +151,108 @@ export class Client { return socket } + + async authenticate({ username, password, privLvl, authenType }: AuthenticateArgs): Promise { + return new Promise((resolve, reject) => { + const socket = this.createSocket() + + const getNextSeqNo = createClientSequenceNumberGenerator() + const sessionId = randomInt() + + const { data, minorVersion } = this.getAuthenticationData(authenType, password) + + const body = new AuthenticationStart({ + action: AuthenticationStart.ACTIONS.TAC_PLUS_AUTHEN_LOGIN, + authenType, + authenService: AUTHEN_SERVICE.TAC_PLUS_AUTHEN_SVC_NONE, + privLvl, + username, + data, + }).toBuffer() + + const header = new Header({ + majorVersion: Header.MAJOR_VERSIONS.TAC_PLUS_MAJOR_VER, + minorVersion, + type: Header.TYPES.TAC_PLUS_AUTHEN, + flags: (this.#secret === undefined ? Header.FLAGS.TAC_PLUS_UNENCRYPTED_FLAG : 0), + seqNo: getNextSeqNo(), + sessionId, + length: body.length, + }) + + const authStartPacket = new Packet(header, body, this.#secret) + + this.#logger.debug(`\nSent:\n${authStartPacket.toHumanReadable()}\n`) + + socket.write(authStartPacket.toBuffer()) + + socket.on('data', (data: Buffer) => { + const decodedPacket = Packet.decodePacket(data, this.#secret) + + this.#logger.debug(`Received:\n${decodedPacket.toHumanReadable()}\n`) + + const authenticationReply = new AuthenticationReply(decodedPacket.body, decodedPacket.header.length) + + switch (authenticationReply.status) { + case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_GETPASS: { + const bodyBuffer = new AuthenticationContinue({ userMsg: password }).toBuffer() + + const innerHeader = new Header({ + majorVersion: Header.MAJOR_VERSIONS.TAC_PLUS_MAJOR_VER, + minorVersion, + type: Header.TYPES.TAC_PLUS_AUTHEN, + flags: (this.#secret === undefined ? Header.FLAGS.TAC_PLUS_UNENCRYPTED_FLAG : 0), + seqNo: getNextSeqNo(), + sessionId, + length: bodyBuffer.length, + }) + + const authContinuePacket = new Packet(innerHeader, bodyBuffer, this.#secret) + + this.#logger.debug(`\nSent:\n${authContinuePacket.toHumanReadable()}\n`) + + return socket.write(authContinuePacket.toBuffer()) + } + case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_PASS: { + socket.destroy() + this.#logger.log('Authentication PASS') + return resolve(true) + } + case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_FAIL: { + socket.destroy() + const error = new TacacsError('Authentication Failed', getNameFromCollectionValue(authenticationReply.status, AuthenticationReply.STATUSES)) + this.#logger.error(error.message) + return reject(error) + } + case AuthenticationReply.STATUSES.TAC_PLUS_AUTHEN_STATUS_ERROR: { + socket.destroy() + const error = new TacacsError('Authentication Errored', getNameFromCollectionValue(authenticationReply.status, AuthenticationReply.STATUSES)) + this.#logger.error(error.message) + return reject(error) + } + default: { + const error = new TacacsError('Unknown', 'Unknown') + reject(error) + } + } + }) + }) + } + + private getAuthenticationData(authenType: AuthenType, password: string) { + switch (authenType) { + case AUTHEN_TYPES.TAC_PLUS_AUTHEN_TYPE_ASCII: { + return createAsciiAuthenticationData() + } + case AUTHEN_TYPES.TAC_PLUS_AUTHEN_TYPE_CHAP: { + return createChapAuthenticationData(password) + } + case AUTHEN_TYPES.TAC_PLUS_AUTHEN_TYPE_PAP: { + return createPapAuthenticationData(password) + } + default: { + notImplemented(`${authenType} Authentication Type`) + } + } + } } diff --git a/packages/tacacs-plus/src/client/types.ts b/packages/tacacs-plus/src/client/types.ts index 243144a..ddba06d 100644 --- a/packages/tacacs-plus/src/client/types.ts +++ b/packages/tacacs-plus/src/client/types.ts @@ -1,5 +1,4 @@ -import type { PrivLevel } from '../common' -import type { HeaderRecord } from '../header/types' +import type { AuthenType, PrivLevel } from '../common' import type { Secret } from '../types' export interface Logger { @@ -9,18 +8,17 @@ export interface Logger { warn: (message: string) => void } -export type Options = - & { - host: string - port: number - secret?: Secret - logger?: Logger - debug?: boolean - socketTimeout?: number - } - & Pick +export interface Options { + host: string + port: number + secret?: Secret + logger?: Logger + debug?: boolean + socketTimeout?: number +} export interface AuthenticateArgs { + authenType: AuthenType username: string password: string privLvl: PrivLevel diff --git a/packages/tacacs-plus/src/types.ts b/packages/tacacs-plus/src/types.ts index f4315ab..9463dce 100644 --- a/packages/tacacs-plus/src/types.ts +++ b/packages/tacacs-plus/src/types.ts @@ -1,4 +1,5 @@ import type { Buffer } from 'node:buffer' +import type { MinorVersion } from './header/types' export interface ToHumanReadable { toHumanReadable: () => string @@ -8,8 +9,23 @@ export interface ToBuffer { toBuffer: () => Buffer } +/** + * @description This interface should be used to create specific authentication data for different authentication types + */ +export interface GetAuthenticationData { + getAuthenticationData: () => { + authenticationData: Buffer + minorVersion: MinorVersion + } +} + export type Secret = string | undefined +export interface CreateAuthenticationDataReturn { + data: Buffer + minorVersion: MinorVersion +} + export * from './authentication/authentication-continue/types' export * from './authentication/authentication-start/types' export * from './authentication/authentication-reply/types'