UNPKG

20 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const Rsa_1 = require("./Crypto/Rsa");
4const Aes_1 = require("./Crypto/Aes");
5const Pbkdf2_1 = require("./Crypto/Pbkdf2");
6exports.ALLOWED_ENVIROMENTS = ["SANDBOX", "PRODUCTION"];
7exports.URL_ENVIROMENTS = {
8 SANDBOX: "https://public-api.sandbox.bunq.com",
9 PRODUCTION: "https://api.bunq.com"
10};
11class Session {
12 constructor(storageInterface, loggerInterface) {
13 this.apiKey = null;
14 this.apiKeyIdentifier = null;
15 this.encryptionKey = false;
16 this.allowedIps = [];
17 this.isOAuthKey = false;
18 // rsa key storage
19 this.publicKey = null;
20 this.publicKeyPem = null;
21 this.privateKey = null;
22 this.privateKeyPem = null;
23 this.serverPublicKey = null;
24 this.serverPublicKeyPem = null;
25 // installation info
26 this.installCreated = null;
27 this.installUpdated = null;
28 this.installToken = null;
29 this.deviceId = null;
30 // session info
31 this.sessionToken = null;
32 this.sessionTokenId = null;
33 this.sessionId = null;
34 this.sessionExpiryTime = null;
35 this.sessionTimeout = 0;
36 this.sessionExpiryTimeChecker = null;
37 this.userInfo = {};
38 this.storageInterface = storageInterface;
39 this.logger = loggerInterface;
40 this.environmentType = "SANDBOX";
41 }
42 /**
43 * Checks default values and looks in storage interface
44 * @param {{forceNewKeypair: boolean}} options
45 * @returns {Promise<void>}
46 */
47 async setup(apiKey, allowedIps = [], environment = "SANDBOX", encryptionKey = false) {
48 if (this.apiKey && this.apiKey !== apiKey) {
49 this.logger.debug("current apiKey set and changed");
50 }
51 if (this.environment !== null && environment !== this.environment && !!apiKey) {
52 // we can't keep the session data if the environment changes
53 await this.destroyInstallationMemory();
54 }
55 // create a unique identifier for this api key
56 if (typeof apiKey === "string") {
57 const derivedApiKey = await Pbkdf2_1.derivePasswordKey(apiKey.substring(0, 8), apiKey.substring(8, 16), 10000);
58 // if already set and changed, we reset data stored in memory
59 if (this.apiKeyIdentifier && this.apiKeyIdentifier !== derivedApiKey.key) {
60 await this.destroyInstallationMemory();
61 }
62 this.apiKeyIdentifier = derivedApiKey.key;
63 }
64 this.apiKey = apiKey;
65 this.allowedIps = allowedIps;
66 this.environmentType = environment;
67 // nothing to do if we don't have an encryption key
68 if (!encryptionKey) {
69 return false;
70 }
71 // validate the key
72 if (!Aes_1.validateKey(encryptionKey)) {
73 throw new Error("Invalid EAS key given! Invalid characters or length (16,24,32 length)");
74 }
75 this.encryptionKey = encryptionKey;
76 // check if storage interface has a session stored
77 const loadedStorage = await this.loadSession();
78 // if there is no stored session but we have an key we setup a new keypair
79 if (loadedStorage === false && this.encryptionKey !== false) {
80 // setup the required rsa keypair
81 await this.setupKeypair();
82 }
83 return true;
84 }
85 /**
86 * Updates the encryption key and stores data using that new key
87 * @param {string} encryptionKey
88 * @returns {Promise<boolean>}
89 */
90 async setEncryptionKey(encryptionKey) {
91 // validate the key
92 if (!Aes_1.validateKey(encryptionKey)) {
93 throw new Error("Invalid EAS key given! Invalid characters or length (16,24,32 length)");
94 }
95 this.encryptionKey = encryptionKey;
96 // overwrite the session data with the new encryption key
97 await this.storeSession();
98 return true;
99 }
100 /**
101 * Setup the keypair and generate a new one when required
102 * @param {boolean} forceNewKeypair
103 * @param {boolean} ignoreCI - if true the hardcoded certs won't be used even if process.env.CI is set
104 * @returns {Promise<boolean>}
105 */
106 async setupKeypair(forceNewKeypair = false, bitSize = 2048, ignoreCI = false) {
107 if (forceNewKeypair === false && this.publicKey !== null && this.privateKey !== null) {
108 return true;
109 }
110 // check if we are in a CI environment
111 if (typeof process !== "undefined" && process.env.ENV_CI === "true" && ignoreCI === false) {
112 // use the stored CI variables instead of creating a new on
113 this.publicKeyPem = process.env.CI_PUBLIC_KEY_PEM;
114 this.privateKeyPem = process.env.CI_PRIVATE_KEY_PEM;
115 this.publicKey = await Rsa_1.publicKeyFromPem(this.publicKeyPem);
116 this.privateKey = await Rsa_1.privateKeyFromPem(this.privateKeyPem);
117 }
118 else {
119 // generate a new keypair and format as pem
120 const keyPair = await Rsa_1.createKeyPair(bitSize);
121 const { publicKey, privateKey } = await Rsa_1.keyPairToPem(keyPair);
122 this.publicKey = keyPair.publicKey;
123 this.privateKey = keyPair.privateKey;
124 this.publicKeyPem = publicKey;
125 this.privateKeyPem = privateKey;
126 }
127 return true;
128 }
129 /**
130 * Checks if a session is stored and verifies/loads it into this instance
131 * @returns {Promise.<boolean>}
132 */
133 async loadSession() {
134 this.logger.debug(" === Loading session data === " + this.storageKeyLocation);
135 // try to load the session interface
136 const encryptedSession = await this.asyncStorageGet(this.storageKeyLocation);
137 // no session found stored
138 if (encryptedSession === undefined || encryptedSession === null) {
139 this.logger.debug("No stored session found");
140 return false;
141 }
142 let session;
143 try {
144 // decrypt the stored sesion
145 session = await this.decryptSession(encryptedSession);
146 }
147 catch (error) {
148 this.logger.debug("Failed to decrypt session");
149 this.logger.debug(error);
150 // failed to decrypt the session, return false
151 return false;
152 }
153 // api keys dont match, this session is outdated
154 if (this.apiKey !== false && this.apiKey !== null && session.apiKey !== this.apiKey) {
155 this.logger.debug("Api key changed or is different (api key could be empty)");
156 return false;
157 }
158 // different environment stored, destroy old session
159 if (session.environment !== this.environment) {
160 this.logger.debug("Environment changed, delete existing session");
161 await this.destroySession();
162 return false;
163 }
164 this.environment = session.environment;
165 // overwrite our current properties with the stored version
166 this.publicKeyPem = session.publicKeyPem;
167 this.privateKeyPem = session.privateKeyPem;
168 this.serverPublicKeyPem = session.serverPublicKeyPem;
169 if (this.privateKeyPem !== null) {
170 this.privateKey = await Rsa_1.privateKeyFromPem(session.privateKeyPem);
171 }
172 if (this.publicKeyPem !== null) {
173 this.publicKey = await Rsa_1.publicKeyFromPem(session.publicKeyPem);
174 }
175 if (this.serverPublicKeyPem !== null) {
176 this.serverPublicKey = await Rsa_1.publicKeyFromPem(session.serverPublicKeyPem);
177 }
178 this.installToken = session.installToken;
179 this.installCreated = session.installCreated;
180 this.installUpdated = session.installUpdated;
181 this.sessionId = session.sessionId;
182 this.sessionToken = session.sessionToken;
183 this.sessionTimeout = session.sessionTimeout;
184 this.sessionExpiryTime = new Date(session.sessionExpiryTime);
185 this.deviceId = session.deviceId;
186 this.userInfo = session.userInfo;
187 this.logger.debug(`sessionId: ${session.sessionId}`);
188 this.logger.debug(`installCreated: ${session.installCreated}`);
189 this.logger.debug(`installUpdated: ${session.installUpdated}`);
190 this.logger.debug(`sessionExpiryTime: ${session.sessionExpiryTime}`);
191 this.logger.debug(`deviceId: ${session.deviceId}`);
192 // if we have a stored installation but no session we reset to prevent
193 // creating two sessions for a single installation
194 if (this.verifyInstallation() && this.verifyDeviceInstallation() && !this.verifySessionInstallation()) {
195 await this.destroyApiSession(true);
196 return false;
197 }
198 try {
199 this.logger.debug(`sessionToken: ${session.sessionToken === null ? null : session.sessionToken.substring(0, 5)}`);
200 this.logger.debug(`installToken: ${session.installToken === null ? null : session.installToken.substring(0, 5)}`);
201 }
202 catch (error) { }
203 return true;
204 }
205 /**
206 * Stores this session using the storageInterface
207 * @returns {Promise.<void>}
208 */
209 async storeSession() {
210 const dataToStore = {
211 environment: this.environment,
212 apiKey: this.apiKey,
213 publicKeyPem: this.publicKeyPem,
214 privateKeyPem: this.privateKeyPem,
215 serverPublicKeyPem: this.serverPublicKeyPem,
216 installUpdated: this.installUpdated,
217 installCreated: this.installCreated,
218 installToken: this.installToken,
219 sessionId: this.sessionId,
220 sessionToken: this.sessionToken,
221 sessionExpiryTime: this.sessionExpiryTime,
222 sessionTimeout: this.sessionTimeout,
223 userInfo: this.userInfo,
224 deviceId: this.deviceId
225 };
226 const serializedData = JSON.stringify(dataToStore);
227 // encrypt the data with our encryption key
228 return await this.encryptSession(serializedData);
229 }
230 /**
231 * Resets all values to default and remove data from storage
232 * @returns {Promise<void>}
233 */
234 async destroySession() {
235 this.logger.debug(` -> #destroySession() `);
236 this.apiKey = null;
237 this.userInfo = {};
238 await this.destroyApiSession();
239 await this.destroyApiInstallation();
240 await this.destroyApiDeviceInstallation();
241 return await this.asyncStorageRemove(this.storageKeyLocation);
242 }
243 /**
244 * Removes info from the object, keeps stored data in
245 * @param {boolean} save
246 * @returns {Promise<boolean>}
247 */
248 async destroyInstallationMemory() {
249 this.logger.debug(` -> #destroyInstallationMemory() `);
250 this.userInfo = {};
251 this.sessionId = null;
252 this.sessionToken = null;
253 this.sessionTokenId = null;
254 this.sessionTimeout = null;
255 this.sessionExpiryTime = null;
256 this.publicKey = null;
257 this.publicKeyPem = null;
258 this.privateKey = null;
259 this.privateKeyPem = null;
260 this.serverPublicKey = null;
261 this.serverPublicKeyPem = null;
262 this.installUpdated = null;
263 this.installCreated = null;
264 this.installToken = null;
265 this.deviceId = null;
266 }
267 /**
268 * Destroys only the data associated with the api session
269 * @param {boolean} save
270 * @returns {Promise<undefined>}
271 */
272 async destroyApiSession(save = false) {
273 this.logger.debug(` -> #destroyApiSession(${save}) `);
274 this.userInfo = {};
275 this.sessionId = null;
276 this.sessionToken = null;
277 this.sessionTokenId = null;
278 this.sessionTimeout = null;
279 this.sessionExpiryTime = null;
280 if (save)
281 return await this.storeSession();
282 }
283 /**
284 * Destroys only the data associated with the installation
285 * @param {boolean} save
286 * @returns {Promise<undefined>}
287 */
288 async destroyApiInstallation(save = false) {
289 this.logger.debug(` -> #destroyApiInstallation(${save}) `);
290 this.publicKey = null;
291 this.publicKeyPem = null;
292 this.privateKey = null;
293 this.privateKeyPem = null;
294 this.serverPublicKey = null;
295 this.serverPublicKeyPem = null;
296 this.installUpdated = null;
297 this.installCreated = null;
298 this.installToken = null;
299 if (save)
300 return await this.storeSession();
301 }
302 /**
303 * Destroys only the data associated with the device installation
304 * @param {boolean} save
305 * @returns {Promise<undefined>}
306 */
307 async destroyApiDeviceInstallation(save = false) {
308 this.logger.debug(` -> #destroyApiDeviceInstallation(${save}) `);
309 this.deviceId = null;
310 if (save)
311 return await this.storeSession();
312 }
313 /**
314 * Attempt to decrypt the session data with our stored IV and encryption key
315 * @param encryptedSession
316 * @returns {Promise<any>}
317 */
318 async decryptSession(encryptedSession) {
319 const IV = await this.asyncStorageGet(this.storageIvLocation);
320 if (this.encryptionKey === false) {
321 throw new Error("No encryption key is set, failed to decrypt session");
322 }
323 // attempt to decrypt the string
324 const decryptedSession = await Aes_1.decryptString(encryptedSession, this.encryptionKey, IV);
325 return JSON.parse(decryptedSession);
326 }
327 /**
328 * Attempt to encrypt the session data with encryption key
329 * @param sessionData
330 * @returns {Promise<boolean>}
331 */
332 async encryptSession(sessionData) {
333 if (!this.encryptionKey)
334 return false;
335 // attempt to decrypt the string
336 const encryptedData = await Aes_1.encryptString(sessionData, this.encryptionKey);
337 // store the new IV and encrypted data
338 const ivStorageSuccess = this.asyncStorageSet(this.storageIvLocation, encryptedData.iv);
339 const dataStorageSuccess = this.asyncStorageSet(this.storageKeyLocation, encryptedData.encryptedString);
340 // await here so we do the storage calls asyncronously
341 await ivStorageSuccess;
342 await dataStorageSuccess;
343 return true;
344 }
345 /**
346 * @param data
347 * @param {string} data_location
348 * @param {string} iv_location
349 * @returns {Promise<boolean>}
350 */
351 async storeEncryptedData(data, location) {
352 if (!this.encryptionKey)
353 return false;
354 // attempt to decrypt the string
355 const encryptedData = await Aes_1.encryptString(JSON.stringify(data), this.encryptionKey);
356 // store the new IV and encrypted data
357 const ivStorage = this.asyncStorageSet(`${location}_IV`, encryptedData.iv);
358 const dataStorage = this.asyncStorageSet(location, encryptedData.encryptedString);
359 await ivStorage;
360 await dataStorage;
361 return true;
362 }
363 /**
364 * @param {string} data_location
365 * @param {string} iv_location
366 * @returns {Promise<any>}
367 */
368 async loadEncryptedData(data_location, iv_location = null) {
369 // set default value for IV location in case none is given
370 iv_location = iv_location === null ? `${data_location}_IV` : iv_location;
371 // load the data from storage
372 const storedData = await this.asyncStorageGet(data_location);
373 const storedIv = await this.asyncStorageGet(iv_location);
374 // check if both values are found
375 if (storedData === undefined || storedData === null || storedIv === undefined || storedIv === null) {
376 return false;
377 }
378 if (this.encryptionKey === false) {
379 throw new Error("No encryption key is set, failed to decrypt data");
380 }
381 // attempt to decrypt the data
382 const decryptedSession = await Aes_1.decryptString(storedData, this.encryptionKey, storedIv);
383 return JSON.parse(decryptedSession);
384 }
385 /**
386 * Wrapper around the storage interface for remove calls
387 * @param key
388 * @param {boolean} silent
389 * @returns {Promise<any>}
390 */
391 async asyncStorageRemove(key, silent = false) {
392 try {
393 return await this.storageInterface.remove(key);
394 }
395 catch (error) {
396 if (silent) {
397 return undefined;
398 }
399 throw error;
400 }
401 }
402 /**
403 * Wrapper around the storage interface for get calls
404 * @param key
405 * @param {boolean} silent
406 * @returns {Promise<any>}
407 */
408 async asyncStorageGet(key, silent = false) {
409 try {
410 return await this.storageInterface.get(key);
411 }
412 catch (error) {
413 if (silent) {
414 return undefined;
415 }
416 throw error;
417 }
418 }
419 /**
420 * Wrapper around the storage interface for set calls
421 * @param key
422 * @param value
423 * @param {boolean} silent
424 * @returns {Promise<any>}
425 */
426 async asyncStorageSet(key, value, silent = false) {
427 try {
428 return await this.storageInterface.set(key, value);
429 }
430 catch (error) {
431 if (silent) {
432 return undefined;
433 }
434 throw error;
435 }
436 }
437 /**
438 * Checks if this session has a succesful installation stored
439 * @returns {boolean}
440 */
441 verifyInstallation() {
442 this.logger.debug(" === Testing installation === ");
443 const installationValid = this.serverPublicKey !== null && this.installToken !== null;
444 this.logger.debug("Installation valid: " + installationValid);
445 this.logger.debug("this.serverPublicKey = " + this.serverPublicKey);
446 this.logger.debug(`this.installToken = ${this.installToken === null ? null : this.installToken.substring(0, 5)}`);
447 return installationValid;
448 }
449 /**
450 * Checks if this session has a succesful device installation stored
451 * @returns {boolean}
452 */
453 verifyDeviceInstallation() {
454 this.logger.debug(" === Testing device installation === ");
455 const deviceValid = this.deviceId !== null;
456 this.logger.debug("Device valid: " + deviceValid);
457 this.logger.debug("this.deviceId: " + this.deviceId);
458 return deviceValid;
459 }
460 /**
461 * Checks if this session has a succesful session setup
462 * @returns {boolean}
463 */
464 verifySessionInstallation() {
465 if (this.sessionId === null) {
466 this.logger.debug(" === Session invalid: sessionId null === ");
467 return false;
468 }
469 if (!this.verifySessionExpiry())
470 return false;
471 return true;
472 }
473 /**
474 * Checks if session has expired yet
475 * @returns {boolean}
476 */
477 verifySessionExpiry() {
478 const currentTime = new Date();
479 if (this.sessionExpiryTime.getTime() <= currentTime.getTime()) {
480 this.logger.debug(" === Session invalid: expired === ");
481 this.logger.debug("this.sessionExpiryTime.getTime() = " + this.sessionExpiryTime.getTime());
482 this.logger.debug("currentTime.getTime() = " + currentTime.getTime());
483 return false;
484 }
485 return true;
486 }
487 /**
488 * Set enviroment and check if type is allowed/valid
489 * @param environmentType
490 */
491 set environmentType(environmentType) {
492 if (exports.ALLOWED_ENVIROMENTS.includes(environmentType)) {
493 this.environment = environmentType;
494 this.environmentUrl = exports.URL_ENVIROMENTS[this.environment];
495 // set the storage location for the environment
496 this.storageKeyLocation = `BUNQJSCLIENT_${this.environment}_SESSION_${this.apiKeyIdentifier}`;
497 this.storageIvLocation = `BUNQJSCLIENT_${this.environment}_IV_${this.apiKeyIdentifier}`;
498 return;
499 }
500 throw new Error("Invalid enviroment given. " + JSON.stringify(exports.ALLOWED_ENVIROMENTS));
501 }
502}
503exports.default = Session;