@archlast/shared 0.0.1
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/README.md +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/token-obfuscation.d.ts +35 -0
- package/dist/src/token-obfuscation.js +88 -0
- package/package.json +27 -0
- package/src/token-obfuscation.ts +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @archlast/shared
|
|
2
|
+
|
|
3
|
+
Shared utilities used by Archlast packages.
|
|
4
|
+
|
|
5
|
+
This package is primarily an internal dependency. Most users should install
|
|
6
|
+
`@archlast/client`, `@archlast/server`, or `@archlast/cli` instead.
|
|
7
|
+
|
|
8
|
+
## Publishing (maintainers)
|
|
9
|
+
|
|
10
|
+
See `docs/npm-publishing.md` for release and publish steps.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { obfuscateToken, deobfuscateToken, isObfuscatedToken, extractRawToken, } from "./src/token-obfuscation.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { obfuscateToken, deobfuscateToken, isObfuscatedToken, extractRawToken, } from "./src/token-obfuscation.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Token Obfuscation
|
|
3
|
+
*
|
|
4
|
+
* Adds dynamic obfuscation to admin tokens to prevent replay attacks
|
|
5
|
+
* and protect the raw token from being intercepted.
|
|
6
|
+
*
|
|
7
|
+
* Format: v1:<timestamp>:<nonce>:<signature>
|
|
8
|
+
* - v1: Version identifier
|
|
9
|
+
* - timestamp: Unix milliseconds (must be within 5 minutes)
|
|
10
|
+
* - nonce: Random 16-char string for uniqueness
|
|
11
|
+
* - signature: HMAC-SHA256 of "token:timestamp:nonce:path"
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Obfuscate an admin token for a specific request
|
|
15
|
+
* @param token - The raw admin token (sat_...)
|
|
16
|
+
* @param path - The request path (for binding)
|
|
17
|
+
* @returns Obfuscated token string
|
|
18
|
+
*/
|
|
19
|
+
export declare function obfuscateToken(token: string, path: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Parse and validate an obfuscated token
|
|
22
|
+
* @param obfuscated - The obfuscated token string
|
|
23
|
+
* @param path - The request path (must match what was used for obfuscation)
|
|
24
|
+
* @returns The raw token if valid, null if invalid
|
|
25
|
+
*/
|
|
26
|
+
export declare function deobfuscateToken(obfuscated: string, path: string): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Check if a token is obfuscated (starts with v1:)
|
|
29
|
+
*/
|
|
30
|
+
export declare function isObfuscatedToken(token: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Extract raw token from obfuscated or raw token
|
|
33
|
+
* Handles both obfuscated (v1:...) and raw (sat_...) tokens
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractRawToken(token: string, path: string): string | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Token Obfuscation
|
|
3
|
+
*
|
|
4
|
+
* Adds dynamic obfuscation to admin tokens to prevent replay attacks
|
|
5
|
+
* and protect the raw token from being intercepted.
|
|
6
|
+
*
|
|
7
|
+
* Format: v1:<timestamp>:<nonce>:<signature>
|
|
8
|
+
* - v1: Version identifier
|
|
9
|
+
* - timestamp: Unix milliseconds (must be within 5 minutes)
|
|
10
|
+
* - nonce: Random 16-char string for uniqueness
|
|
11
|
+
* - signature: HMAC-SHA256 of "token:timestamp:nonce:path"
|
|
12
|
+
*/
|
|
13
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
14
|
+
const VERSION = "v1";
|
|
15
|
+
const TIMESTAMP_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
const NONCE_LENGTH = 16;
|
|
17
|
+
const SECRET_KEY = process.env.ARCHLAST_TOKEN_OBFUSCATION_SECRET || "archlast-default-secret-change-in-production";
|
|
18
|
+
/**
|
|
19
|
+
* Obfuscate an admin token for a specific request
|
|
20
|
+
* @param token - The raw admin token (sat_...)
|
|
21
|
+
* @param path - The request path (for binding)
|
|
22
|
+
* @returns Obfuscated token string
|
|
23
|
+
*/
|
|
24
|
+
export function obfuscateToken(token, path) {
|
|
25
|
+
const timestamp = Date.now();
|
|
26
|
+
const nonce = randomBytes(NONCE_LENGTH).toString("base64url").slice(0, NONCE_LENGTH);
|
|
27
|
+
// Create signature: HMAC-SHA256(token:timestamp:nonce:path)
|
|
28
|
+
const message = `${token}:${timestamp}:${nonce}:${path}`;
|
|
29
|
+
const signature = createHmac("sha256", SECRET_KEY)
|
|
30
|
+
.update(message)
|
|
31
|
+
.digest("base64url");
|
|
32
|
+
return `${VERSION}:${timestamp}:${nonce}:${signature}:${token}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse and validate an obfuscated token
|
|
36
|
+
* @param obfuscated - The obfuscated token string
|
|
37
|
+
* @param path - The request path (must match what was used for obfuscation)
|
|
38
|
+
* @returns The raw token if valid, null if invalid
|
|
39
|
+
*/
|
|
40
|
+
export function deobfuscateToken(obfuscated, path) {
|
|
41
|
+
try {
|
|
42
|
+
// Parse format: v1:timestamp:nonce:signature:token
|
|
43
|
+
const parts = obfuscated.split(":");
|
|
44
|
+
if (parts.length !== 5 || parts[0] !== VERSION) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const [, timestampStr, nonce, signature, token] = parts;
|
|
48
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
49
|
+
// Check timestamp is within window
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
if (Math.abs(now - timestamp) > TIMESTAMP_WINDOW_MS) {
|
|
52
|
+
console.warn(`[Token Obfuscation] Timestamp outside window: ${Math.abs(now - timestamp)}ms ago`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
// Verify signature
|
|
56
|
+
const message = `${token}:${timestamp}:${nonce}:${path}`;
|
|
57
|
+
const expectedSignature = createHmac("sha256", SECRET_KEY)
|
|
58
|
+
.update(message)
|
|
59
|
+
.digest("base64url");
|
|
60
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
61
|
+
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
|
|
62
|
+
console.warn(`[Token Obfuscation] Signature verification failed`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`[Token Obfuscation] Deobfuscation error:`, error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if a token is obfuscated (starts with v1:)
|
|
74
|
+
*/
|
|
75
|
+
export function isObfuscatedToken(token) {
|
|
76
|
+
return token.startsWith(`${VERSION}:`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract raw token from obfuscated or raw token
|
|
80
|
+
* Handles both obfuscated (v1:...) and raw (sat_...) tokens
|
|
81
|
+
*/
|
|
82
|
+
export function extractRawToken(token, path) {
|
|
83
|
+
if (isObfuscatedToken(token)) {
|
|
84
|
+
return deobfuscateToken(token, path);
|
|
85
|
+
}
|
|
86
|
+
// Raw token - return as-is (for backwards compatibility during transition)
|
|
87
|
+
return token;
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@archlast/shared",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Shared utilities for Archlast packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.7.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Token Obfuscation
|
|
3
|
+
*
|
|
4
|
+
* Adds dynamic obfuscation to admin tokens to prevent replay attacks
|
|
5
|
+
* and protect the raw token from being intercepted.
|
|
6
|
+
*
|
|
7
|
+
* Format: v1:<timestamp>:<nonce>:<signature>
|
|
8
|
+
* - v1: Version identifier
|
|
9
|
+
* - timestamp: Unix milliseconds (must be within 5 minutes)
|
|
10
|
+
* - nonce: Random 16-char string for uniqueness
|
|
11
|
+
* - signature: HMAC-SHA256 of "token:timestamp:nonce:path"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
15
|
+
|
|
16
|
+
const VERSION = "v1";
|
|
17
|
+
const TIMESTAMP_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
18
|
+
const NONCE_LENGTH = 16;
|
|
19
|
+
const SECRET_KEY = process.env.ARCHLAST_TOKEN_OBFUSCATION_SECRET || "archlast-default-secret-change-in-production";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Obfuscate an admin token for a specific request
|
|
23
|
+
* @param token - The raw admin token (sat_...)
|
|
24
|
+
* @param path - The request path (for binding)
|
|
25
|
+
* @returns Obfuscated token string
|
|
26
|
+
*/
|
|
27
|
+
export function obfuscateToken(token: string, path: string): string {
|
|
28
|
+
const timestamp = Date.now();
|
|
29
|
+
const nonce = randomBytes(NONCE_LENGTH).toString("base64url").slice(0, NONCE_LENGTH);
|
|
30
|
+
|
|
31
|
+
// Create signature: HMAC-SHA256(token:timestamp:nonce:path)
|
|
32
|
+
const message = `${token}:${timestamp}:${nonce}:${path}`;
|
|
33
|
+
const signature = createHmac("sha256", SECRET_KEY)
|
|
34
|
+
.update(message)
|
|
35
|
+
.digest("base64url");
|
|
36
|
+
|
|
37
|
+
return `${VERSION}:${timestamp}:${nonce}:${signature}:${token}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse and validate an obfuscated token
|
|
42
|
+
* @param obfuscated - The obfuscated token string
|
|
43
|
+
* @param path - The request path (must match what was used for obfuscation)
|
|
44
|
+
* @returns The raw token if valid, null if invalid
|
|
45
|
+
*/
|
|
46
|
+
export function deobfuscateToken(obfuscated: string, path: string): string | null {
|
|
47
|
+
try {
|
|
48
|
+
// Parse format: v1:timestamp:nonce:signature:token
|
|
49
|
+
const parts = obfuscated.split(":");
|
|
50
|
+
if (parts.length !== 5 || parts[0] !== VERSION) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [, timestampStr, nonce, signature, token] = parts;
|
|
55
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
56
|
+
|
|
57
|
+
// Check timestamp is within window
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (Math.abs(now - timestamp) > TIMESTAMP_WINDOW_MS) {
|
|
60
|
+
console.warn(`[Token Obfuscation] Timestamp outside window: ${Math.abs(now - timestamp)}ms ago`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify signature
|
|
65
|
+
const message = `${token}:${timestamp}:${nonce}:${path}`;
|
|
66
|
+
const expectedSignature = createHmac("sha256", SECRET_KEY)
|
|
67
|
+
.update(message)
|
|
68
|
+
.digest("base64url");
|
|
69
|
+
|
|
70
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
71
|
+
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
|
|
72
|
+
console.warn(`[Token Obfuscation] Signature verification failed`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return token;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`[Token Obfuscation] Deobfuscation error:`, error);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a token is obfuscated (starts with v1:)
|
|
85
|
+
*/
|
|
86
|
+
export function isObfuscatedToken(token: string): boolean {
|
|
87
|
+
return token.startsWith(`${VERSION}:`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract raw token from obfuscated or raw token
|
|
92
|
+
* Handles both obfuscated (v1:...) and raw (sat_...) tokens
|
|
93
|
+
*/
|
|
94
|
+
export function extractRawToken(token: string, path: string): string | null {
|
|
95
|
+
if (isObfuscatedToken(token)) {
|
|
96
|
+
return deobfuscateToken(token, path);
|
|
97
|
+
}
|
|
98
|
+
// Raw token - return as-is (for backwards compatibility during transition)
|
|
99
|
+
return token;
|
|
100
|
+
}
|