Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,5 @@ npm_package(
":package.json",
],
package = "@tummycrypt/tinyland-auth",
version = "0.3.0",
version = "0.3.1",
)
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @tummycrypt/tinyland-auth

## 0.3.1

### Patch Changes

- Fix Node ESM consumption of the TOTP and invitation exports by importing the
CommonJS `otplib` package through its default namespace.

## 0.2.2

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module(
name = "tummycrypt_tinyland_auth",
version = "0.3.0",
version = "0.3.1",
compatibility_level = 1,
)

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tummycrypt/tinyland-auth",
"version": "0.3.0",
"version": "0.3.1",
"packageManager": "pnpm@10.13.1",
"description": "Production-grade authentication system with TOTP, RBAC, and pluggable storage",
"type": "module",
Expand Down Expand Up @@ -113,4 +113,4 @@
"access": "public",
"provenance": true
}
}
}
101 changes: 38 additions & 63 deletions src/core/totp/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@








import { authenticator } from 'otplib';
import * as qrcode from 'qrcode';
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
import type { TOTPSecret, EncryptedData, TOTPConfig } from '../../types/index.js';
import { timingSafeVerify } from '../security/index.js';

import otplib from "otplib";
import * as qrcode from "qrcode";
import {
createCipheriv,
createDecipheriv,
randomBytes,
scryptSync,
} from "crypto";
import type {
TOTPSecret,
EncryptedData,
TOTPConfig,
} from "../../types/index.js";
import { timingSafeVerify } from "../security/index.js";

const { authenticator } = otplib;

authenticator.options = { window: 1 };


const ALGORITHM = 'aes-256-gcm';
const ALGORITHM = "aes-256-gcm";
const SALT_LENGTH = 32;
const IV_LENGTH = 16;
const KEY_LENGTH = 32;

export interface TOTPServiceConfig {

encryptionKey: string;

issuer: string;

devMode?: boolean;

testCode?: string;
}

Expand All @@ -45,9 +45,6 @@ export class TOTPService {
this.testCode = config.testCode;
}




async generateSecret(handle: string, email: string): Promise<TOTPSecret> {
const secret = authenticator.generateSecret();
const otpauth = authenticator.keyuri(email, this.issuer, secret);
Expand All @@ -62,39 +59,33 @@ export class TOTPService {
};
}




encrypt(text: string): EncryptedData {
const salt = randomBytes(SALT_LENGTH);
const key = scryptSync(this.encryptionKey, salt, KEY_LENGTH);
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);

const encrypted = Buffer.concat([
cipher.update(text, 'utf8'),
cipher.update(text, "utf8"),
cipher.final(),
]);

const tag = cipher.getAuthTag();

return {
encrypted: encrypted.toString('base64'),
salt: salt.toString('base64'),
iv: iv.toString('base64'),
tag: tag.toString('base64'),
encrypted: encrypted.toString("base64"),
salt: salt.toString("base64"),
iv: iv.toString("base64"),
tag: tag.toString("base64"),
};
}




decrypt(encryptedData: EncryptedData): string {
const salt = Buffer.from(encryptedData.salt, 'base64');
const salt = Buffer.from(encryptedData.salt, "base64");
const key = scryptSync(this.encryptionKey, salt, KEY_LENGTH);
const iv = Buffer.from(encryptedData.iv, 'base64');
const tag = Buffer.from(encryptedData.tag, 'base64');
const encrypted = Buffer.from(encryptedData.encrypted, 'base64');
const iv = Buffer.from(encryptedData.iv, "base64");
const tag = Buffer.from(encryptedData.tag, "base64");
const encrypted = Buffer.from(encryptedData.encrypted, "base64");

const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
Expand All @@ -104,27 +95,22 @@ export class TOTPService {
decipher.final(),
]);

return decrypted.toString('utf8');
return decrypted.toString("utf8");
}




async verifyToken(
secretOrNull: TOTPSecret | null,
token: string
token: string,
): Promise<boolean> {
const cleanToken = token.replace(/\s/g, '');
const cleanToken = token.replace(/\s/g, "");


if (this.devMode && this.testCode && cleanToken === this.testCode) {
return true;
}

return await timingSafeVerify(async () => {
if (!secretOrNull) {

const dummySecret = 'JBSWY3DPEHPK3PXP';
const dummySecret = "JBSWY3DPEHPK3PXP";
authenticator.verify({ token: cleanToken, secret: dummySecret });
return false;
}
Expand All @@ -136,40 +122,29 @@ export class TOTPService {
}, 150);
}




generateToken(secret: TOTPSecret): string {
return authenticator.generate(secret.secret);
}




async generateQRCode(secret: TOTPSecret): Promise<string> {
const otpauth = authenticator.keyuri(secret.email, this.issuer, secret.secret);
const otpauth = authenticator.keyuri(
secret.email,
this.issuer,
secret.secret,
);
return await qrcode.toDataURL(otpauth);
}




encryptBackupCodes(codes: string[]): EncryptedData {
return this.encrypt(JSON.stringify(codes));
}




decryptBackupCodes(encryptedData: EncryptedData): string[] {
const json = this.decrypt(encryptedData);
return JSON.parse(json);
}
}




export function createTOTPService(config: TOTPConfig): TOTPService {
return new TOTPService({
encryptionKey: config.encryptionKey,
Expand Down
Loading
Loading