altcha-lib 1.4.0 → 2.0.0-beta.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/LICENSE.txt +1 -1
- package/README.md +44 -149
- package/bin/cli.mjs +35 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/{cjs/dist → dist/cjs/v1}/index.js +3 -3
- package/dist/cjs/v2/algorithms/argon2id.d.ts +2 -0
- package/dist/cjs/v2/algorithms/argon2id.js +26 -0
- package/dist/cjs/v2/algorithms/pbkdf2.d.ts +2 -0
- package/dist/cjs/v2/algorithms/pbkdf2.js +23 -0
- package/dist/cjs/v2/algorithms/scrypt.d.ts +2 -0
- package/dist/cjs/v2/algorithms/scrypt.js +17 -0
- package/dist/cjs/v2/algorithms/sha.d.ts +2 -0
- package/dist/cjs/v2/algorithms/sha.js +35 -0
- package/dist/cjs/v2/algorithms/web/pbkdf2.d.ts +2 -0
- package/dist/cjs/v2/algorithms/web/pbkdf2.js +19 -0
- package/dist/cjs/v2/algorithms/web/sha.d.ts +2 -0
- package/dist/cjs/v2/algorithms/web/sha.js +23 -0
- package/dist/cjs/v2/capped-map.d.ts +7 -0
- package/dist/cjs/v2/capped-map.js +18 -0
- package/dist/cjs/v2/frameworks/express.d.ts +21 -0
- package/dist/cjs/v2/frameworks/express.js +77 -0
- package/dist/cjs/v2/frameworks/fastify.d.ts +26 -0
- package/dist/cjs/v2/frameworks/fastify.js +88 -0
- package/dist/cjs/v2/frameworks/h3.d.ts +37 -0
- package/dist/cjs/v2/frameworks/h3.js +89 -0
- package/dist/cjs/v2/frameworks/hono.d.ts +99 -0
- package/dist/cjs/v2/frameworks/hono.js +86 -0
- package/dist/cjs/v2/frameworks/nestjs.d.ts +79 -0
- package/dist/cjs/v2/frameworks/nestjs.js +198 -0
- package/dist/cjs/v2/frameworks/nextjs.d.ts +21 -0
- package/dist/cjs/v2/frameworks/nextjs.js +112 -0
- package/dist/cjs/v2/frameworks/shared.d.ts +8 -0
- package/dist/cjs/v2/frameworks/shared.js +121 -0
- package/dist/cjs/v2/frameworks/sveltekit.d.ts +29 -0
- package/dist/cjs/v2/frameworks/sveltekit.js +101 -0
- package/dist/cjs/v2/frameworks/types.d.ts +47 -0
- package/dist/cjs/v2/frameworks/types.js +2 -0
- package/dist/cjs/v2/helpers.d.ts +27 -0
- package/dist/cjs/v2/helpers.js +127 -0
- package/dist/cjs/v2/index.d.ts +19 -0
- package/dist/cjs/v2/index.js +28 -0
- package/dist/cjs/v2/obfuscation.d.ts +11 -0
- package/dist/cjs/v2/obfuscation.js +74 -0
- package/dist/cjs/v2/pow.d.ts +60 -0
- package/dist/cjs/v2/pow.js +286 -0
- package/dist/cjs/v2/server-signature.d.ts +12 -0
- package/dist/cjs/v2/server-signature.js +68 -0
- package/dist/cjs/v2/types.d.ts +277 -0
- package/dist/cjs/v2/types.js +18 -0
- package/dist/cjs/v2/workers/argon2id.js +7 -0
- package/dist/cjs/v2/workers/pbkdf2.js +7 -0
- package/dist/cjs/v2/workers/scrypt.d.ts +1 -0
- package/dist/cjs/v2/workers/scrypt.js +7 -0
- package/dist/cjs/v2/workers/sha.d.ts +1 -0
- package/dist/cjs/v2/workers/sha.js +7 -0
- package/dist/cjs/v2/workers/shared.d.ts +4 -0
- package/dist/cjs/v2/workers/shared.js +32 -0
- package/dist/esm/tsconfig.build.tsbuildinfo +1 -0
- package/dist/{index.js → esm/v1/index.js} +3 -3
- package/dist/esm/v1/types.js +1 -0
- package/dist/esm/v1/worker.d.ts +1 -0
- package/dist/esm/v2/algorithms/argon2id.d.ts +2 -0
- package/dist/esm/v2/algorithms/argon2id.js +20 -0
- package/dist/esm/v2/algorithms/pbkdf2.d.ts +2 -0
- package/dist/esm/v2/algorithms/pbkdf2.js +20 -0
- package/dist/esm/v2/algorithms/scrypt.d.ts +2 -0
- package/dist/esm/v2/algorithms/scrypt.js +14 -0
- package/dist/esm/v2/algorithms/sha.d.ts +2 -0
- package/dist/esm/v2/algorithms/sha.js +32 -0
- package/dist/esm/v2/algorithms/web/pbkdf2.d.ts +2 -0
- package/dist/esm/v2/algorithms/web/pbkdf2.js +16 -0
- package/dist/esm/v2/algorithms/web/sha.d.ts +2 -0
- package/dist/esm/v2/algorithms/web/sha.js +20 -0
- package/dist/esm/v2/capped-map.d.ts +7 -0
- package/dist/esm/v2/capped-map.js +15 -0
- package/dist/esm/v2/frameworks/express.d.ts +21 -0
- package/dist/esm/v2/frameworks/express.js +71 -0
- package/dist/esm/v2/frameworks/fastify.d.ts +26 -0
- package/dist/esm/v2/frameworks/fastify.js +82 -0
- package/dist/esm/v2/frameworks/h3.d.ts +37 -0
- package/dist/esm/v2/frameworks/h3.js +83 -0
- package/dist/esm/v2/frameworks/hono.d.ts +99 -0
- package/dist/esm/v2/frameworks/hono.js +80 -0
- package/dist/esm/v2/frameworks/nestjs.d.ts +79 -0
- package/dist/esm/v2/frameworks/nestjs.js +202 -0
- package/dist/esm/v2/frameworks/nextjs.d.ts +21 -0
- package/dist/esm/v2/frameworks/nextjs.js +106 -0
- package/dist/esm/v2/frameworks/shared.d.ts +8 -0
- package/dist/esm/v2/frameworks/shared.js +117 -0
- package/dist/esm/v2/frameworks/sveltekit.d.ts +29 -0
- package/dist/esm/v2/frameworks/sveltekit.js +95 -0
- package/dist/esm/v2/frameworks/types.d.ts +47 -0
- package/dist/esm/v2/frameworks/types.js +1 -0
- package/dist/esm/v2/helpers.d.ts +27 -0
- package/dist/esm/v2/helpers.js +112 -0
- package/dist/esm/v2/index.d.ts +19 -0
- package/dist/esm/v2/index.js +17 -0
- package/dist/esm/v2/obfuscation.d.ts +11 -0
- package/dist/esm/v2/obfuscation.js +70 -0
- package/dist/esm/v2/pow.d.ts +60 -0
- package/dist/esm/v2/pow.js +281 -0
- package/dist/esm/v2/server-signature.d.ts +12 -0
- package/dist/esm/v2/server-signature.js +63 -0
- package/dist/esm/v2/types.d.ts +277 -0
- package/dist/esm/v2/types.js +15 -0
- package/dist/esm/v2/workers/argon2id.d.ts +1 -0
- package/dist/esm/v2/workers/argon2id.js +5 -0
- package/dist/esm/v2/workers/pbkdf2.d.ts +1 -0
- package/dist/esm/v2/workers/pbkdf2.js +5 -0
- package/dist/esm/v2/workers/scrypt.d.ts +1 -0
- package/dist/esm/v2/workers/scrypt.js +5 -0
- package/dist/esm/v2/workers/sha.d.ts +1 -0
- package/dist/esm/v2/workers/sha.js +5 -0
- package/dist/esm/v2/workers/shared.d.ts +4 -0
- package/dist/esm/v2/workers/shared.js +29 -0
- package/package.json +138 -27
- package/cjs/dist/tsconfig.cjs.tsbuildinfo +0 -1
- package/cjs/package.json +0 -3
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- /package/{cjs/dist → dist/cjs/v1}/helpers.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/helpers.js +0 -0
- /package/{cjs/dist → dist/cjs/v1}/index.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/types.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/types.js +0 -0
- /package/{cjs/dist → dist/cjs/v1}/worker.d.ts +0 -0
- /package/{cjs/dist → dist/cjs/v1}/worker.js +0 -0
- /package/dist/{types.js → cjs/v2/workers/argon2id.d.ts} +0 -0
- /package/dist/{worker.d.ts → cjs/v2/workers/pbkdf2.d.ts} +0 -0
- /package/dist/{helpers.d.ts → esm/v1/helpers.d.ts} +0 -0
- /package/dist/{helpers.js → esm/v1/helpers.js} +0 -0
- /package/dist/{index.d.ts → esm/v1/index.d.ts} +0 -0
- /package/dist/{types.d.ts → esm/v1/types.d.ts} +0 -0
- /package/dist/{worker.js → esm/v1/worker.js} +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
var AltchaModule_1;
|
|
14
|
+
import { Module, Controller, Get, Post, Req, Injectable, Inject, HttpException, HttpStatus, createParamDecorator, } from '@nestjs/common';
|
|
15
|
+
import { randomInt } from '../helpers.js';
|
|
16
|
+
import { createChallenge } from '../pow.js';
|
|
17
|
+
import { deriveHmacKeySecret, verify } from './shared.js';
|
|
18
|
+
import { CappedMap } from '../capped-map.js';
|
|
19
|
+
export { CappedMap, deriveHmacKeySecret, randomInt };
|
|
20
|
+
const ALTCHA_OPTIONS = Symbol('ALTCHA_OPTIONS');
|
|
21
|
+
export const Altcha = createParamDecorator((_data, ctx) => {
|
|
22
|
+
const request = ctx.switchToHttp().getRequest();
|
|
23
|
+
return request.altcha;
|
|
24
|
+
});
|
|
25
|
+
export function createAltchaMiddleware(options = {}) {
|
|
26
|
+
const { throwOnFailure = true } = options;
|
|
27
|
+
let AltchaMiddleware = class AltchaMiddleware {
|
|
28
|
+
altchaService;
|
|
29
|
+
constructor(altchaService) {
|
|
30
|
+
this.altchaService = altchaService;
|
|
31
|
+
}
|
|
32
|
+
async use(req, res, next) {
|
|
33
|
+
const payload = this.altchaService.getPayloadFromRequest(req);
|
|
34
|
+
const { error, payload: resultPayload, verification, } = await this.altchaService.verify(payload);
|
|
35
|
+
req.altcha = {
|
|
36
|
+
error,
|
|
37
|
+
payload: resultPayload,
|
|
38
|
+
verification,
|
|
39
|
+
};
|
|
40
|
+
const setCookie = this.altchaService.setCookie;
|
|
41
|
+
if (setCookie) {
|
|
42
|
+
res.clearCookie(setCookie.name);
|
|
43
|
+
}
|
|
44
|
+
if (error && throwOnFailure) {
|
|
45
|
+
throw new HttpException(error, HttpStatus.BAD_REQUEST);
|
|
46
|
+
}
|
|
47
|
+
next();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
AltchaMiddleware = __decorate([
|
|
51
|
+
Injectable(),
|
|
52
|
+
__metadata("design:paramtypes", [AltchaService])
|
|
53
|
+
], AltchaMiddleware);
|
|
54
|
+
return AltchaMiddleware;
|
|
55
|
+
}
|
|
56
|
+
let AltchaService = class AltchaService {
|
|
57
|
+
hmacSignatureSecret;
|
|
58
|
+
hmacKeySignatureSecret;
|
|
59
|
+
createChallengeParameters;
|
|
60
|
+
deriveKey;
|
|
61
|
+
fieldName;
|
|
62
|
+
setCookieOptions;
|
|
63
|
+
store;
|
|
64
|
+
constructor(options) {
|
|
65
|
+
this.hmacSignatureSecret = options.hmacSignatureSecret;
|
|
66
|
+
this.hmacKeySignatureSecret = options.hmacKeySignatureSecret;
|
|
67
|
+
this.createChallengeParameters = options.createChallengeParameters;
|
|
68
|
+
this.deriveKey = options.deriveKey;
|
|
69
|
+
this.fieldName = options.fieldName || 'altcha';
|
|
70
|
+
this.setCookieOptions = options.setCookie;
|
|
71
|
+
this.store = options.store;
|
|
72
|
+
}
|
|
73
|
+
get setCookie() {
|
|
74
|
+
return this.setCookieOptions;
|
|
75
|
+
}
|
|
76
|
+
async getChallenge() {
|
|
77
|
+
const challenge = await createChallenge({
|
|
78
|
+
deriveKey: this.deriveKey,
|
|
79
|
+
hmacSignatureSecret: this.hmacSignatureSecret,
|
|
80
|
+
hmacKeySignatureSecret: this.hmacKeySignatureSecret,
|
|
81
|
+
...this.createChallengeParameters(),
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
configuration: this.setCookieOptions
|
|
85
|
+
? { setCookie: this.setCookieOptions }
|
|
86
|
+
: undefined,
|
|
87
|
+
...challenge,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
getPayloadFromRequest(req, cookieName) {
|
|
91
|
+
if (cookieName) {
|
|
92
|
+
return req.cookies?.[cookieName];
|
|
93
|
+
}
|
|
94
|
+
return req.body?.[this.fieldName];
|
|
95
|
+
}
|
|
96
|
+
async verify(payload) {
|
|
97
|
+
return verify(payload, this.deriveKey, this.hmacSignatureSecret, this.hmacKeySignatureSecret, this.store);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
AltchaService = __decorate([
|
|
101
|
+
Injectable(),
|
|
102
|
+
__param(0, Inject(ALTCHA_OPTIONS)),
|
|
103
|
+
__metadata("design:paramtypes", [Object])
|
|
104
|
+
], AltchaService);
|
|
105
|
+
export { AltchaService };
|
|
106
|
+
let AltchaController = class AltchaController {
|
|
107
|
+
altchaService;
|
|
108
|
+
constructor(altchaService) {
|
|
109
|
+
this.altchaService = altchaService;
|
|
110
|
+
}
|
|
111
|
+
async getChallenge() {
|
|
112
|
+
return this.altchaService.getChallenge();
|
|
113
|
+
}
|
|
114
|
+
async verifySolution(req) {
|
|
115
|
+
const payload = this.altchaService.getPayloadFromRequest(req);
|
|
116
|
+
return this.altchaService.verify(payload);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
__decorate([
|
|
120
|
+
Get('challenge'),
|
|
121
|
+
__metadata("design:type", Function),
|
|
122
|
+
__metadata("design:paramtypes", []),
|
|
123
|
+
__metadata("design:returntype", Promise)
|
|
124
|
+
], AltchaController.prototype, "getChallenge", null);
|
|
125
|
+
__decorate([
|
|
126
|
+
Post('verify'),
|
|
127
|
+
__param(0, Req()),
|
|
128
|
+
__metadata("design:type", Function),
|
|
129
|
+
__metadata("design:paramtypes", [Object]),
|
|
130
|
+
__metadata("design:returntype", Promise)
|
|
131
|
+
], AltchaController.prototype, "verifySolution", null);
|
|
132
|
+
AltchaController = __decorate([
|
|
133
|
+
Controller('altcha'),
|
|
134
|
+
__metadata("design:paramtypes", [AltchaService])
|
|
135
|
+
], AltchaController);
|
|
136
|
+
export { AltchaController };
|
|
137
|
+
let AltchaMiddleware = class AltchaMiddleware {
|
|
138
|
+
altchaService;
|
|
139
|
+
constructor(altchaService) {
|
|
140
|
+
this.altchaService = altchaService;
|
|
141
|
+
}
|
|
142
|
+
async use(req, res, next) {
|
|
143
|
+
const payload = this.altchaService.getPayloadFromRequest(req, this.altchaService.setCookie?.name);
|
|
144
|
+
const { error, payload: resultPayload, verification, } = await this.altchaService.verify(payload);
|
|
145
|
+
req.altcha = {
|
|
146
|
+
error,
|
|
147
|
+
payload: resultPayload,
|
|
148
|
+
verification,
|
|
149
|
+
};
|
|
150
|
+
const setCookie = this.altchaService.setCookie;
|
|
151
|
+
if (setCookie) {
|
|
152
|
+
res.clearCookie(setCookie.name);
|
|
153
|
+
}
|
|
154
|
+
if (error) {
|
|
155
|
+
throw new HttpException(error, HttpStatus.BAD_REQUEST);
|
|
156
|
+
}
|
|
157
|
+
next();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
AltchaMiddleware = __decorate([
|
|
161
|
+
Injectable(),
|
|
162
|
+
__metadata("design:paramtypes", [AltchaService])
|
|
163
|
+
], AltchaMiddleware);
|
|
164
|
+
export { AltchaMiddleware };
|
|
165
|
+
let AltchaModule = AltchaModule_1 = class AltchaModule {
|
|
166
|
+
static register(options) {
|
|
167
|
+
return {
|
|
168
|
+
module: AltchaModule_1,
|
|
169
|
+
controllers: [AltchaController],
|
|
170
|
+
providers: [
|
|
171
|
+
{
|
|
172
|
+
provide: ALTCHA_OPTIONS,
|
|
173
|
+
useValue: options,
|
|
174
|
+
},
|
|
175
|
+
AltchaService,
|
|
176
|
+
],
|
|
177
|
+
exports: [AltchaService, AltchaMiddleware],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
static registerAsync(asyncOptions) {
|
|
181
|
+
return {
|
|
182
|
+
module: AltchaModule_1,
|
|
183
|
+
imports: asyncOptions.imports ?? [],
|
|
184
|
+
controllers: [AltchaController],
|
|
185
|
+
providers: [
|
|
186
|
+
{
|
|
187
|
+
provide: ALTCHA_OPTIONS,
|
|
188
|
+
useFactory: asyncOptions.useFactory,
|
|
189
|
+
inject: asyncOptions.inject ?? [],
|
|
190
|
+
},
|
|
191
|
+
AltchaService,
|
|
192
|
+
AltchaMiddleware,
|
|
193
|
+
],
|
|
194
|
+
exports: [AltchaService, AltchaMiddleware],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
AltchaModule = AltchaModule_1 = __decorate([
|
|
199
|
+
Module({})
|
|
200
|
+
], AltchaModule);
|
|
201
|
+
export { AltchaModule };
|
|
202
|
+
export default AltchaModule;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomInt } from '../helpers.js';
|
|
2
|
+
import { CappedMap } from '../capped-map.js';
|
|
3
|
+
import { deriveHmacKeySecret, verify } from './shared.js';
|
|
4
|
+
import type { AltchaMiddlewareOptions, AltchaOptions, AltchaResult } from './types.js';
|
|
5
|
+
export { CappedMap, deriveHmacKeySecret, randomInt };
|
|
6
|
+
export type { AltchaOptions, AltchaResult };
|
|
7
|
+
export declare function create(options: AltchaOptions): {
|
|
8
|
+
challengeHandler: (_req: Request) => Promise<Response>;
|
|
9
|
+
withMiddleware: (handler: (req: Request) => Promise<Response> | Response, options?: AltchaMiddlewareOptions) => (req: Request) => Promise<Response>;
|
|
10
|
+
verifyHandler: (req: Request) => Promise<Response>;
|
|
11
|
+
getPayloadFromRequest: (req: Request, cookieName?: string) => Promise<string | undefined>;
|
|
12
|
+
middleware: (req: Request, throwOnFailure?: boolean) => Promise<Response | AltchaResult>;
|
|
13
|
+
verify: typeof verify;
|
|
14
|
+
};
|
|
15
|
+
declare const _default: {
|
|
16
|
+
CappedMap: typeof CappedMap;
|
|
17
|
+
create: typeof create;
|
|
18
|
+
deriveHmacKeySecret: typeof deriveHmacKeySecret;
|
|
19
|
+
randomInt: typeof randomInt;
|
|
20
|
+
};
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomInt } from '../helpers.js';
|
|
2
|
+
import { createChallenge } from '../pow.js';
|
|
3
|
+
import { CappedMap } from '../capped-map.js';
|
|
4
|
+
import { deriveHmacKeySecret, verify } from './shared.js';
|
|
5
|
+
export { CappedMap, deriveHmacKeySecret, randomInt };
|
|
6
|
+
function getCookieFromRequest(req, name) {
|
|
7
|
+
const header = req.headers.get('cookie');
|
|
8
|
+
if (!header) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
12
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
13
|
+
}
|
|
14
|
+
function deleteCookie(res, name, path) {
|
|
15
|
+
res.headers.append('Set-Cookie', `${name}=; Path=${path ?? '/'}; Max-Age=0`);
|
|
16
|
+
}
|
|
17
|
+
export function create(options) {
|
|
18
|
+
const { createChallengeParameters, deriveKey, fieldName = 'altcha', hmacSignatureSecret, hmacKeySignatureSecret, setCookie, store, } = options;
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
|
+
async function challengeHandler(_req) {
|
|
21
|
+
const challenge = await createChallenge({
|
|
22
|
+
deriveKey,
|
|
23
|
+
hmacSignatureSecret,
|
|
24
|
+
hmacKeySignatureSecret,
|
|
25
|
+
...createChallengeParameters(),
|
|
26
|
+
});
|
|
27
|
+
const body = {
|
|
28
|
+
configuration: setCookie
|
|
29
|
+
? {
|
|
30
|
+
setCookie,
|
|
31
|
+
}
|
|
32
|
+
: undefined,
|
|
33
|
+
...challenge,
|
|
34
|
+
};
|
|
35
|
+
const response = Response.json(body);
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
async function verifyHandler(req) {
|
|
39
|
+
const payload = await getPayloadFromRequest(req);
|
|
40
|
+
const result = await verify(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret, store);
|
|
41
|
+
return Response.json(result);
|
|
42
|
+
}
|
|
43
|
+
async function getPayloadFromRequest(req, cookieName) {
|
|
44
|
+
let payload = undefined;
|
|
45
|
+
if (cookieName) {
|
|
46
|
+
payload = getCookieFromRequest(req, cookieName);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const contentType = req.headers.get('content-type') ?? '';
|
|
50
|
+
let body = null;
|
|
51
|
+
if (contentType.includes('application/json')) {
|
|
52
|
+
body = await req.json();
|
|
53
|
+
}
|
|
54
|
+
else if (contentType.includes('multipart/form-data') ||
|
|
55
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
56
|
+
body = Object.fromEntries((await req.formData()).entries());
|
|
57
|
+
}
|
|
58
|
+
payload = body?.[fieldName];
|
|
59
|
+
}
|
|
60
|
+
return payload;
|
|
61
|
+
}
|
|
62
|
+
async function middleware(req, throwOnFailure = true) {
|
|
63
|
+
const payload = await getPayloadFromRequest(req, setCookie?.name);
|
|
64
|
+
const { error, payload: verifiedPayload, verification, } = await verify(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret, store);
|
|
65
|
+
const result = {
|
|
66
|
+
error,
|
|
67
|
+
payload: verifiedPayload,
|
|
68
|
+
verification,
|
|
69
|
+
};
|
|
70
|
+
const response = Response.json(result);
|
|
71
|
+
if (setCookie) {
|
|
72
|
+
deleteCookie(response, setCookie.name, setCookie.path);
|
|
73
|
+
}
|
|
74
|
+
if (error && throwOnFailure) {
|
|
75
|
+
return Response.json({ error }, { status: 403 });
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
function withMiddleware(handler, options = {}) {
|
|
80
|
+
const { throwOnFailure = true } = options;
|
|
81
|
+
return async (req) => {
|
|
82
|
+
const result = await middleware(req, throwOnFailure);
|
|
83
|
+
// If middleware returned a Response, it means verification failed
|
|
84
|
+
if (result instanceof Response) {
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
// Attach the result to the request for the handler to access
|
|
88
|
+
req.__altcha = result;
|
|
89
|
+
return handler(req);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
challengeHandler,
|
|
94
|
+
withMiddleware,
|
|
95
|
+
verifyHandler,
|
|
96
|
+
getPayloadFromRequest,
|
|
97
|
+
middleware,
|
|
98
|
+
verify,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export default {
|
|
102
|
+
CappedMap,
|
|
103
|
+
create,
|
|
104
|
+
deriveHmacKeySecret,
|
|
105
|
+
randomInt,
|
|
106
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DeriveKeyFunction, Payload, ServerSignaturePayload, VerifySolutionResult } from '../types.js';
|
|
2
|
+
import type { Store } from './types.js';
|
|
3
|
+
export declare function deriveHmacKeySecret(masterSecret: string): Promise<string>;
|
|
4
|
+
export declare function verify(payload: unknown, deriveKey: DeriveKeyFunction, hmacSignatureSecret: string, hmacKeySignatureSecret?: string, store?: Store): Promise<{
|
|
5
|
+
error: string | null;
|
|
6
|
+
payload: Payload | ServerSignaturePayload | null;
|
|
7
|
+
verification: VerifySolutionResult | null;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { verifySolution } from '../pow.js';
|
|
2
|
+
import { verifyServerSignature } from '../server-signature.js';
|
|
3
|
+
import { bufferToHex, hmac } from '../helpers.js';
|
|
4
|
+
import { HmacAlgorithm, } from '../types.js';
|
|
5
|
+
export async function deriveHmacKeySecret(masterSecret) {
|
|
6
|
+
return bufferToHex(await hmac(HmacAlgorithm.SHA_256, masterSecret, 'derived-secret'));
|
|
7
|
+
}
|
|
8
|
+
export async function verify(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret, store) {
|
|
9
|
+
if (!payload) {
|
|
10
|
+
return {
|
|
11
|
+
error: 'ALTCHA payload is missing.',
|
|
12
|
+
payload: null,
|
|
13
|
+
verification: null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (typeof payload === 'string') {
|
|
17
|
+
payload = parsePayload(payload);
|
|
18
|
+
}
|
|
19
|
+
if (!payload) {
|
|
20
|
+
return {
|
|
21
|
+
error: 'ALTCHA payload is invalid.',
|
|
22
|
+
payload: null,
|
|
23
|
+
verification: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const type = getPayloadType(payload);
|
|
27
|
+
let verification = null;
|
|
28
|
+
let challengeId = null;
|
|
29
|
+
try {
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'client':
|
|
32
|
+
challengeId = getChallengeId(payload);
|
|
33
|
+
if (store && challengeId) {
|
|
34
|
+
await checkChallengeId(store, challengeId);
|
|
35
|
+
}
|
|
36
|
+
verification = await verifyClientPayload(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret);
|
|
37
|
+
break;
|
|
38
|
+
case 'server':
|
|
39
|
+
challengeId = payload.id;
|
|
40
|
+
if (store && challengeId) {
|
|
41
|
+
await checkChallengeId(store, challengeId);
|
|
42
|
+
}
|
|
43
|
+
verification = await verifyServerSignaturePayload(payload, hmacSignatureSecret);
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
throw new Error('ALTCHA payload is invalid.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
52
|
+
payload: payload,
|
|
53
|
+
verification: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!verification?.verified) {
|
|
57
|
+
return {
|
|
58
|
+
error: 'ALTCHA verification failed.',
|
|
59
|
+
payload: payload,
|
|
60
|
+
verification,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
error: null,
|
|
65
|
+
payload: payload,
|
|
66
|
+
verification,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function checkChallengeId(store, challengeId) {
|
|
70
|
+
if (await store.get(challengeId)) {
|
|
71
|
+
throw new Error('ALTCHA payload has been already used.');
|
|
72
|
+
}
|
|
73
|
+
await store.set(challengeId, true);
|
|
74
|
+
}
|
|
75
|
+
function getChallengeId(payload) {
|
|
76
|
+
const { challenge } = payload;
|
|
77
|
+
const data = challenge.parameters.data;
|
|
78
|
+
return data?.challengeId
|
|
79
|
+
? String(data.challengeId)
|
|
80
|
+
: challenge.parameters.nonce;
|
|
81
|
+
}
|
|
82
|
+
function getPayloadType(payload) {
|
|
83
|
+
if (!payload || typeof payload !== 'object') {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if ('verificationData' in payload) {
|
|
87
|
+
return 'server';
|
|
88
|
+
}
|
|
89
|
+
if ('challenge' in payload && 'solution' in payload) {
|
|
90
|
+
return 'client';
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function parsePayload(payload) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(atob(payload));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function verifyClientPayload(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret) {
|
|
103
|
+
const { challenge, solution } = payload;
|
|
104
|
+
return verifySolution({
|
|
105
|
+
challenge,
|
|
106
|
+
deriveKey,
|
|
107
|
+
hmacSignatureSecret,
|
|
108
|
+
hmacKeySignatureSecret,
|
|
109
|
+
solution,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function verifyServerSignaturePayload(payload, hmacSecret) {
|
|
113
|
+
return verifyServerSignature({
|
|
114
|
+
payload,
|
|
115
|
+
hmacSecret,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type RequestEvent, type Handle } from '@sveltejs/kit';
|
|
2
|
+
import { randomInt } from '../helpers.js';
|
|
3
|
+
import { CappedMap } from '../capped-map.js';
|
|
4
|
+
import { deriveHmacKeySecret, verify } from './shared.js';
|
|
5
|
+
import type { AltchaMiddlewareOptions, AltchaOptions, AltchaResult } from './types.js';
|
|
6
|
+
export { CappedMap, deriveHmacKeySecret, randomInt };
|
|
7
|
+
export type { AltchaOptions, AltchaResult };
|
|
8
|
+
declare global {
|
|
9
|
+
namespace App {
|
|
10
|
+
interface Locals {
|
|
11
|
+
altcha?: AltchaResult;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export declare function create(options: AltchaOptions): {
|
|
16
|
+
challengeHandler: () => Promise<Response>;
|
|
17
|
+
verifyHandler: (event: RequestEvent) => Promise<Response>;
|
|
18
|
+
verifyEvent: (event: RequestEvent) => Promise<AltchaResult>;
|
|
19
|
+
createHandle: (middlewareOptions?: AltchaMiddlewareOptions) => Handle;
|
|
20
|
+
getPayloadFromEvent: (event: RequestEvent, cookieName?: string) => Promise<string | undefined>;
|
|
21
|
+
verify: typeof verify;
|
|
22
|
+
};
|
|
23
|
+
declare const _default: {
|
|
24
|
+
CappedMap: typeof CappedMap;
|
|
25
|
+
create: typeof create;
|
|
26
|
+
deriveHmacKeySecret: typeof deriveHmacKeySecret;
|
|
27
|
+
randomInt: typeof randomInt;
|
|
28
|
+
};
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { error, json } from '@sveltejs/kit';
|
|
2
|
+
import { createChallenge } from '../pow.js';
|
|
3
|
+
import { randomInt } from '../helpers.js';
|
|
4
|
+
import { CappedMap } from '../capped-map.js';
|
|
5
|
+
import { deriveHmacKeySecret, verify } from './shared.js';
|
|
6
|
+
export { CappedMap, deriveHmacKeySecret, randomInt };
|
|
7
|
+
async function getPayloadFromEvent(event, fieldName, cookieName) {
|
|
8
|
+
if (cookieName) {
|
|
9
|
+
return event.cookies.get(cookieName);
|
|
10
|
+
}
|
|
11
|
+
const contentType = event.request.headers.get('content-type') ?? '';
|
|
12
|
+
let body = null;
|
|
13
|
+
if (contentType.includes('application/json')) {
|
|
14
|
+
body = await event.request.json();
|
|
15
|
+
}
|
|
16
|
+
else if (contentType.includes('multipart/form-data') ||
|
|
17
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
18
|
+
const formData = await event.request.formData();
|
|
19
|
+
body = Object.fromEntries(formData.entries());
|
|
20
|
+
}
|
|
21
|
+
return body?.[fieldName];
|
|
22
|
+
}
|
|
23
|
+
export function create(options) {
|
|
24
|
+
const { createChallengeParameters, deriveKey, fieldName = 'altcha', hmacSignatureSecret, hmacKeySignatureSecret, setCookie, store, } = options;
|
|
25
|
+
async function challengeHandler() {
|
|
26
|
+
const challenge = await createChallenge({
|
|
27
|
+
deriveKey,
|
|
28
|
+
hmacSignatureSecret,
|
|
29
|
+
hmacKeySignatureSecret,
|
|
30
|
+
...createChallengeParameters(),
|
|
31
|
+
});
|
|
32
|
+
return json({
|
|
33
|
+
configuration: setCookie
|
|
34
|
+
? {
|
|
35
|
+
setCookie,
|
|
36
|
+
}
|
|
37
|
+
: undefined,
|
|
38
|
+
...challenge,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function verifyHandler(event) {
|
|
42
|
+
const payload = await getPayloadFromEvent(event, fieldName);
|
|
43
|
+
const result = await verify(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret, store);
|
|
44
|
+
return json(result);
|
|
45
|
+
}
|
|
46
|
+
function createHandle(middlewareOptions = {}) {
|
|
47
|
+
const { throwOnFailure = true } = middlewareOptions;
|
|
48
|
+
return async ({ event, resolve }) => {
|
|
49
|
+
const payload = await getPayloadFromEvent(event, fieldName, setCookie?.name);
|
|
50
|
+
const { error: verifyError, payload: resultPayload, verification, } = await verify(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret, store);
|
|
51
|
+
event.locals.altcha = {
|
|
52
|
+
error: verifyError,
|
|
53
|
+
payload: resultPayload,
|
|
54
|
+
verification,
|
|
55
|
+
};
|
|
56
|
+
if (setCookie) {
|
|
57
|
+
event.cookies.delete(setCookie.name, {
|
|
58
|
+
path: setCookie.path ?? '/',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (verifyError && throwOnFailure) {
|
|
62
|
+
error(403, verifyError);
|
|
63
|
+
}
|
|
64
|
+
return resolve(event);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function verifyEvent(event) {
|
|
68
|
+
const payload = await getPayloadFromEvent(event, fieldName, setCookie?.name);
|
|
69
|
+
const { error: verifyError, payload: resultPayload, verification, } = await verify(payload, deriveKey, hmacSignatureSecret, hmacKeySignatureSecret, store);
|
|
70
|
+
if (setCookie) {
|
|
71
|
+
event.cookies.delete(setCookie.name, {
|
|
72
|
+
path: setCookie.path ?? '/',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
error: verifyError,
|
|
77
|
+
payload: resultPayload,
|
|
78
|
+
verification,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
challengeHandler,
|
|
83
|
+
verifyHandler,
|
|
84
|
+
verifyEvent,
|
|
85
|
+
createHandle,
|
|
86
|
+
getPayloadFromEvent: (event, cookieName) => getPayloadFromEvent(event, fieldName, cookieName),
|
|
87
|
+
verify,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export default {
|
|
91
|
+
CappedMap,
|
|
92
|
+
create,
|
|
93
|
+
deriveHmacKeySecret,
|
|
94
|
+
randomInt,
|
|
95
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CreateChallengeOptions, DeriveKeyFunction, Payload, ServerSignaturePayload, SetCookieOptions, VerifySolutionResult } from '../types.js';
|
|
2
|
+
export interface AltchaOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Returns configuration options passed to `createChallenge`.
|
|
5
|
+
* Must include `algorithm` and `cost`; all other `CreateChallengeOptions` fields are optional.
|
|
6
|
+
*/
|
|
7
|
+
createChallengeParameters: () => Pick<CreateChallengeOptions, 'algorithm' | 'cost'> & Partial<CreateChallengeOptions>;
|
|
8
|
+
/**
|
|
9
|
+
* Algorithm-specific key derivation function used to generate challenge keys.
|
|
10
|
+
*/
|
|
11
|
+
deriveKey: DeriveKeyFunction;
|
|
12
|
+
/**
|
|
13
|
+
* Name of the form field that carries the ALTCHA payload.
|
|
14
|
+
* Defaults to `'altcha'` if not specified.
|
|
15
|
+
*/
|
|
16
|
+
fieldName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* HMAC secret used to sign and verify challenge signatures.
|
|
19
|
+
*/
|
|
20
|
+
hmacSignatureSecret: string;
|
|
21
|
+
/**
|
|
22
|
+
* HMAC secret used to sign keys in deterministic mode.
|
|
23
|
+
*/
|
|
24
|
+
hmacKeySignatureSecret?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Cookie configuration for sending the payload via a cookie.
|
|
27
|
+
* The `name` field is required; all other `SetCookieOptions` fields are optional.
|
|
28
|
+
*/
|
|
29
|
+
setCookie?: RequireField<SetCookieOptions, 'name'>;
|
|
30
|
+
/**
|
|
31
|
+
* Store implementation for tracking used challenges and preventing replay attacks.
|
|
32
|
+
*/
|
|
33
|
+
store?: Store;
|
|
34
|
+
}
|
|
35
|
+
export interface AltchaMiddlewareOptions {
|
|
36
|
+
throwOnFailure?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface AltchaResult {
|
|
39
|
+
error: string | null;
|
|
40
|
+
payload: Payload | ServerSignaturePayload | null;
|
|
41
|
+
verification: VerifySolutionResult | null;
|
|
42
|
+
}
|
|
43
|
+
export type RequireField<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
44
|
+
export interface Store {
|
|
45
|
+
get: (key: string) => Promise<unknown> | unknown;
|
|
46
|
+
set: (key: string, value: boolean) => Promise<unknown> | unknown;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HmacAlgorithm } from './types.js';
|
|
2
|
+
/** Checks if a buffer starts with the given prefix bytes. */
|
|
3
|
+
export declare function bufferStartsWith(buffer: Uint8Array, prefix: Uint8Array): boolean;
|
|
4
|
+
/** Converts a byte buffer to a lowercase hex string. */
|
|
5
|
+
export declare function bufferToHex(buffer: Uint8Array | ArrayBuffer): string;
|
|
6
|
+
/** Returns a canonical (sorted-key) JSON string for consistent hashing/signing. */
|
|
7
|
+
export declare function canonicalJSON(obj: unknown): string;
|
|
8
|
+
/** Concatenates two Uint8Arrays into a new buffer. */
|
|
9
|
+
export declare function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array<ArrayBuffer>;
|
|
10
|
+
/** Converts a hex string to a Uint8Array. Throws if the string has odd length. */
|
|
11
|
+
export declare function hexToBuffer(hex: string): Uint8Array;
|
|
12
|
+
/** Checks if two strings are equal in constant time. */
|
|
13
|
+
export declare function constantTimeEqual(a: string, b: string): boolean;
|
|
14
|
+
/** Yields to the event loop after the given milliseconds. */
|
|
15
|
+
export declare function delay(ms: number): Promise<void>;
|
|
16
|
+
/** Generate a SHA hash */
|
|
17
|
+
export declare function hash(algorithm: string, data: ArrayBuffer | string): Promise<Uint8Array>;
|
|
18
|
+
/** Computes an HMAC signature using the Web Crypto API. */
|
|
19
|
+
export declare function hmac(algorithm: HmacAlgorithm, data: string | Uint8Array, keyStr: string): Promise<Uint8Array>;
|
|
20
|
+
/** Inject CSS tag into the document */
|
|
21
|
+
export declare function injectCss(css: string, id?: string): void;
|
|
22
|
+
/** Generates a random integer between the specified minimum and maximum values (inclusive). */
|
|
23
|
+
export declare function randomInt(max: number, min?: number): number;
|
|
24
|
+
/** Recursively sorts object keys alphabetically for deterministic serialization. */
|
|
25
|
+
export declare function sortKeys<T = unknown>(obj: T): T;
|
|
26
|
+
/** Returns elapsed time in milliseconds since `start`, rounded to one decimal. */
|
|
27
|
+
export declare function timeDuration(start: number): number;
|