UNPKG

cripta

Version:

A layer of encryption a little too judicious

250 lines (195 loc) 6.47 kB
import crypto from 'node:crypto' import { DataBag, Timestamp } from 't0n' import { Claim } from './claim' import { Config, Cripta, sha256 } from '../' export class Token { claims: DataBag = new DataBag() header: DataBag = new DataBag() headerString: string = '' body: any = Symbol('UNDEFINED') bodyString: string = '' signature: string = '' #same: boolean = false #sameConfig: Config = new Config({entropy: 0}) #leeway: number = 0 #cripta: Cripta = new Cripta() #separator: string = '.' secret(key: string) { this.#cripta.config.setKey(key) this.#sameConfig.setKey(key) } parseHeader(header: string | null | undefined = ''): this { if (header) { try { const decoded = this.#cripta.decode(header) if (typeof decoded === 'object') { this.header.replace(decoded as any) this.headerString = header } } catch (e) {} } return this } encodedHeader(): string { this.#newEntropy() return this.headerString = this.#cripta.encode( this.#same ? this.header.all() : this.#randomizeObject(this.header.all()) ) } parseBody(body: string | null | undefined = ''): this { if (body && this.header.has('e')) { try { const decoded = this.#cripta.onceKey(this.header.get('e')).decode(body) this.body = decoded this.bodyString = body } catch (e) {} } return this } encodedBody(): string { return this.bodyString = this.#cripta.onceKey(this.#entropy()).encode(this.body) } setSignature(signature: string): this { this.signature = signature return this } sign(): string { return this.#hash( this.headerString + this.bodyString, this.secret + this.#entropy() ) } signed(): boolean { return this.signature ? this.signature === this.sign() : false } isValid(): boolean { return this.body !== Symbol('UNDEFINED') && this.signed() ? this.validateClaims() : false } isInvalid(): boolean { return !this.isValid() } hasValue(prop: string, value: any = null): boolean { if (this.header.has(prop)) return value === null ? true : this.header.get(prop) === value if (this.body && typeof this.body === 'object' && Object.keys(this.body).length > 0 && prop in this.body) return value === null ? true : this.body[prop] === value return false } validateClaims(): boolean { const now = this.claims.get(Claim.NOW, Math.floor(Date.now() / 1000)) if ( this.isExpired(now) || !this.notBefore(now) || !this.issuedBefore(now) || !this.validate(Claim.AUDIENCE) || !this.validate(Claim.ISSUER) || !this.validate(Claim.ID) || !this.validate(Claim.SUBJECT) || !this.validateCustomClaims() ) { return false } return true } isExpired(date: number | Date | null = null): boolean { const claim = Claim.EXPIRATION_TIME if (!this.header.has(claim)) return false return Timestamp.timestamp(date) - this.#leeway >= this.header.get(claim) } #validAfter(claim: string, date: number | Date | null = null): boolean { if (!this.header.has(claim)) return true return Timestamp.timestamp(date) + this.#leeway >= this.header.get(claim) } notBefore(date: number | Date | null = null): boolean { return this.#validAfter(Claim.NOT_BEFORE, date) } issuedBefore(date: number | Date | null = null): boolean { return this.#validAfter(Claim.ISSUED_AT, date) } validate(claim: string, expected: any = null): boolean { expected = expected || this.claims.get(claim, null) let value = this.getTokenValue(claim) value = Array.isArray(value) ? value : [value] return Array.isArray(expected) ? expected.every(exp => value.includes(exp)) : value.includes(expected) } protected getTokenValue(claim: string): any { if (this.header.has(claim)) return this.header.get(claim) if (this.body && typeof this.body === 'object' && Object.keys(this.body).length > 0 && claim in this.body) return this.body[claim] return null } protected validateCustomClaims(): boolean { const internal = new Set(['e', ...Claim.ALL]) const headers: Record<string, any> = {} for (const [key, value] of this.header) { if (!internal.has(key)) headers[key] = value } const claims: Record<string, any> = {} for (const [key, value] of this.claims) { if (!internal.has(key) && !headers[key]) claims[key] = value } const all = { ...headers, ...claims } for (const claim in all) { if (!this.validate(claim, all[claim])) return false } return true } same(entropy: string | number = '') { this.#same = true this.header.set('e', this.#hash(entropy || this.#cripta.config.key?.toString('hex')).substring(0, 6)) this.#cripta.config.entropy = 0 } unique() { this.#same = false this.#newEntropy() this.#cripta.config.entropy = 4 } leeway(time: number | Date | null = null) { // TODO: add suport to Date type this.#leeway = typeof time === 'number' ? Math.abs(time) : 0 } #entropy(): string { return this.header.get('e') || this.#newEntropy() } #newEntropy(): string { if (this.#same && this.header.has('e')) return this.header.get('e') const entropy = crypto.randomBytes(3).toString('hex') this.header.set('e', entropy) return entropy } #hash(data: any, secret: string | null = null): string { secret && this.#sameConfig.setKey(secret) return this.#cripta.onceConfig(this.#sameConfig).encode(sha256(String(data), 'binary').toString().substring(0, 16)) } getParts(token: string): string[] | false { if (!token) return false const parts = token.split(this.#separator) return parts.length === 3 ? parts : false } toString(): string { return [ this.headerString || this.encodedHeader(), this.bodyString || this.encodedBody(), this.sign(), ].join(this.#separator) } #randomizeObject(obj: Record<string, any>): Record<string, any> { const keys = Object.keys(obj) for (let i = keys.length - 1; i > 0; i--) { const j = (Math.random() * (i + 1)) | 0 ;[keys[i], keys[j]] = [keys[j], keys[i]] } const out: Record<string, any> = {} for (let i = 0; i < keys.length; i++) { const k = keys[i] out[k] = obj[k] } return out } }