@boxyhq/saml-jackson 0.2.3-beta.231 → 0.2.3-beta.235
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 +1 -2
- package/package.json +12 -4
- package/ nodemon.json +0 -12
- package/.dockerignore +0 -2
- package/.eslintrc.js +0 -18
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -43
- package/.github/pull_request_template.md +0 -31
- package/.github/workflows/codesee-arch-diagram.yml +0 -81
- package/.github/workflows/main.yml +0 -123
- package/_dev/docker-compose.yml +0 -37
- package/map.js +0 -1
- package/prettier.config.js +0 -4
- package/src/controller/api.ts +0 -225
- package/src/controller/error.ts +0 -13
- package/src/controller/oauth/allowed.ts +0 -22
- package/src/controller/oauth/code-verifier.ts +0 -11
- package/src/controller/oauth/redirect.ts +0 -12
- package/src/controller/oauth.ts +0 -334
- package/src/controller/utils.ts +0 -17
- package/src/db/db.ts +0 -100
- package/src/db/encrypter.ts +0 -38
- package/src/db/mem.ts +0 -128
- package/src/db/mongo.ts +0 -110
- package/src/db/redis.ts +0 -103
- package/src/db/sql/entity/JacksonIndex.ts +0 -43
- package/src/db/sql/entity/JacksonStore.ts +0 -43
- package/src/db/sql/entity/JacksonTTL.ts +0 -17
- package/src/db/sql/model/JacksonIndex.ts +0 -3
- package/src/db/sql/model/JacksonStore.ts +0 -8
- package/src/db/sql/sql.ts +0 -181
- package/src/db/store.ts +0 -49
- package/src/db/utils.ts +0 -26
- package/src/env.ts +0 -42
- package/src/index.ts +0 -84
- package/src/jackson.ts +0 -173
- package/src/read-config.ts +0 -29
- package/src/saml/claims.ts +0 -41
- package/src/saml/saml.ts +0 -233
- package/src/saml/x509.ts +0 -51
- package/src/test/api.test.ts +0 -270
- package/src/test/data/metadata/boxyhq.js +0 -6
- package/src/test/data/metadata/boxyhq.xml +0 -30
- package/src/test/data/saml_response +0 -1
- package/src/test/db.test.ts +0 -313
- package/src/test/oauth.test.ts +0 -362
- package/src/typings.ts +0 -167
- package/tsconfig.build.json +0 -6
- package/tsconfig.json +0 -26
package/src/controller/error.ts
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
export class JacksonError extends Error {
|
2
|
-
public name: string;
|
3
|
-
public statusCode: number;
|
4
|
-
|
5
|
-
constructor(message: string, statusCode = 500) {
|
6
|
-
super(message);
|
7
|
-
|
8
|
-
this.name = this.constructor.name;
|
9
|
-
this.statusCode = statusCode;
|
10
|
-
|
11
|
-
Error.captureStackTrace(this, this.constructor);
|
12
|
-
}
|
13
|
-
}
|
@@ -1,22 +0,0 @@
|
|
1
|
-
export const redirect = (
|
2
|
-
redirectUrl: string,
|
3
|
-
redirectUrls: string[]
|
4
|
-
): boolean => {
|
5
|
-
const url: URL = new URL(redirectUrl);
|
6
|
-
|
7
|
-
for (const idx in redirectUrls) {
|
8
|
-
const rUrl: URL = new URL(redirectUrls[idx]);
|
9
|
-
|
10
|
-
// TODO: Check pathname, for now pathname is ignored
|
11
|
-
|
12
|
-
if (
|
13
|
-
rUrl.protocol === url.protocol &&
|
14
|
-
rUrl.hostname === url.hostname &&
|
15
|
-
rUrl.port === url.port
|
16
|
-
) {
|
17
|
-
return true;
|
18
|
-
}
|
19
|
-
}
|
20
|
-
|
21
|
-
return false;
|
22
|
-
};
|
@@ -1,11 +0,0 @@
|
|
1
|
-
import crypto from 'crypto';
|
2
|
-
|
3
|
-
export const transformBase64 = (input: string): string => {
|
4
|
-
return input.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
5
|
-
};
|
6
|
-
|
7
|
-
export const encode = (code_challenge: string): string => {
|
8
|
-
return transformBase64(
|
9
|
-
crypto.createHash('sha256').update(code_challenge).digest('base64')
|
10
|
-
);
|
11
|
-
};
|
@@ -1,12 +0,0 @@
|
|
1
|
-
export const success = (
|
2
|
-
redirectUrl: string,
|
3
|
-
params: Record<string, string>
|
4
|
-
): string => {
|
5
|
-
const url: URL = new URL(redirectUrl);
|
6
|
-
|
7
|
-
for (const [key, value] of Object.entries(params)) {
|
8
|
-
url.searchParams.set(key, value);
|
9
|
-
}
|
10
|
-
|
11
|
-
return url.href;
|
12
|
-
};
|
package/src/controller/oauth.ts
DELETED
@@ -1,334 +0,0 @@
|
|
1
|
-
import crypto from 'crypto';
|
2
|
-
import {
|
3
|
-
IOAuthController,
|
4
|
-
JacksonOption,
|
5
|
-
OAuthReqBody,
|
6
|
-
OAuthTokenReq,
|
7
|
-
OAuthTokenRes,
|
8
|
-
Profile,
|
9
|
-
SAMLResponsePayload,
|
10
|
-
Storable,
|
11
|
-
} from 'saml-jackson';
|
12
|
-
import * as dbutils from '../db/utils';
|
13
|
-
import saml from '../saml/saml';
|
14
|
-
import { JacksonError } from './error';
|
15
|
-
import * as allowed from './oauth/allowed';
|
16
|
-
import * as codeVerifier from './oauth/code-verifier';
|
17
|
-
import * as redirect from './oauth/redirect';
|
18
|
-
import { IndexNames } from './utils';
|
19
|
-
|
20
|
-
const relayStatePrefix = 'boxyhq_jackson_';
|
21
|
-
|
22
|
-
function getEncodedClientId(
|
23
|
-
client_id: string
|
24
|
-
): { tenant: string | null; product: string | null } | null {
|
25
|
-
try {
|
26
|
-
const sp = new URLSearchParams(client_id);
|
27
|
-
const tenant = sp.get('tenant');
|
28
|
-
const product = sp.get('product');
|
29
|
-
if (tenant && product) {
|
30
|
-
return {
|
31
|
-
tenant: sp.get('tenant'),
|
32
|
-
product: sp.get('product'),
|
33
|
-
};
|
34
|
-
}
|
35
|
-
|
36
|
-
return null;
|
37
|
-
} catch (err) {
|
38
|
-
return null;
|
39
|
-
}
|
40
|
-
}
|
41
|
-
|
42
|
-
export class OAuthController implements IOAuthController {
|
43
|
-
private configStore: Storable;
|
44
|
-
private sessionStore: Storable;
|
45
|
-
private codeStore: Storable;
|
46
|
-
private tokenStore: Storable;
|
47
|
-
private opts: JacksonOption;
|
48
|
-
|
49
|
-
constructor({ configStore, sessionStore, codeStore, tokenStore, opts }) {
|
50
|
-
this.configStore = configStore;
|
51
|
-
this.sessionStore = sessionStore;
|
52
|
-
this.codeStore = codeStore;
|
53
|
-
this.tokenStore = tokenStore;
|
54
|
-
this.opts = opts;
|
55
|
-
}
|
56
|
-
|
57
|
-
public async authorize(
|
58
|
-
body: OAuthReqBody
|
59
|
-
): Promise<{ redirect_url: string }> {
|
60
|
-
const {
|
61
|
-
response_type = 'code',
|
62
|
-
client_id,
|
63
|
-
redirect_uri,
|
64
|
-
state,
|
65
|
-
tenant,
|
66
|
-
product,
|
67
|
-
code_challenge,
|
68
|
-
code_challenge_method = '',
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
70
|
-
provider = 'saml',
|
71
|
-
} = body;
|
72
|
-
|
73
|
-
if (!redirect_uri) {
|
74
|
-
throw new JacksonError('Please specify a redirect URL.', 400);
|
75
|
-
}
|
76
|
-
|
77
|
-
if (!state) {
|
78
|
-
throw new JacksonError(
|
79
|
-
'Please specify a state to safeguard against XSRF attacks.',
|
80
|
-
400
|
81
|
-
);
|
82
|
-
}
|
83
|
-
|
84
|
-
let samlConfig;
|
85
|
-
|
86
|
-
if (tenant && product) {
|
87
|
-
const samlConfigs = await this.configStore.getByIndex({
|
88
|
-
name: IndexNames.TenantProduct,
|
89
|
-
value: dbutils.keyFromParts(tenant, product),
|
90
|
-
});
|
91
|
-
|
92
|
-
if (!samlConfigs || samlConfigs.length === 0) {
|
93
|
-
throw new JacksonError('SAML configuration not found.', 403);
|
94
|
-
}
|
95
|
-
|
96
|
-
// TODO: Support multiple matches
|
97
|
-
samlConfig = samlConfigs[0];
|
98
|
-
} else if (
|
99
|
-
client_id &&
|
100
|
-
client_id !== '' &&
|
101
|
-
client_id !== 'undefined' &&
|
102
|
-
client_id !== 'null'
|
103
|
-
) {
|
104
|
-
// if tenant and product are encoded in the client_id then we parse it and check for the relevant config(s)
|
105
|
-
const sp = getEncodedClientId(client_id);
|
106
|
-
if (sp?.tenant) {
|
107
|
-
const samlConfigs = await this.configStore.getByIndex({
|
108
|
-
name: IndexNames.TenantProduct,
|
109
|
-
value: dbutils.keyFromParts(sp.tenant, sp.product || ''),
|
110
|
-
});
|
111
|
-
|
112
|
-
if (!samlConfigs || samlConfigs.length === 0) {
|
113
|
-
throw new JacksonError('SAML configuration not found.', 403);
|
114
|
-
}
|
115
|
-
|
116
|
-
// TODO: Support multiple matches
|
117
|
-
samlConfig = samlConfigs[0];
|
118
|
-
} else {
|
119
|
-
samlConfig = await this.configStore.get(client_id);
|
120
|
-
}
|
121
|
-
} else {
|
122
|
-
throw new JacksonError(
|
123
|
-
'You need to specify client_id or tenant & product',
|
124
|
-
403
|
125
|
-
);
|
126
|
-
}
|
127
|
-
|
128
|
-
if (!samlConfig) {
|
129
|
-
throw new JacksonError('SAML configuration not found.', 403);
|
130
|
-
}
|
131
|
-
|
132
|
-
if (!allowed.redirect(redirect_uri, samlConfig.redirectUrl)) {
|
133
|
-
throw new JacksonError('Redirect URL is not allowed.', 403);
|
134
|
-
}
|
135
|
-
|
136
|
-
const samlReq = saml.request({
|
137
|
-
entityID: this.opts.samlAudience,
|
138
|
-
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
|
139
|
-
signingKey: samlConfig.certs.privateKey,
|
140
|
-
});
|
141
|
-
|
142
|
-
const sessionId = crypto.randomBytes(16).toString('hex');
|
143
|
-
|
144
|
-
await this.sessionStore.put(sessionId, {
|
145
|
-
id: samlReq.id,
|
146
|
-
redirect_uri,
|
147
|
-
response_type,
|
148
|
-
state,
|
149
|
-
code_challenge,
|
150
|
-
code_challenge_method,
|
151
|
-
});
|
152
|
-
|
153
|
-
const redirectUrl = redirect.success(
|
154
|
-
samlConfig.idpMetadata.sso.redirectUrl,
|
155
|
-
{
|
156
|
-
RelayState: relayStatePrefix + sessionId,
|
157
|
-
SAMLRequest: Buffer.from(samlReq.request).toString('base64'),
|
158
|
-
}
|
159
|
-
);
|
160
|
-
|
161
|
-
return { redirect_url: redirectUrl };
|
162
|
-
}
|
163
|
-
|
164
|
-
public async samlResponse(
|
165
|
-
body: SAMLResponsePayload
|
166
|
-
): Promise<{ redirect_url: string }> {
|
167
|
-
const { SAMLResponse } = body; // RelayState will contain the sessionId from earlier quasi-oauth flow
|
168
|
-
|
169
|
-
let RelayState = body.RelayState || '';
|
170
|
-
|
171
|
-
if (!this.opts.idpEnabled && !RelayState.startsWith(relayStatePrefix)) {
|
172
|
-
// IDP is disabled so block the request
|
173
|
-
|
174
|
-
throw new JacksonError(
|
175
|
-
'IdP (Identity Provider) flow has been disabled. Please head to your Service Provider to login.',
|
176
|
-
403
|
177
|
-
);
|
178
|
-
}
|
179
|
-
|
180
|
-
if (!RelayState.startsWith(relayStatePrefix)) {
|
181
|
-
RelayState = '';
|
182
|
-
}
|
183
|
-
|
184
|
-
RelayState = RelayState.replace(relayStatePrefix, '');
|
185
|
-
|
186
|
-
const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
|
187
|
-
|
188
|
-
const parsedResp = await saml.parseAsync(rawResponse);
|
189
|
-
|
190
|
-
const samlConfigs = await this.configStore.getByIndex({
|
191
|
-
name: IndexNames.EntityID,
|
192
|
-
value: parsedResp?.issuer,
|
193
|
-
});
|
194
|
-
|
195
|
-
if (!samlConfigs || samlConfigs.length === 0) {
|
196
|
-
throw new JacksonError('SAML configuration not found.', 403);
|
197
|
-
}
|
198
|
-
|
199
|
-
// TODO: Support multiple matches
|
200
|
-
const samlConfig = samlConfigs[0];
|
201
|
-
|
202
|
-
let session;
|
203
|
-
|
204
|
-
if (RelayState !== '') {
|
205
|
-
session = await this.sessionStore.get(RelayState);
|
206
|
-
if (!session) {
|
207
|
-
throw new JacksonError(
|
208
|
-
'Unable to validate state from the origin request.',
|
209
|
-
403
|
210
|
-
);
|
211
|
-
}
|
212
|
-
}
|
213
|
-
|
214
|
-
const validateOpts: Record<string, string> = {
|
215
|
-
thumbprint: samlConfig.idpMetadata.thumbprint,
|
216
|
-
audience: this.opts.samlAudience,
|
217
|
-
};
|
218
|
-
|
219
|
-
if (session && session.id) {
|
220
|
-
validateOpts.inResponseTo = session.id;
|
221
|
-
}
|
222
|
-
|
223
|
-
const profile = await saml.validateAsync(rawResponse, validateOpts);
|
224
|
-
|
225
|
-
// store details against a code
|
226
|
-
const code = crypto.randomBytes(20).toString('hex');
|
227
|
-
|
228
|
-
const codeVal: Record<string, unknown> = {
|
229
|
-
profile,
|
230
|
-
clientID: samlConfig.clientID,
|
231
|
-
clientSecret: samlConfig.clientSecret,
|
232
|
-
};
|
233
|
-
|
234
|
-
if (session) {
|
235
|
-
codeVal.session = session;
|
236
|
-
}
|
237
|
-
|
238
|
-
await this.codeStore.put(code, codeVal);
|
239
|
-
|
240
|
-
if (
|
241
|
-
session &&
|
242
|
-
session.redirect_uri &&
|
243
|
-
!allowed.redirect(session.redirect_uri, samlConfig.redirectUrl)
|
244
|
-
) {
|
245
|
-
throw new JacksonError('Redirect URL is not allowed.', 403);
|
246
|
-
}
|
247
|
-
|
248
|
-
const params: Record<string, string> = {
|
249
|
-
code,
|
250
|
-
};
|
251
|
-
|
252
|
-
if (session && session.state) {
|
253
|
-
params.state = session.state;
|
254
|
-
}
|
255
|
-
|
256
|
-
const redirectUrl = redirect.success(
|
257
|
-
(session && session.redirect_uri) || samlConfig.defaultRedirectUrl,
|
258
|
-
params
|
259
|
-
);
|
260
|
-
|
261
|
-
return { redirect_url: redirectUrl };
|
262
|
-
}
|
263
|
-
|
264
|
-
public async token(body: OAuthTokenReq): Promise<OAuthTokenRes> {
|
265
|
-
const {
|
266
|
-
client_id,
|
267
|
-
client_secret,
|
268
|
-
code_verifier,
|
269
|
-
code,
|
270
|
-
grant_type = 'authorization_code',
|
271
|
-
} = body;
|
272
|
-
|
273
|
-
if (grant_type !== 'authorization_code') {
|
274
|
-
throw new JacksonError('Unsupported grant_type', 400);
|
275
|
-
}
|
276
|
-
|
277
|
-
if (!code) {
|
278
|
-
throw new JacksonError('Please specify code', 400);
|
279
|
-
}
|
280
|
-
|
281
|
-
const codeVal = await this.codeStore.get(code);
|
282
|
-
if (!codeVal || !codeVal.profile) {
|
283
|
-
throw new JacksonError('Invalid code', 403);
|
284
|
-
}
|
285
|
-
|
286
|
-
if (client_id && client_secret) {
|
287
|
-
// check if we have an encoded client_id
|
288
|
-
if (client_id !== 'dummy' && client_secret !== 'dummy') {
|
289
|
-
const sp = getEncodedClientId(client_id);
|
290
|
-
if (!sp) {
|
291
|
-
// OAuth flow
|
292
|
-
if (
|
293
|
-
client_id !== codeVal.clientID ||
|
294
|
-
client_secret !== codeVal.clientSecret
|
295
|
-
) {
|
296
|
-
throw new JacksonError('Invalid client_id or client_secret', 401);
|
297
|
-
}
|
298
|
-
}
|
299
|
-
}
|
300
|
-
} else if (code_verifier) {
|
301
|
-
// PKCE flow
|
302
|
-
let cv = code_verifier;
|
303
|
-
if (codeVal.session.code_challenge_method.toLowerCase() === 's256') {
|
304
|
-
cv = codeVerifier.encode(code_verifier);
|
305
|
-
}
|
306
|
-
|
307
|
-
if (codeVal.session.code_challenge !== cv) {
|
308
|
-
throw new JacksonError('Invalid code_verifier', 401);
|
309
|
-
}
|
310
|
-
} else if (codeVal && codeVal.session) {
|
311
|
-
throw new JacksonError(
|
312
|
-
'Please specify client_secret or code_verifier',
|
313
|
-
401
|
314
|
-
);
|
315
|
-
}
|
316
|
-
|
317
|
-
// store details against a token
|
318
|
-
const token = crypto.randomBytes(20).toString('hex');
|
319
|
-
|
320
|
-
await this.tokenStore.put(token, codeVal.profile);
|
321
|
-
|
322
|
-
return {
|
323
|
-
access_token: token,
|
324
|
-
token_type: 'bearer',
|
325
|
-
expires_in: this.opts.db.ttl,
|
326
|
-
};
|
327
|
-
}
|
328
|
-
|
329
|
-
public async userInfo(token: string): Promise<Profile> {
|
330
|
-
const { claims } = await this.tokenStore.get(token);
|
331
|
-
|
332
|
-
return claims;
|
333
|
-
}
|
334
|
-
}
|
package/src/controller/utils.ts
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
import { Request } from 'express';
|
2
|
-
|
3
|
-
export const extractAuthToken = (req: Request): string | null => {
|
4
|
-
const authHeader = req.get('authorization');
|
5
|
-
const parts = (authHeader || '').split(' ');
|
6
|
-
|
7
|
-
if (parts.length > 1) {
|
8
|
-
return parts[1];
|
9
|
-
}
|
10
|
-
|
11
|
-
return null;
|
12
|
-
};
|
13
|
-
|
14
|
-
export enum IndexNames {
|
15
|
-
EntityID = 'entityID',
|
16
|
-
TenantProduct = 'tenantProduct',
|
17
|
-
}
|
package/src/db/db.ts
DELETED
@@ -1,100 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
DatabaseDriver,
|
3
|
-
DatabaseOption,
|
4
|
-
Encrypted,
|
5
|
-
EncryptionKey,
|
6
|
-
Index,
|
7
|
-
Storable,
|
8
|
-
} from 'saml-jackson';
|
9
|
-
import * as encrypter from './encrypter';
|
10
|
-
import mem from './mem';
|
11
|
-
import mongo from './mongo';
|
12
|
-
import redis from './redis';
|
13
|
-
import sql from './sql/sql';
|
14
|
-
import store from './store';
|
15
|
-
|
16
|
-
const decrypt = (res: Encrypted, encryptionKey: EncryptionKey): unknown => {
|
17
|
-
if (res.iv && res.tag) {
|
18
|
-
return JSON.parse(
|
19
|
-
encrypter.decrypt(res.value, res.iv, res.tag, encryptionKey)
|
20
|
-
);
|
21
|
-
}
|
22
|
-
|
23
|
-
return JSON.parse(res.value);
|
24
|
-
};
|
25
|
-
|
26
|
-
class DB implements DatabaseDriver {
|
27
|
-
private db: DatabaseDriver;
|
28
|
-
private encryptionKey: EncryptionKey;
|
29
|
-
|
30
|
-
constructor(db: DatabaseDriver, encryptionKey: EncryptionKey) {
|
31
|
-
this.db = db;
|
32
|
-
this.encryptionKey = encryptionKey;
|
33
|
-
}
|
34
|
-
|
35
|
-
async get(namespace: string, key: string): Promise<unknown> {
|
36
|
-
const res = await this.db.get(namespace, key);
|
37
|
-
|
38
|
-
if (!res) {
|
39
|
-
return null;
|
40
|
-
}
|
41
|
-
|
42
|
-
return decrypt(res, this.encryptionKey);
|
43
|
-
}
|
44
|
-
|
45
|
-
async getByIndex(namespace: string, idx: Index): Promise<unknown[]> {
|
46
|
-
const res = await this.db.getByIndex(namespace, idx);
|
47
|
-
const encryptionKey = this.encryptionKey;
|
48
|
-
return res.map((r) => {
|
49
|
-
return decrypt(r, encryptionKey);
|
50
|
-
});
|
51
|
-
}
|
52
|
-
|
53
|
-
// ttl is in seconds
|
54
|
-
async put(
|
55
|
-
namespace: string,
|
56
|
-
key: string,
|
57
|
-
val: unknown,
|
58
|
-
ttl = 0,
|
59
|
-
...indexes: Index[]
|
60
|
-
): Promise<unknown> {
|
61
|
-
if (ttl > 0 && indexes && indexes.length > 0) {
|
62
|
-
throw new Error('secondary indexes not allow on a store with ttl');
|
63
|
-
}
|
64
|
-
|
65
|
-
const dbVal = this.encryptionKey
|
66
|
-
? encrypter.encrypt(JSON.stringify(val), this.encryptionKey)
|
67
|
-
: { value: JSON.stringify(val) };
|
68
|
-
|
69
|
-
return await this.db.put(namespace, key, dbVal, ttl, ...indexes);
|
70
|
-
}
|
71
|
-
|
72
|
-
async delete(namespace: string, key: string): Promise<unknown> {
|
73
|
-
return await this.db.delete(namespace, key);
|
74
|
-
}
|
75
|
-
|
76
|
-
store(namespace: string, ttl = 0): Storable {
|
77
|
-
return store.new(namespace, this, ttl);
|
78
|
-
}
|
79
|
-
}
|
80
|
-
|
81
|
-
export = {
|
82
|
-
new: async (options: DatabaseOption) => {
|
83
|
-
const encryptionKey = options.encryptionKey
|
84
|
-
? Buffer.from(options.encryptionKey, 'latin1')
|
85
|
-
: null;
|
86
|
-
|
87
|
-
switch (options.engine) {
|
88
|
-
case 'redis':
|
89
|
-
return new DB(await redis.new(options), encryptionKey);
|
90
|
-
case 'sql':
|
91
|
-
return new DB(await sql.new(options), encryptionKey);
|
92
|
-
case 'mongo':
|
93
|
-
return new DB(await mongo.new(options), encryptionKey);
|
94
|
-
case 'mem':
|
95
|
-
return new DB(await mem.new(options), encryptionKey);
|
96
|
-
default:
|
97
|
-
throw new Error('unsupported db engine: ' + options.engine);
|
98
|
-
}
|
99
|
-
},
|
100
|
-
};
|
package/src/db/encrypter.ts
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
import crypto from 'crypto';
|
2
|
-
import { Encrypted, EncryptionKey } from 'saml-jackson';
|
3
|
-
|
4
|
-
const ALGO = 'aes-256-gcm';
|
5
|
-
const BLOCK_SIZE = 16; // 128 bit
|
6
|
-
|
7
|
-
export const encrypt = (text: string, key: EncryptionKey): Encrypted => {
|
8
|
-
const iv = crypto.randomBytes(BLOCK_SIZE);
|
9
|
-
const cipher = crypto.createCipheriv(ALGO, key, iv);
|
10
|
-
|
11
|
-
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
12
|
-
ciphertext += cipher.final('base64');
|
13
|
-
|
14
|
-
return {
|
15
|
-
iv: iv.toString('base64'),
|
16
|
-
tag: cipher.getAuthTag().toString('base64'),
|
17
|
-
value: ciphertext,
|
18
|
-
};
|
19
|
-
};
|
20
|
-
|
21
|
-
export const decrypt = (
|
22
|
-
ciphertext: string,
|
23
|
-
iv: string,
|
24
|
-
tag: string,
|
25
|
-
key: EncryptionKey
|
26
|
-
): string => {
|
27
|
-
const decipher = crypto.createDecipheriv(
|
28
|
-
ALGO,
|
29
|
-
key,
|
30
|
-
Buffer.from(iv, 'base64')
|
31
|
-
);
|
32
|
-
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
33
|
-
|
34
|
-
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
35
|
-
cleartext += decipher.final('utf8');
|
36
|
-
|
37
|
-
return cleartext;
|
38
|
-
};
|
package/src/db/mem.ts
DELETED
@@ -1,128 +0,0 @@
|
|
1
|
-
// This is an in-memory implementation to be used with testing and prototyping only
|
2
|
-
|
3
|
-
import { DatabaseDriver, DatabaseOption, Index, Encrypted } from 'saml-jackson';
|
4
|
-
import * as dbutils from './utils';
|
5
|
-
|
6
|
-
class Mem implements DatabaseDriver {
|
7
|
-
private options: DatabaseOption;
|
8
|
-
private store: any;
|
9
|
-
private indexes: any;
|
10
|
-
private cleanup: any;
|
11
|
-
private ttlStore: any;
|
12
|
-
private ttlCleanup: any;
|
13
|
-
private timerId: any;
|
14
|
-
|
15
|
-
constructor(options: DatabaseOption) {
|
16
|
-
this.options = options;
|
17
|
-
}
|
18
|
-
|
19
|
-
async init(): Promise<Mem> {
|
20
|
-
this.store = {}; // map of key, value
|
21
|
-
this.indexes = {}; // map of key, Set
|
22
|
-
this.cleanup = {}; // map of indexes for cleanup when store key is deleted
|
23
|
-
this.ttlStore = {}; // map of key to ttl
|
24
|
-
|
25
|
-
if (this.options.ttl) {
|
26
|
-
this.ttlCleanup = async () => {
|
27
|
-
const now = Date.now();
|
28
|
-
for (const k in this.ttlStore) {
|
29
|
-
if (this.ttlStore[k].expiresAt < now) {
|
30
|
-
await this.delete(this.ttlStore[k].namespace, this.ttlStore[k].key);
|
31
|
-
}
|
32
|
-
}
|
33
|
-
|
34
|
-
if (this.options.ttl) {
|
35
|
-
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
|
36
|
-
}
|
37
|
-
};
|
38
|
-
|
39
|
-
this.timerId = setTimeout(this.ttlCleanup, this.options.ttl * 1000);
|
40
|
-
}
|
41
|
-
|
42
|
-
return this;
|
43
|
-
}
|
44
|
-
|
45
|
-
async get(namespace: string, key: string): Promise<any> {
|
46
|
-
const res = this.store[dbutils.key(namespace, key)];
|
47
|
-
if (res) {
|
48
|
-
return res;
|
49
|
-
}
|
50
|
-
|
51
|
-
return null;
|
52
|
-
}
|
53
|
-
|
54
|
-
async getByIndex(namespace: string, idx: Index): Promise<any> {
|
55
|
-
const dbKeys = await this.indexes[dbutils.keyForIndex(namespace, idx)];
|
56
|
-
|
57
|
-
const ret: string[] = [];
|
58
|
-
for (const dbKey of dbKeys || []) {
|
59
|
-
ret.push(await this.get(namespace, dbKey));
|
60
|
-
}
|
61
|
-
|
62
|
-
return ret;
|
63
|
-
}
|
64
|
-
|
65
|
-
async put(
|
66
|
-
namespace: string,
|
67
|
-
key: string,
|
68
|
-
val: Encrypted,
|
69
|
-
ttl = 0,
|
70
|
-
...indexes: any[]
|
71
|
-
): Promise<any> {
|
72
|
-
const k = dbutils.key(namespace, key);
|
73
|
-
|
74
|
-
this.store[k] = val;
|
75
|
-
|
76
|
-
if (ttl) {
|
77
|
-
this.ttlStore[k] = {
|
78
|
-
namespace,
|
79
|
-
key,
|
80
|
-
expiresAt: Date.now() + ttl * 1000,
|
81
|
-
};
|
82
|
-
}
|
83
|
-
|
84
|
-
// no ttl support for secondary indexes
|
85
|
-
for (const idx of indexes || []) {
|
86
|
-
const idxKey = dbutils.keyForIndex(namespace, idx);
|
87
|
-
let set = this.indexes[idxKey];
|
88
|
-
if (!set) {
|
89
|
-
set = new Set();
|
90
|
-
this.indexes[idxKey] = set;
|
91
|
-
}
|
92
|
-
|
93
|
-
set.add(key);
|
94
|
-
|
95
|
-
const cleanupKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
|
96
|
-
let cleanup = this.cleanup[cleanupKey];
|
97
|
-
if (!cleanup) {
|
98
|
-
cleanup = new Set();
|
99
|
-
this.cleanup[cleanupKey] = cleanup;
|
100
|
-
}
|
101
|
-
|
102
|
-
cleanup.add(idxKey);
|
103
|
-
}
|
104
|
-
}
|
105
|
-
|
106
|
-
async delete(namespace: string, key: string): Promise<any> {
|
107
|
-
const k = dbutils.key(namespace, key);
|
108
|
-
|
109
|
-
delete this.store[k];
|
110
|
-
|
111
|
-
const idxKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
|
112
|
-
// delete secondary indexes and then the mapping of the seconary indexes
|
113
|
-
const dbKeys = this.cleanup[idxKey];
|
114
|
-
|
115
|
-
for (const dbKey of dbKeys || []) {
|
116
|
-
this.indexes[dbKey] && this.indexes[dbKey].delete(key);
|
117
|
-
}
|
118
|
-
|
119
|
-
delete this.cleanup[idxKey];
|
120
|
-
delete this.ttlStore[k];
|
121
|
-
}
|
122
|
-
}
|
123
|
-
|
124
|
-
export default {
|
125
|
-
new: async (options: DatabaseOption) => {
|
126
|
-
return await new Mem(options).init();
|
127
|
-
},
|
128
|
-
};
|