+ this.mysteryBox = new MysteryBox(options);
+ this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name)));
+
+ this.cookieLifespan = options.authenticator.inactiveSessionLifespanSeconds || 60 * 60 * 24 * 32;
+
+ this.TOTP = TOTP; // For ease of test stubbing.
+ }
+
+
+ /**
+ * Populate the authentication database with a new identifier, the
+ * secured credential, and optionally an OTP key.
+ * @param {*} dbCtx
+ * @param {String} identifier
+ * @param {String} credential plaintext
+ * @param {String=} otpKey
+ * @returns {Promise<void>}
+ */
+ async createIdentifier(dbCtx, identifier, credential, otpKey = null) {
+ const _scope = _fileScope('createIdentifier');
+ try {
+ const secureCredential = await this._secureCredential(credential);
+ await this.db.authenticationUpsert(dbCtx, identifier, secureCredential, otpKey);
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, identifier });
+ throw e;
+ }
+ }
+
+
+ /**
+ * Update the authentication database with a new secured credential
+ * for an indentifier.
+ * @param {*} dbCtx
+ * @param {*} identifier
+ * @param {*} credential plaintext
+ * @returns {Promise<void>}
+ */
+ async updateCredential(dbCtx, identifier, credential) {
+ const _scope = _fileScope('updateCredential');
+ try {
+ const secureCredential = await this._secureCredential(credential);
+ await this.db.authenticationUpdateCredential(dbCtx, identifier, secureCredential);
+ this.logger.info(_scope, 'success', { identifier });
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, identifier });
+ throw e;
+ }
+ }
+
+
+ /**
+ * Encode a plaintext credential in the preferred way to store in database.
+ * @param {String} credential
+ * @param {String=} authn
+ * @returns {Promise<String>}
+ */
+ async _secureCredential(credential, authn = this.authnPreferred) {
+ const _scope = _fileScope('_secureCredential');
+ try {
+ switch (authn) {
+ case 'plain':
+ return '$plain$' + credential;
+
+ case 'argon2':
+ return await this.authn.argon2.hash(credential, { type: this.authn.argon2.argon2id });
+
+ default:
+ throw new RangeError('Unknown authn');
+ }
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, authn });
+ throw e;
+ }
+ }
+
+
+ /**
+ * Checks a supplied credential against supplied data.
+ * @param {AuthInfo} authData from database
+ * @param {String} credential plaintext
+ * @returns {Promise<Boolean>}
+ */
+ async _validateAuthDataCredential(authData, credential) {
+ const _scope = _fileScope('_validateAuthDataCredential');
+
+ if (!authData?.credential) {
+ this.logger.debug(_scope, 'failed, no authInfo');
+ return false;
+ }
+ if (authData.credential.startsWith('$argon2')
+ && this.authnEnabled.has('argon2')) {
+ return await this._isValidArgon2Identifier(authData, credential);
+ }
+ if (authData.credential.startsWith('$PAM$')
+ && this.authnEnabled.has('pam')) {
+ return await this._isValidPAMIdentifier(authData, credential);
+ }
+ if (authData.credential.startsWith('$plain$')
+ && this.authnEnabled.has('plain')) {
+ return this.constructor._isValidPlainIdentifier(authData, credential);
+ }
+ this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { authData });
+ return false;
+ }
+
+
+ /**
+ * Check argon2.
+ * @param {AuthInfo} authData
+ * @param {String} credential
+ * @returns {Promise<Boolean>}
+ */
+ async _isValidArgon2Identifier(authData, credential) {
+ return await this.authn.argon2.verify(authData.credential, credential);
+ }
+
+
+ /**
+ * Check plaintext.
+ * @param {AuthInfo} authData
+ * @param {String} credential
+ * @returns {Promise<Boolean>}
+ */
+ static _isValidPlainIdentifier(authData, credential) {
+ return authData.credential.substring('$plain$'.length) === credential;
+ }
+
+
+ /**
+ * Check system PAM.
+ * @param {AuthInfo} authData
+ * @param {String} credential
+ * @returns {Promise<Boolean>}
+ */
+ async _isValidPAMIdentifier(authData, credential) {
+ const _scope = _fileScope('_isValidPAMIdentifier');
+ const { identifier } = authData;
+ let isValid = false;
+ if (this.forbiddenPAMIdentifiers.includes(identifier)) {
+ return false;
+ }
+ try {
+ await this.authn.pam.pamAuthenticatePromise({ username: identifier, password: credential });
+ isValid = true;
+ } catch (e) {
+ this.logger.debug(_scope, 'failed', { error: e });
+ if (!(e instanceof this.authn.pam.PamError)) {
+ throw e;
+ }
+ }
+ return isValid;