fiberx-backend-toolkit 0.0.50 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/constants.d.ts +5 -1
- package/dist/config/constants.js +6 -2
- package/dist/types/util_type.d.ts +23 -0
- package/dist/types/util_type.js +1 -0
- package/dist/utils/encryptor_decryptor_util.js +1 -1
- package/dist/utils/input_validator_util.js +0 -1
- package/dist/utils/totp_service_util.d.ts +21 -0
- package/dist/utils/totp_service_util.js +158 -0
- package/package.json +1 -1
|
@@ -23,4 +23,8 @@ export declare const REQUEST_RATE_LIMITTER_OPTIONS: {
|
|
|
23
23
|
max_requests: number;
|
|
24
24
|
message: string;
|
|
25
25
|
};
|
|
26
|
-
export declare const
|
|
26
|
+
export declare const CHARACTER_CORPUS: string[];
|
|
27
|
+
export declare const ALPHABET_CORPUS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
28
|
+
export declare const DEFAULT_TOTP_STEP = 30;
|
|
29
|
+
export declare const DEFAULT_TOTP_DIGITS = 6;
|
|
30
|
+
export declare const DEFAULT_TOTP_WINDOW = 1;
|
package/dist/config/constants.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ALPHABET_CORPUS = exports.REQUEST_RATE_LIMITTER_OPTIONS = exports.CORS_MAX_AGE_IN_MICRO_SECONDS = exports.CORS_MAX_AGE_IN_SECONDS = exports.CORS_ALLOWED_HEADERS = exports.CORS_ALLOWED_METHODS = exports.DEVICE_ID_HEADERS_NAME = exports.DEVICE_ID_COOKIE_NAME = exports.DEVICE_ID_COOKIE_MAX_AGE = exports.REQUEST_ID_HEADERS_NAME = exports.REQUEST_ID_COOKIE_NAME = exports.REQUEST_ID_COOKIE_MAX_AGE = exports.SEQUELIZE_SEEDER_META_TABLE_NAME = exports.SEQUELIZE_META_TABLE_NAME = exports.SEEDERS_DIR = exports.MIGRATIONS_DIR = exports.MODELS_DIR = exports.SCHEMA_SNAPSHOTS_DIR = exports.SCHEMAS_DIR = exports.ENV_VAR_DIR = exports.LOG_DIR = exports.BASE_DIR = void 0;
|
|
6
|
+
exports.DEFAULT_TOTP_WINDOW = exports.DEFAULT_TOTP_DIGITS = exports.DEFAULT_TOTP_STEP = exports.ALPHABET_CORPUS = exports.CHARACTER_CORPUS = exports.REQUEST_RATE_LIMITTER_OPTIONS = exports.CORS_MAX_AGE_IN_MICRO_SECONDS = exports.CORS_MAX_AGE_IN_SECONDS = exports.CORS_ALLOWED_HEADERS = exports.CORS_ALLOWED_METHODS = exports.DEVICE_ID_HEADERS_NAME = exports.DEVICE_ID_COOKIE_NAME = exports.DEVICE_ID_COOKIE_MAX_AGE = exports.REQUEST_ID_HEADERS_NAME = exports.REQUEST_ID_COOKIE_NAME = exports.REQUEST_ID_COOKIE_MAX_AGE = exports.SEQUELIZE_SEEDER_META_TABLE_NAME = exports.SEQUELIZE_META_TABLE_NAME = exports.SEEDERS_DIR = exports.MIGRATIONS_DIR = exports.MODELS_DIR = exports.SCHEMA_SNAPSHOTS_DIR = exports.SCHEMAS_DIR = exports.ENV_VAR_DIR = exports.LOG_DIR = exports.BASE_DIR = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
// Database constants
|
|
9
9
|
exports.BASE_DIR = process.cwd();
|
|
@@ -33,4 +33,8 @@ exports.REQUEST_RATE_LIMITTER_OPTIONS = {
|
|
|
33
33
|
max_requests: 50,
|
|
34
34
|
message: "⏳ Too many requests from this IP, please try again later"
|
|
35
35
|
};
|
|
36
|
-
exports.
|
|
36
|
+
exports.CHARACTER_CORPUS = ["6", ";", "s", "[", "w", "*", "n", "K", "h", "U", "#", "P", "T", "&", "M", "2", "}", "x", ")", "{", "|", "i", "%", "m", ":", "E", "q", "?", "0", "@", "1", "v", "A", "W", "<", "y", "Y", "4", "5", ".", "e", "G", ",", "D", "7", "I", "j", ">", "g", "t", "k", "c", "V", "9", "J", "L", "u", "H", "b", "Z", "+", '"', "'", "a", "f"];
|
|
37
|
+
exports.ALPHABET_CORPUS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
38
|
+
exports.DEFAULT_TOTP_STEP = 30;
|
|
39
|
+
exports.DEFAULT_TOTP_DIGITS = 6;
|
|
40
|
+
exports.DEFAULT_TOTP_WINDOW = 1;
|
|
@@ -45,3 +45,26 @@ export declare enum ErrorHandlingStrategyEnum {
|
|
|
45
45
|
THROW_ERROR = 2,
|
|
46
46
|
CONTROLLER_ERROR = 3
|
|
47
47
|
}
|
|
48
|
+
export type GenerateSecretOptionsType = {
|
|
49
|
+
name: string;
|
|
50
|
+
issuer: string;
|
|
51
|
+
length?: number;
|
|
52
|
+
};
|
|
53
|
+
export type VerifyResultType = {
|
|
54
|
+
valid: boolean;
|
|
55
|
+
counter?: number;
|
|
56
|
+
};
|
|
57
|
+
export type VerifyOptionsType = {
|
|
58
|
+
secret: string;
|
|
59
|
+
token: string;
|
|
60
|
+
};
|
|
61
|
+
export interface TOTPServiceOptions {
|
|
62
|
+
STEP?: number;
|
|
63
|
+
DIGITS?: number;
|
|
64
|
+
WINDOW?: number;
|
|
65
|
+
ALGORITHM?: "SHA1" | "SHA256" | "SHA512";
|
|
66
|
+
}
|
|
67
|
+
export interface GenerateSecretResult {
|
|
68
|
+
secret_key: string;
|
|
69
|
+
otpauth_url: string;
|
|
70
|
+
}
|
package/dist/types/util_type.js
CHANGED
|
@@ -8,3 +8,4 @@ var ErrorHandlingStrategyEnum;
|
|
|
8
8
|
ErrorHandlingStrategyEnum[ErrorHandlingStrategyEnum["THROW_ERROR"] = 2] = "THROW_ERROR";
|
|
9
9
|
ErrorHandlingStrategyEnum[ErrorHandlingStrategyEnum["CONTROLLER_ERROR"] = 3] = "CONTROLLER_ERROR";
|
|
10
10
|
})(ErrorHandlingStrategyEnum || (exports.ErrorHandlingStrategyEnum = ErrorHandlingStrategyEnum = {}));
|
|
11
|
+
;
|
|
@@ -9,7 +9,6 @@ const dayjs_1 = __importDefault(require("dayjs"));
|
|
|
9
9
|
const isSameOrAfter_1 = __importDefault(require("dayjs/plugin/isSameOrAfter"));
|
|
10
10
|
const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
11
11
|
const axios_1 = __importDefault(require("axios"));
|
|
12
|
-
// import speakeasy from "speakeasy";
|
|
13
12
|
const env_manager_util_1 = __importDefault(require("../utils/env_manager_util"));
|
|
14
13
|
dayjs_1.default.extend(isSameOrAfter_1.default);
|
|
15
14
|
class InputValidatorUtil {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GenerateSecretOptionsType, VerifyResultType, VerifyOptionsType, TOTPServiceOptions, GenerateSecretResult } from "../types/util_type";
|
|
2
|
+
declare class TOTPServiceUtil {
|
|
3
|
+
readonly name = "totp_service_util";
|
|
4
|
+
private static instance;
|
|
5
|
+
private logger;
|
|
6
|
+
private readonly STEP;
|
|
7
|
+
private readonly DIGITS;
|
|
8
|
+
private readonly WINDOW;
|
|
9
|
+
private readonly ALGORITHM;
|
|
10
|
+
private readonly SECRET_KEY_REGEX;
|
|
11
|
+
private constructor();
|
|
12
|
+
static getInstance(options?: TOTPServiceOptions): TOTPServiceUtil;
|
|
13
|
+
private base32Decode;
|
|
14
|
+
private base32Encode;
|
|
15
|
+
private generateForCounter;
|
|
16
|
+
generateSecret(options: GenerateSecretOptionsType): GenerateSecretResult;
|
|
17
|
+
verify(options: VerifyOptionsType & {
|
|
18
|
+
last_used_counter?: number;
|
|
19
|
+
}): VerifyResultType;
|
|
20
|
+
}
|
|
21
|
+
export default TOTPServiceUtil;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const constants_1 = require("../config/constants");
|
|
8
|
+
const logger_util_1 = __importDefault(require("./logger_util"));
|
|
9
|
+
const input_validator_util_1 = __importDefault(require("./input_validator_util"));
|
|
10
|
+
class TOTPServiceUtil {
|
|
11
|
+
name = "totp_service_util";
|
|
12
|
+
static instance;
|
|
13
|
+
logger = new logger_util_1.default(this.name);
|
|
14
|
+
// ===============================
|
|
15
|
+
// CONFIG
|
|
16
|
+
// ===============================
|
|
17
|
+
STEP;
|
|
18
|
+
DIGITS;
|
|
19
|
+
WINDOW;
|
|
20
|
+
ALGORITHM;
|
|
21
|
+
SECRET_KEY_REGEX = (/^[A-Z2-7]+$/i);
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.STEP = options.STEP ?? constants_1.DEFAULT_TOTP_STEP;
|
|
24
|
+
this.DIGITS = options.DIGITS ?? constants_1.DEFAULT_TOTP_DIGITS;
|
|
25
|
+
this.WINDOW = options.WINDOW ?? constants_1.DEFAULT_TOTP_WINDOW;
|
|
26
|
+
this.ALGORITHM = options.ALGORITHM?.toLowerCase() ?? "sha1";
|
|
27
|
+
}
|
|
28
|
+
// ✅ Global access point
|
|
29
|
+
static getInstance(options = {}) {
|
|
30
|
+
if (!TOTPServiceUtil.instance) {
|
|
31
|
+
TOTPServiceUtil.instance = new TOTPServiceUtil(options);
|
|
32
|
+
}
|
|
33
|
+
return TOTPServiceUtil.instance;
|
|
34
|
+
}
|
|
35
|
+
// ===============================
|
|
36
|
+
// BASE32 DECODE
|
|
37
|
+
// ===============================
|
|
38
|
+
base32Decode(input) {
|
|
39
|
+
if (!/^[A-Z2-7]+=*$/i.test(input)) {
|
|
40
|
+
throw new Error("Invalid base32 secret");
|
|
41
|
+
}
|
|
42
|
+
const alphabet = constants_1.ALPHABET_CORPUS;
|
|
43
|
+
const cleaned = input.replace(/=+$/, "").toUpperCase();
|
|
44
|
+
let bits = 0;
|
|
45
|
+
let value = 0;
|
|
46
|
+
const output = [];
|
|
47
|
+
for (const char of cleaned) {
|
|
48
|
+
const idx = alphabet.indexOf(char);
|
|
49
|
+
if (idx === -1) {
|
|
50
|
+
throw new Error("Invalid base32 character while decoding base 32 for totp service util");
|
|
51
|
+
}
|
|
52
|
+
value = (value << 5) | idx;
|
|
53
|
+
bits += 5;
|
|
54
|
+
if (bits >= 8) {
|
|
55
|
+
output.push((value >>> (bits - 8)) & 255);
|
|
56
|
+
bits -= 8;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Buffer.from(output);
|
|
60
|
+
}
|
|
61
|
+
// ===============================
|
|
62
|
+
// BASE32 ENCODE
|
|
63
|
+
// ===============================
|
|
64
|
+
base32Encode(buffer) {
|
|
65
|
+
const alphabet = constants_1.ALPHABET_CORPUS;
|
|
66
|
+
let bits = 0;
|
|
67
|
+
let value = 0;
|
|
68
|
+
let output = "";
|
|
69
|
+
for (const byte of buffer) {
|
|
70
|
+
value = (value << 8) | byte;
|
|
71
|
+
bits += 8;
|
|
72
|
+
while (bits >= 5) {
|
|
73
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
74
|
+
bits -= 5;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (bits > 0) {
|
|
78
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
// ===============================
|
|
83
|
+
// INTERNAL: GENERATE OTP
|
|
84
|
+
// ===============================
|
|
85
|
+
generateForCounter(secret, counter) {
|
|
86
|
+
const buffer = Buffer.alloc(8);
|
|
87
|
+
buffer.writeBigUInt64BE(BigInt(counter));
|
|
88
|
+
const hmac = crypto_1.default
|
|
89
|
+
.createHmac(this.ALGORITHM, secret)
|
|
90
|
+
.update(buffer)
|
|
91
|
+
.digest();
|
|
92
|
+
const offset = hmac[hmac.length - 1] & 0xf;
|
|
93
|
+
const code = ((hmac[offset] & 0x7f) << 24) |
|
|
94
|
+
((hmac[offset + 1] & 0xff) << 16) |
|
|
95
|
+
((hmac[offset + 2] & 0xff) << 8) |
|
|
96
|
+
(hmac[offset + 3] & 0xff);
|
|
97
|
+
const otp = code % 10 ** this.DIGITS;
|
|
98
|
+
return otp.toString().padStart(this.DIGITS, "0");
|
|
99
|
+
}
|
|
100
|
+
// ===============================
|
|
101
|
+
// PUBLIC: GENERATE SECRET
|
|
102
|
+
// ===============================
|
|
103
|
+
generateSecret(options) {
|
|
104
|
+
const { name, issuer, length = 20 } = options;
|
|
105
|
+
const buffer = crypto_1.default.randomBytes(length);
|
|
106
|
+
const base32 = this.base32Encode(buffer);
|
|
107
|
+
const encoder_issuer = encodeURIComponent(issuer);
|
|
108
|
+
const encoded_name = encodeURIComponent(name);
|
|
109
|
+
const otpauth_url = `otpauth://totp/${encoder_issuer}:${encoded_name}` +
|
|
110
|
+
`?secret=${base32}` +
|
|
111
|
+
`&issuer=${encoder_issuer}` +
|
|
112
|
+
`&algorithm=${this.ALGORITHM.toUpperCase()}` +
|
|
113
|
+
`&digits=${this.DIGITS}` +
|
|
114
|
+
`&period=${this.STEP}`;
|
|
115
|
+
return {
|
|
116
|
+
secret_key: base32,
|
|
117
|
+
otpauth_url,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ===============================
|
|
121
|
+
// PUBLIC: VERIFY TOKEN
|
|
122
|
+
// ===============================
|
|
123
|
+
verify(options) {
|
|
124
|
+
const { secret, token, last_used_counter } = options;
|
|
125
|
+
if (!/^\d+$/.test(token)) {
|
|
126
|
+
return { valid: false };
|
|
127
|
+
}
|
|
128
|
+
if (token.length !== this.DIGITS) {
|
|
129
|
+
return { valid: false };
|
|
130
|
+
}
|
|
131
|
+
if (!this.SECRET_KEY_REGEX.test(secret)) {
|
|
132
|
+
throw new Error("Invalid TOTP secret format");
|
|
133
|
+
}
|
|
134
|
+
let key;
|
|
135
|
+
try {
|
|
136
|
+
key = this.base32Decode(secret);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.logger.error("Failed to decode TOTP secret");
|
|
140
|
+
return { valid: false };
|
|
141
|
+
}
|
|
142
|
+
const time = Math.floor(Date.now() / 1000 / this.STEP);
|
|
143
|
+
for (let error_window = -this.WINDOW; error_window <= this.WINDOW; error_window++) {
|
|
144
|
+
const counter = time + error_window;
|
|
145
|
+
const generated = this.generateForCounter(key, counter);
|
|
146
|
+
if (input_validator_util_1.default.isSafeHashCompare(generated, token)) {
|
|
147
|
+
// 🔒 Replay protection
|
|
148
|
+
if (typeof last_used_counter === "number" &&
|
|
149
|
+
counter <= last_used_counter) {
|
|
150
|
+
return { valid: false };
|
|
151
|
+
}
|
|
152
|
+
return { valid: true, counter };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { valid: false };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.default = TOTPServiceUtil;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fiberx-backend-toolkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"description": "A TypeScript backend toolkit providing shared domain logic, infrastructure helpers, and utilities for FiberX server-side applications and services.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./dist/index.js",
|