Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(crypto): self validate hash and user factory #1096

Merged
merged 24 commits into from
Apr 30, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/math/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"include": ["src/**/*.ts"],
"exclude": [],
"references": [
{"path": "../logger"}
{"path": "../logger"},
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
]
}
5 changes: 3 additions & 2 deletions core/token/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"esm",
"alwatr"
],
"main": "token.js",
"main": "index.js",
"type": "module",
"types": "token.d.ts",
"types": "index.d.ts",
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com>",
"license": "MIT",
"files": [
Expand All @@ -41,6 +41,7 @@
"dependencies": {
"@alwatr/logger": "^0.30.0",
"@alwatr/math": "^0.30.0",
"@alwatr/type": "^0.30.0",
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
"tslib": "^2.5.0"
}
}
108 changes: 108 additions & 0 deletions core/token/src/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {createHash, randomBytes, type BinaryLike} from 'node:crypto';

import type {HashGeneratorConfig} from './type.js';

/**
* Secure **self-validate** hash generator.
*/
export class AlwatrHashGenerator {
constructor(public config: HashGeneratorConfig = {algorithm: 'sha1', encoding: 'base64url', crcLength: 8}) {}

/**
* Generate simple hash from random data.
*
* Example:
*
* ```js
* const clientId = hashGenerator.random();
* ```
*/
random(): string {
return this.generate(randomBytes(16));
}

/**
* Generate **self-validate** hash from random data.
*
* Example:
*
* ```ts
* const userId = hashGenerator.randomSelfValidate();
* ```
*/
randomSelfValidate(): string {
return this.generateSelfValidate(randomBytes(16));
}

/**
* Generate simple hash from data.
*
* Example:
*
* ```ts
* const crcHash = hashGenerator.generate(downloadedData);
* ```
*/
generate(data: BinaryLike): string {
return createHash(this.config.algorithm).update(data).digest(this.config.encoding);
}

/**
* Generate crc hash.
*/
_generateCrc(data: BinaryLike): string {
const crc = this.generate(data);
return this.config.crcLength == null || this.config.crcLength < 1 ? crc : crc.substring(0, this.config.crcLength);
}

/**
* Generate **self-validate** hash from data.
*
* Example:
*
* ```js
* const userId = hashGenerator.generateSelfValidate(randomData);
* ```
*/
generateSelfValidate(data: BinaryLike): string {
const mainHash = this.generate(data);
const crcHash = this._generateCrc(mainHash);
return mainHash + crcHash;
}

/**
* Verify `generate(data)` equals to `hash`.
*
* Example:
*
* ```ts
* if (!hashGenerator.verify(downloadedData, crcHash)) {
* new Error('data_corrupted');
* }
* ```
*/
verify(data: BinaryLike, hash: string): boolean {
return hash === this.generate(data);
}

/**
* Verify a **self-validate** hash to check its generated by this class (same options).
*
* Example:
*
* ```ts
* if (!hashGenerator.verifySelfValidate(user.id)) {
* new Error('invalid_user');
* }
* ```
*/
verifySelfValidate(hash: string): boolean {
const gapPos =
this.config.crcLength == null || this.config.crcLength < 1
? hash.length / 2
: hash.length - this.config.crcLength;
const mainHash = hash.substring(0, gapPos);
const crcHash = hash.substring(gapPos);
return crcHash === this._generateCrc(mainHash);
}
}
11 changes: 11 additions & 0 deletions core/token/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {globalAlwatr} from '@alwatr/logger';

export * from './hash.js';
export * from './token.js';
export * from './user.js';
export * from './type.js';

globalAlwatr.registeredList.push({
name: '@alwatr/token',
version: _ALWATR_VERSION_,
});
35 changes: 20 additions & 15 deletions core/token/src/token.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import {createHmac} from 'node:crypto';

import {createLogger, globalAlwatr} from '@alwatr/logger';
import {parseDuration} from '@alwatr/math';

import type {TokenGeneratorConfig, TokenStatus, DigestAlgorithm} from './type.js';

export type {TokenGeneratorConfig, TokenStatus, DigestAlgorithm};

globalAlwatr.registeredList.push({
name: '@alwatr/token',
version: _ALWATR_VERSION_,
});
import type {TokenGeneratorConfig, TokenStatus} from './type.js';

/**
* Secure authentication HOTP token generator (HMAC-based One-Time Password algorithm).
*/
export class AlwatrTokenGenerator {
protected _logger = createLogger('alwatr-token-generator');
private _duration: number | null;
protected _duration: number | null;

get epoch(): number {
return this._duration == null
? 0
: Math.floor(Date.now() / this._duration);
return this._duration == null ? 0 : Math.floor(Date.now() / this._duration);
}

constructor(public config: TokenGeneratorConfig) {
this._logger.logMethodArgs?.('constructor', config);
this._duration = config.duration == null ? null : parseDuration(config.duration);
}

Expand All @@ -33,10 +24,24 @@ export class AlwatrTokenGenerator {
.digest(this.config.encoding);
}

/**
* Generate HOTP token from data base on special duration.
*
* ```ts
* user.auth = tokenGenerator.generate(`${user.id}-${user.role}`);
* ```
*/
generate(data: string): string {
return this._generate(data, this.epoch);
}

/**
* Token validation.
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
*
* ```ts
* const validateStatus = tokenGenerator.verify(`${user.id}-${user.role}`, user.auth);
* ```
*/
verify(data: string, token: string): TokenStatus {
const epoch = this.epoch;
if (token === this._generate(data, epoch)) {
Expand Down
35 changes: 30 additions & 5 deletions core/token/src/type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type {DurationString} from '@alwatr/math';

export type DigestAlgorithm = 'md5' | 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512';
export type CryptoAlgorithm = 'md5' | 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512';
export type CryptoEncoding = 'base64' | 'base64url' | 'hex' | 'binary';

export type TokenStatus = 'valid' | 'invalid' | 'expired';
export type HashStatus = 'valid' | 'invalid';

export type TokenGeneratorConfig = {

export interface TokenGeneratorConfig {
/**
* Secret string data to generate token.
*/
Expand All @@ -20,10 +23,32 @@ export type TokenGeneratorConfig = {
/**
* OpenSSl digest algorithm.
*/
algorithm: DigestAlgorithm;
algorithm: CryptoAlgorithm;

/**
* Encoding of token.
*/
encoding: 'base64' | 'base64url' | 'hex' | 'binary';
};
encoding: CryptoEncoding;
}

export interface HashGeneratorConfig {
/**
* OpenSSl digest algorithm.
*/
algorithm: CryptoAlgorithm;

/**
* Encoding of hash.
*/
encoding: CryptoEncoding;

/**
* CRC hash max length.
*/
crcLength?: number;
}

export interface UserFactoryConfig {
tokenConfig: TokenGeneratorConfig;
hashConfig: HashGeneratorConfig;
}
76 changes: 76 additions & 0 deletions core/token/src/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {AlwatrHashGenerator} from './hash.js';
import {AlwatrTokenGenerator} from './token.js';

import type {HashGeneratorConfig, TokenGeneratorConfig, TokenStatus} from './type.js';

/**
* Secure User ID/Token Factory (generate and validate authentication process).
*/
export class AlwatrUserFactory {
protected _tokenGenerator;
protected _hashGenerator;

constructor(
tokenConfig: TokenGeneratorConfig,
hashConfig?: HashGeneratorConfig,
) {
this._tokenGenerator = new AlwatrTokenGenerator(tokenConfig);
this._hashGenerator = new AlwatrHashGenerator(hashConfig);
}

/**
* Generate new self-verifiable user-id.
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
*
* Example:
*
* ```ts
* const newUser = {
* id: userFactory.generateId(),
* ...
* }
* ```
*/
generateId(): string {
return this._hashGenerator.randomSelfValidate();
}

/**
* Validate user-id without token.
*
* Example:
*
* ```ts
* const newUser = {
* id: userFactory.generateId(),
* ...
* }
* ```
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
*/
verifyId(id: string): boolean {
return this._hashGenerator.verifySelfValidate(id);
}

/**
* Generate user auth token.
*
* Example:
*
* ```ts
* ```
*/
generateToken(uniquelyList: Array<string | number | boolean>): string {
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
return this._tokenGenerator.generate(uniquelyList.join());
}

/**
* Verify user auth token.
*
* Example:
*
* ```ts
* ```
*/
verifyToken(uniquelyList: Array<string | number | boolean>, token: string): TokenStatus {
return this._tokenGenerator.verify(uniquelyList.join(), token);
}
}
1 change: 1 addition & 0 deletions core/token/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"exclude": [],
"references": [
{"path": "../logger"},
{"path": "../type"},
njfamirm marked this conversation as resolved.
Show resolved Hide resolved
{"path": "../math"},
]
}
6 changes: 4 additions & 2 deletions demo/token/benchmark.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {type DigestAlgorithm, AlwatrTokenGenerator} from '@alwatr/token';
import {AlwatrTokenGenerator} from '@alwatr/token';
import {delay} from '@alwatr/util';

import type {CryptoAlgorithm} from '@alwatr/token/type.js';

if (process.env.NODE_ENV !== 'production') {
console.log('Please run node in production for benchmark. NODE_ENV=production node demo/token/benchmark.js');
process.exit();
Expand All @@ -15,7 +17,7 @@ const tokenGenerator = new AlwatrTokenGenerator({

const sampleData = 'Lorem ipsum dolor sit amet consectetur adipisicing elit.';

function benchmark(algorithm: DigestAlgorithm): void {
function benchmark(algorithm: CryptoAlgorithm): void {
tokenGenerator.config.algorithm = algorithm;
const now = Date.now();
const testRun = 1_000_000;
Expand Down
16 changes: 16 additions & 0 deletions demo/token/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {AlwatrHashGenerator} from '@alwatr/token';

const hashGenerator = new AlwatrHashGenerator({
algorithm: 'sha1',
encoding: 'base64url',
crcLength: 8,
});

const test = (): void => {
const hash = hashGenerator.randomSelfValidate();
console.log('hash: %s validation: %s', hash, hashGenerator.verifySelfValidate(hash));
};

for (let index = 0; index < 10; index++) {
test();
}
2 changes: 1 addition & 1 deletion demo/token/index.ts → demo/token/token.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createLogger} from '@alwatr/logger';
import {type TokenStatus, AlwatrTokenGenerator} from '@alwatr/token';

const logger = createLogger('token/demo');
const logger = createLogger('token/demo', true);

const tokenGenerator = new AlwatrTokenGenerator({
secret: 'my-very-secret-key',
Expand Down