Skip to content

Commit

Permalink
feat: add authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Mihai Plamadeala authored and LwveMike committed Apr 16, 2024
1 parent 422c212 commit 6022577
Show file tree
Hide file tree
Showing 12 changed files with 972 additions and 303 deletions.
2 changes: 0 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export default antfu(
ignores: [
// eslint ignore globs here
],
},
{
rules: {
curly: ['error', 'all'],
},
Expand Down
155 changes: 98 additions & 57 deletions src/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Buffer } from 'node:buffer'
import { Buffer } from 'node:buffer'
import type { HeaderRecord } from './header'
import { isOdd } from './helpers'
import type { AuthenService, AuthenType, PrivLevel } from './common'
import { AUTHEN_SERVICE, isAuthenService, isAuthenType, isPrivLevel } from './common'

export const AUTH_START_ACTIONS = {
TAC_PLUS_AUTHEN_LOGIN: 0x01,
Expand All @@ -16,56 +17,13 @@ function isAuthStartAction(maybeAction: number): maybeAction is AuthStartAction
return (ALLOWED_AUTH_START_ACTIONS_VALUES as number[]).includes(maybeAction)
}

// Used privileges
// interface PrivilegeLevels {
// TAC_PLUS_PRIV_LVL_MIN: 0x00
// TAC_PLUS_PRIV_LVL_USER: 0x01
// TAC_PLUS_PRIV_LVL_ROOT: 0x0F
// TAC_PLUS_PRIV_LVL_MAX: 0x0F
// }

type PrivLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15

function isPrivLevel(maybePrivLevel: number): maybePrivLevel is PrivLevel {
return maybePrivLevel > -1 && maybePrivLevel < 16
}

export const AUTHEN_TYPES = {
TAC_PLUS_AUTHEN_TYPE_ASCII: 0x01,
TAC_PLUS_AUTHEN_TYPE_PAP: 0x02,
TAC_PLUS_AUTHEN_TYPE_CHAP: 0x03,
TAC_PLUS_AUTHEN_TYPE_MSCHAP: 0x05,
TAC_PLUS_AUTHEN_TYPE_MSCHAPV2: 0x06,
export const PrivilegeLevels = {
TAC_PLUS_PRIV_LVL_MIN: 0x00,
TAC_PLUS_PRIV_LVL_USER: 0x01,
TAC_PLUS_PRIV_LVL_ROOT: 0x0F,
TAC_PLUS_PRIV_LVL_MAX: 0x0F,
} as const

const ALLOWED_AUTHEN_TYPES = Object.values(AUTHEN_TYPES)

type AuthenType = typeof ALLOWED_AUTHEN_TYPES[number]

function isAuthenType(maybeAuthenType: number): maybeAuthenType is AuthenType {
return (ALLOWED_AUTHEN_TYPES as number[]).includes(maybeAuthenType)
}

export const AUTHEN_SERVICE = {
TAC_PLUS_AUTHEN_SVC_NONE: 0x00,
TAC_PLUS_AUTHEN_SVC_LOGIN: 0x01,
TAC_PLUS_AUTHEN_SVC_ENABLE: 0x02,
TAC_PLUS_AUTHEN_SVC_PPP: 0x03,
TAC_PLUS_AUTHEN_SVC_PT: 0x05,
TAC_PLUS_AUTHEN_SVC_RCMD: 0x06,
TAC_PLUS_AUTHEN_SVC_X25: 0x07,
TAC_PLUS_AUTHEN_SVC_NASI: 0x08,
TAC_PLUS_AUTHEN_SVC_FWPROXY: 0x09,
} as const

const ALLOWED_AUTHEN_SERVICE = Object.values(AUTHEN_SERVICE)

type AuthenService = typeof ALLOWED_AUTHEN_SERVICE[number]

function isAuthenService(maybeAuthenService: number): maybeAuthenService is AuthenService {
return (ALLOWED_AUTHEN_SERVICE as number[]).includes(maybeAuthenService)
}

interface AuthStartRecord {
action: AuthStartAction
privLvl: PrivLevel
Expand Down Expand Up @@ -132,6 +90,7 @@ function isStatus(maybeStatus: number): maybeStatus is Status {
}

export const AUTH_REPLY_FLAGS = {
TAC_PLUS_REPLY_NO_ACTION: 0x00,
TAC_PLUS_REPLY_FLAG_NOECHO: 0x01,
} as const

Expand Down Expand Up @@ -205,6 +164,26 @@ function validateAuthContinue({ userMessageLength, dataLength, flags }: UnknownA
}
}

interface CreateAuthStartArgs {
action: AuthStartAction
privLvl?: PrivLevel
authenType: AuthenType
authenService: AuthenService
username: string
port?: string
remAddr?: string
data?: Buffer
}

const TAC_PLUS_VIRTUAL_PORT = ''
const TAC_PLUS_VIRTUAL_REM_ADDR = ''

interface CreateAuthContinueArgs {
password: string
flags: number
data?: Buffer
}

export class Authentication {
static decodeAuthStart(data: Buffer, length: HeaderRecord['length']): AuthStartRecord {
if (data.length < Authentication.START_MIN_LENGTH) {
Expand Down Expand Up @@ -330,18 +309,80 @@ export class Authentication {
}
}

static handleAuthentication(data: Buffer, header: HeaderRecord) {
const shouldAuthContinue = isOdd(header.seqNo)
static createAuthStart(options: CreateAuthStartArgs) {
const username = options.username
const action = AUTH_START_ACTIONS.TAC_PLUS_AUTHEN_LOGIN
const privLvl = options.privLvl ?? PrivilegeLevels.TAC_PLUS_PRIV_LVL_MIN
const authenType = options.authenType
const service = AUTHEN_SERVICE.TAC_PLUS_AUTHEN_SVC_LOGIN
const data = options.data ?? Buffer.alloc(0)
const remAddr = options.remAddr ?? TAC_PLUS_VIRTUAL_REM_ADDR
const port = options.port ?? TAC_PLUS_VIRTUAL_PORT

const usernameBuffer = Buffer.from(username)
const remAddrBuffer = Buffer.from(remAddr)
const portBuffer = Buffer.from(port)

const header = Buffer.alloc(8)
header.writeUInt8(action, 0)
header.writeUInt8(privLvl, 1)
header.writeUInt8(authenType, 2)
header.writeUInt8(service, 3)
header.writeUInt8(usernameBuffer.length, 4)
header.writeUInt8(portBuffer.length, 5)
header.writeUInt8(remAddrBuffer.length, 6)
header.writeUInt8(data.length, 7)

return Buffer.concat([header, usernameBuffer, portBuffer, remAddrBuffer, data])
}

static createAuthContinue(options: any) {
options.flags = options.flags ?? 0x00
options.userMessage = options.userMessage || null
options.data = options.data || null

const bSize = 2 + 2 + 1 + (options.userMessage ? options.userMessage.length : 0) + (options.data ? options.data.length : 0)
let resp = Buffer.alloc(bSize)
let offset = 0

if (header.seqNo === 1) {
return Authentication.decodeAuthStart(data, header.length)
resp.writeInt16BE(options.userMessage ? options.userMessage.length : 0, offset)
offset += 2
resp.writeInt16BE(options.data ? options.data.length : 0, offset)
offset += 2

resp.writeUInt8(options.flags, offset)
offset += 1

if (options.userMessage) {
resp.write(options.userMessage, offset)
offset += options.userMessage.length
}

if (shouldAuthContinue) {
return Authentication.decodeAuthContinue(data, header.length)
if (options.data) {
if (options.data instanceof Buffer) {
resp = Buffer.concat([resp, options.data])
}

else { resp.write(options.data, offset) }

offset += options.data.length
}

return Authentication.decodeAuthReply(data, header.length)
return resp
}

static createAuthContinue2(args: CreateAuthContinueArgs) {
const password = args.password
const data = args.data ?? Buffer.alloc(0)
const flags = args.flags ?? 0 // TODO(lwvemike): see why this is needed 0

const passwordBuffer = Buffer.from(password)
const header = Buffer.alloc(5)
header.writeUInt16BE(passwordBuffer.length, 0)
header.writeUInt16BE(data.length, 2)
header.writeUInt8(flags, 4)

return Buffer.concat([header, passwordBuffer, data])
}

static readonly START_MIN_LENGTH = 8
Expand Down
154 changes: 154 additions & 0 deletions src/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Buffer } from 'node:buffer'
import { AUTHEN_TYPES, isAuthenService, isPrivLevel } from './common'

export const AUTHEN_METHODS = {
TAC_PLUS_AUTHEN_METH_NOT_SET: 0x00,
TAC_PLUS_AUTHEN_METH_NONE: 0x01,
TAC_PLUS_AUTHEN_METH_KRB5: 0x02,
TAC_PLUS_AUTHEN_METH_LINE: 0x03,
TAC_PLUS_AUTHEN_METH_ENABLE: 0x04,
TAC_PLUS_AUTHEN_METH_LOCAL: 0x05,
TAC_PLUS_AUTHEN_METH_TACACSPLUS: 0x06,
TAC_PLUS_AUTHEN_METH_GUEST: 0x08,
TAC_PLUS_AUTHEN_METH_RADIUS: 0x10,
TAC_PLUS_AUTHEN_METH_KRB4: 0x11,
TAC_PLUS_AUTHEN_METH_RCMD: 0x20,
} as const

const ALLOWED_AUTHEN_METHODS = Object.values(AUTHEN_METHODS)

type AuthenMethod = typeof ALLOWED_AUTHEN_METHODS[number]

function isAuthenMethod(maybeAuthenMethod: number): maybeAuthenMethod is AuthenMethod {
return (ALLOWED_AUTHEN_METHODS as number[]).includes(maybeAuthenMethod)
}

export const AUTHORIZATION_AUTHEN_TYPES = {
...AUTHEN_TYPES,
TAC_PLUS_AUTHEN_TYPE_NOT_SET: 0x00,
} as const

export const ALLOWED_AUTHORIZATION_AUTHEN_TYPES = Object.values(AUTHORIZATION_AUTHEN_TYPES)

export type AuthorizationAuthenType = typeof ALLOWED_AUTHORIZATION_AUTHEN_TYPES[number]

interface UnknownAuthorizationRequest {
authenMethod: number
privLvl: number
authenType: number
authenService: number
}

function _validateAuthorizationRequest({ authenMethod, privLvl, authenType, authenService }: UnknownAuthorizationRequest) {
if (!isAuthenMethod(authenMethod)) {
throw new Error('Invalid authen method')
}

if (!isPrivLevel(privLvl)) {
throw new Error('Invalid privilege level')
}

if (!isAuthorizationAuthenType(authenType)) {
throw new Error('Invalid authen type')
}

if (!isAuthenService(authenService)) {
throw new Error('Invalid authen service')
}

return {
authenMethod,
privLvl,
authenType,
authenService,
}
}

export function isAuthorizationAuthenType(maybeAuthenType: number): maybeAuthenType is AuthorizationAuthenType {
return (ALLOWED_AUTHORIZATION_AUTHEN_TYPES as number[]).includes(maybeAuthenType)
}

export const AUTHORIZATION_STATUS = {
TAC_PLUS_AUTHOR_STATUS_PASS_ADD: 0x01,
TAC_PLUS_AUTHOR_STATUS_PASS_REPL: 0x02,
TAC_PLUS_AUTHOR_STATUS_FAIL: 0x10,
TAC_PLUS_AUTHOR_STATUS_ERROR: 0x11,
TAC_PLUS_AUTHOR_STATUS_FOLLOW: 0x21,
} as const

export const ALLOWED_AUTHORIZATION_STATUS = Object.values(AUTHORIZATION_STATUS)

type AuthorizationStatus = typeof ALLOWED_AUTHORIZATION_STATUS[number]

function isAuthorizationStatus(maybeStatus: number): maybeStatus is AuthorizationStatus {
return (ALLOWED_AUTHORIZATION_STATUS as number[]).includes(maybeStatus)
}

interface UnknownAuthorizationReply {
status: number
}

function _validateAuthReply({ status }: UnknownAuthorizationReply) {
if (!isAuthorizationStatus(status)) {
throw new Error('Invalid status')
}

return { status }
}

export class Authorization {
static createAuthRequest(args: CreateAuthorizationRequestArgs) {
const username = args.username
const authenMethod = args.authenMethod
const privLvl = args.privLvl
const authenType = args.authenType
const service = args.service
const argss = args.arguments ?? []
const remAddr = args.remAddr ?? ''
const port = args.port ?? ''

const usernameBuffer = Buffer.from(username)
const remAddrBuffer = Buffer.from(remAddr)
const portBuffer = Buffer.from(port)

const headerLength = 8
const argLenSum = argss.reduce((sum, arg) => sum + Buffer.byteLength(arg), 0)
const totalLength = headerLength + usernameBuffer.length + remAddrBuffer.length + portBuffer.length + argLenSum

const header = Buffer.alloc(totalLength)
header.writeUInt8(authenMethod, 0)
header.writeUInt8(privLvl, 1)
header.writeUInt8(authenType, 2)
header.writeUInt8(service, 3)
header.writeUInt8(usernameBuffer.length, 4)
header.writeUInt8(portBuffer.length, 5)
header.writeUInt8(remAddrBuffer.length, 6)
header.writeUInt8(argss.length, 7)

let offset = headerLength
for (const arg of argss) {
const argBuffer = Buffer.from(arg)
header.writeUInt8(argBuffer.length, offset)
offset++
}

offset = headerLength
header.write(username, offset)
offset += usernameBuffer.length
header.write(port, offset)
offset += portBuffer.length
header.write(remAddr, offset)
offset += remAddrBuffer.length

for (const arg of argss) {
const argBuffer = Buffer.from(arg)
header.write(argBuffer.toString(), offset)
offset += argBuffer.length
}

return header
}

static readonly REQUEST_MIN_LENGTH = 8
static readonly REPLY_MIN_LENGTH = 6
}
Loading

0 comments on commit 6022577

Please sign in to comment.