1
0
mirror of https://github.com/bitwarden/jslib synced 2026-01-09 12:03:23 +00:00

Merge branch 'master' of https://github.com/bitwarden/jslib into improve-hostname-and-domain-retrieval

This commit is contained in:
Daniel James Smith
2022-03-11 12:19:39 +01:00
492 changed files with 83679 additions and 5149 deletions

View File

@@ -15,9 +15,7 @@
"scripts": {
"clean": "rimraf dist/**/*",
"build": "npm run clean && tsc",
"build:watch": "npm run clean && tsc -watch",
"lint": "tslint 'src/**/*.ts' 'spec/**/*.ts'",
"lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix"
"build:watch": "npm run clean && tsc -watch"
},
"devDependencies": {
"@types/inquirer": "^7.3.1",

View File

@@ -9,7 +9,7 @@ import { StringResponse } from "./models/response/stringResponse";
export abstract class BaseProgram {
constructor(
private stateService: StateService,
protected stateService: StateService,
private writeLn: (s: string, finalLine: boolean, error: boolean) => void
) {}
@@ -79,7 +79,7 @@ export abstract class BaseProgram {
return message.raw;
}
let out: string = "";
let out = "";
if (message.title != null) {
if (message.noColor) {
out = message.title;

View File

@@ -1,12 +1,8 @@
import * as program from "commander";
import * as http from "http";
import * as program from "commander";
import * as inquirer from "inquirer";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { AuthResult } from "jslib-common/models/domain/authResult";
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
import Separator from "inquirer/lib/objects/separator";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
@@ -14,25 +10,27 @@ import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { Response } from "../models/response";
import { KeyConnectorUserKeyRequest } from "jslib-common/models/request/keyConnectorUserKeyRequest";
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
import { MessageResponse } from "../models/response/messageResponse";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { Utils } from "jslib-common/misc/utils";
import { AuthResult } from "jslib-common/models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "jslib-common/models/domain/logInCredentials";
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequest";
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
// tslint:disable-next-line
const open = require("open");
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/messageResponse";
export class LoginCommand {
protected validatedParams: () => Promise<any>;
@@ -56,9 +54,8 @@ export class LoginCommand {
protected stateService: StateService,
protected cryptoService: CryptoService,
protected policyService: PolicyService,
clientId: string,
private syncService: SyncService,
protected keyConnectorService: KeyConnectorService
protected twoFactorService: TwoFactorService,
clientId: string
) {
this.clientId = clientId;
}
@@ -73,6 +70,8 @@ export class LoginCommand {
let clientId: string = null;
let clientSecret: string = null;
let selectedProvider: any = null;
if (options.apikey != null) {
const apiIdentifiers = await this.apiIdentifiers();
clientId = apiIdentifiers.clientId;
@@ -148,163 +147,143 @@ export class LoginCommand {
return Response.error("Invalid two-step login method.");
}
const twoFactor =
twoFactorToken == null
? null
: {
provider: twoFactorMethod,
token: twoFactorToken,
remember: false,
};
try {
if (this.validatedParams != null) {
await this.validatedParams();
}
let response: AuthResult = null;
if (twoFactorToken != null && twoFactorMethod != null) {
if (clientId != null && clientSecret != null) {
response = await this.authService.logInApiKeyComplete(
clientId,
clientSecret,
twoFactorMethod,
twoFactorToken,
false
);
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logInSsoComplete(
if (clientId != null && clientSecret != null) {
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logIn(
new SsoLogInCredentials(
ssoCode,
ssoCodeVerifier,
this.ssoRedirectUri,
twoFactorMethod,
twoFactorToken,
false
);
} else {
response = await this.authService.logInComplete(
email,
password,
twoFactorMethod,
twoFactorToken,
false,
this.clientSecret
);
}
orgIdentifier,
twoFactor
)
);
} else {
if (clientId != null && clientSecret != null) {
response = await this.authService.logInApiKey(clientId, clientSecret);
} else if (ssoCode != null && ssoCodeVerifier != null) {
response = await this.authService.logInSso(
ssoCode,
ssoCodeVerifier,
this.ssoRedirectUri,
orgIdentifier
);
response = await this.authService.logIn(
new PasswordLogInCredentials(email, password, null, twoFactor)
);
}
if (response.captchaSiteKey) {
const credentials = new PasswordLogInCredentials(email, password);
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
// Error Response
if (handledResponse instanceof Response) {
return handledResponse;
} else {
response = await this.authService.logIn(email, password);
response = handledResponse;
}
}
if (response.requiresTwoFactor) {
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
if (twoFactorProviders.length === 0) {
return Response.badRequest("No providers available for this client.");
}
if (response.captchaSiteKey) {
const badCaptcha = Response.badRequest(
"Your authentication request appears to be coming from a bot\n" +
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
"(https://bitwarden.com/help/article/cli-auth-challenges)"
);
if (twoFactorMethod != null) {
try {
const captchaClientSecret = await this.apiClientSecret(true);
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
return badCaptcha;
}
const secondResponse = await this.authService.logInComplete(
email,
password,
twoFactorMethod,
twoFactorToken,
false,
captchaClientSecret
);
response = secondResponse;
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
} catch (e) {
if (
(e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
(e as ErrorResponse).message.includes("Captcha is invalid")
) {
return badCaptcha;
} else {
throw e;
}
return Response.error("Invalid two-step login method.");
}
}
if (response.twoFactor) {
let selectedProvider: any = null;
const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null);
if (twoFactorProviders.length === 0) {
return Response.badRequest("No providers available for this client.");
}
if (twoFactorMethod != null) {
try {
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
} catch (e) {
return Response.error("Invalid two-step login method.");
if (selectedProvider == null) {
if (twoFactorProviders.length === 1) {
selectedProvider = twoFactorProviders[0];
} else if (this.canInteract) {
const twoFactorOptions: (string | Separator)[] = twoFactorProviders.map((p) => p.name);
twoFactorOptions.push(new inquirer.Separator());
twoFactorOptions.push("Cancel");
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "list",
name: "method",
message: "Two-step login method:",
choices: twoFactorOptions,
});
const i = twoFactorOptions.indexOf(answer.method);
if (i === twoFactorOptions.length - 1) {
return Response.error("Login failed.");
}
selectedProvider = twoFactorProviders[i];
}
if (selectedProvider == null) {
if (twoFactorProviders.length === 1) {
selectedProvider = twoFactorProviders[0];
} else if (this.canInteract) {
const twoFactorOptions = twoFactorProviders.map((p) => p.name);
twoFactorOptions.push(new inquirer.Separator());
twoFactorOptions.push("Cancel");
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "list",
name: "method",
message: "Two-step login method:",
choices: twoFactorOptions,
});
const i = twoFactorOptions.indexOf(answer.method);
if (i === twoFactorOptions.length - 1) {
return Response.error("Login failed.");
}
selectedProvider = twoFactorProviders[i];
}
if (selectedProvider == null) {
return Response.error("Login failed. No provider selected.");
}
return Response.error("Login failed. No provider selected.");
}
}
if (
twoFactorToken == null &&
response.twoFactorProviders.size > 1 &&
selectedProvider.type === TwoFactorProviderType.Email
) {
const emailReq = new TwoFactorEmailRequest();
emailReq.email = this.authService.email;
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
await this.apiService.postTwoFactorEmail(emailReq);
if (
twoFactorToken == null &&
response.twoFactorProviders.size > 1 &&
selectedProvider.type === TwoFactorProviderType.Email
) {
const emailReq = new TwoFactorEmailRequest();
emailReq.email = this.authService.email;
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
await this.apiService.postTwoFactorEmail(emailReq);
}
if (twoFactorToken == null) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "token",
message: "Two-step login code:",
});
twoFactorToken = answer.token;
}
if (twoFactorToken == null) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "token",
message: "Two-step login code:",
});
twoFactorToken = answer.token;
}
if (twoFactorToken == null || twoFactorToken === "") {
return Response.badRequest("Code is required.");
}
if (twoFactorToken == null || twoFactorToken === "") {
return Response.badRequest("Code is required.");
}
}
response = await this.authService.logInTwoFactor(
selectedProvider.type,
twoFactorToken,
false
);
response = await this.authService.logInTwoFactor(
{
provider: selectedProvider.type,
token: twoFactorToken,
remember: false,
},
null
);
}
if (response.captchaSiteKey) {
const twoFactorRequest: TokenRequestTwoFactor = {
provider: selectedProvider.type,
token: twoFactorToken,
remember: false,
};
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
// Error Response
if (handledResponse instanceof Response) {
return handledResponse;
} else {
response = handledResponse;
}
}
if (response.twoFactor) {
if (response.requiresTwoFactor) {
return Response.error("Login failed.");
}
@@ -315,14 +294,6 @@ export class LoginCommand {
);
}
// Full sync required for the reset password and key connector checks
await this.syncService.fullSync(true);
// Handle converting to Key Connector if required
if (await this.keyConnectorService.userNeedsMigration()) {
return await this.migrateToKeyConnector();
}
// Handle Updating Temp Password if NOT using an API Key for authentication
if (response.forcePasswordReset && clientId == null && clientSecret == null) {
return await this.updateTempPassword();
@@ -464,6 +435,48 @@ export class LoginCommand {
}
}
private async handleCaptchaRequired(
twoFactorRequest: TokenRequestTwoFactor,
credentials: PasswordLogInCredentials = null
): Promise<AuthResult | Response> {
const badCaptcha = Response.badRequest(
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
"(https://bitwarden.com/help/cli-auth-challenges)"
);
try {
const captchaClientSecret = await this.apiClientSecret(true);
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
return badCaptcha;
}
let authResultResponse: AuthResult = null;
if (credentials != null) {
credentials.captchaToken = captchaClientSecret;
credentials.twoFactor = twoFactorRequest;
authResultResponse = await this.authService.logIn(credentials);
} else {
authResultResponse = await this.authService.logInTwoFactor(
twoFactorRequest,
captchaClientSecret
);
}
return authResultResponse;
} catch (e) {
if (
e instanceof ErrorResponse ||
(e.constructor.name === "ErrorResponse" &&
(e as ErrorResponse).message.includes("Captcha is invalid"))
) {
return badCaptcha;
} else {
return Response.error(e);
}
}
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf("@");
@@ -479,68 +492,6 @@ export class LoginCommand {
return userInput;
}
private async migrateToKeyConnector() {
// If no interaction available, alert user to use web vault
if (!this.canInteract) {
await this.logout();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(
new MessageResponse(
"An organization you are a member of is using Key Connector. " +
"In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.",
null
)
);
}
const organization = await this.keyConnectorService.getManagingOrganization();
const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "list",
name: "convert",
message:
organization.name +
" is using a self-hosted key server. A master password is no longer required to log in for members of this organization. ",
choices: [
{
name: "Remove master password and log in",
value: "remove",
},
{
name: "Leave organization and log in",
value: "leave",
},
{
name: "Exit",
value: "exit",
},
],
});
if (answer.convert === "remove") {
await this.keyConnectorService.migrateUser();
// Update environment URL - required for api key login
const urls = this.environmentService.getUrls();
urls.keyConnector = organization.keyConnectorUrl;
await this.environmentService.setUrls(urls, true);
return await this.handleSuccessResponse();
} else if (answer.convert === "leave") {
await this.apiService.postLeaveOrganization(organization.id);
await this.syncService.fullSync(true);
return await this.handleSuccessResponse();
} else {
await this.logout();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error("You have been logged out.");
}
}
private async apiClientId(): Promise<string> {
let clientId: string = null;
@@ -565,7 +516,7 @@ export class LoginCommand {
return clientId;
}
private async apiClientSecret(isAdditionalAuthentication: boolean = false): Promise<string> {
private async apiClientSecret(isAdditionalAuthentication = false): Promise<string> {
const additionalAuthenticationMessage = "Additional authentication required.\nAPI key ";
let clientSecret: string = null;

View File

@@ -1,5 +1,3 @@
import * as program from "commander";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";

View File

@@ -1,4 +1,3 @@
import * as program from "commander";
import * as fetch from "node-fetch";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@@ -8,7 +7,7 @@ import { Response } from "../models/response";
import { MessageResponse } from "../models/response/messageResponse";
export class UpdateCommand {
inPkg: boolean = false;
inPkg = false;
constructor(
private platformUtilsService: PlatformUtilsService,

View File

@@ -1,20 +1,20 @@
import * as child_process from "child_process";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ClientType } from "jslib-common/enums/clientType";
import { DeviceType } from "jslib-common/enums/deviceType";
import { ThemeType } from "jslib-common/enums/themeType";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
// tslint:disable-next-line
// eslint-disable-next-line
const open = require("open");
export class CliPlatformUtilsService implements PlatformUtilsService {
identityClientId: string;
clientType: ClientType;
private deviceCache: DeviceType = null;
constructor(identityClientId: string, private packageJson: any) {
this.identityClientId = identityClientId;
constructor(clientType: ClientType, private packageJson: any) {
this.clientType = clientType;
}
getDevice(): DeviceType {
@@ -41,6 +41,10 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return device.replace("desktop", "");
}
getClientType() {
return this.clientType;
}
isFirefox() {
return false;
}

View File

@@ -1,5 +1,4 @@
import { LogLevelType } from "jslib-common/enums/logLevelType";
import { ConsoleLogService as BaseConsoleLogService } from "jslib-common/services/consoleLog.service";
export class ConsoleLogService extends BaseConsoleLogService {
@@ -13,7 +12,7 @@ export class ConsoleLogService extends BaseConsoleLogService {
}
if (process.env.BW_RESPONSE === "true") {
// tslint:disable-next-line
// eslint-disable-next-line
console.error(message);
return;
}

View File

@@ -1,18 +1,20 @@
import * as fs from "fs";
import * as path from "path";
import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";
import * as path from "path";
import { LogService } from "jslib-common/abstractions/log.service";
import { StorageService } from "jslib-common/abstractions/storage.service";
import { NodeUtils } from "jslib-common/misc/nodeUtils";
import { sequentialize } from "jslib-common/misc/sequentialize";
import { Utils } from "jslib-common/misc/utils";
export class LowdbStorageService implements StorageService {
protected dataFilePath: string;
private db: lowdb.LowdbSync<any>;
private defaults: any;
private ready = false;
constructor(
protected logService: LogService,
@@ -23,7 +25,12 @@ export class LowdbStorageService implements StorageService {
this.defaults = defaults;
}
@sequentialize(() => "lowdbStorageInit")
async init() {
if (this.ready) {
return;
}
this.logService.info("Initializing lowdb storage service.");
let adapter: lowdb.AdapterSync<any>;
if (Utils.isNode && this.dir != null) {
@@ -59,7 +66,7 @@ export class LowdbStorageService implements StorageService {
if (fs.existsSync(this.dataFilePath)) {
const backupPath = this.dataFilePath + ".bak";
this.logService.warning(`Writing backup of data file to ${backupPath}`);
await fs.copyFile(this.dataFilePath, backupPath, (err) => {
await fs.copyFile(this.dataFilePath, backupPath, () => {
this.logService.warning(
`Error while creating data file backup, "${e.message}". No backup may have been created.`
);
@@ -81,9 +88,12 @@ export class LowdbStorageService implements StorageService {
this.logService.info("Successfully wrote defaults to db.");
});
}
this.ready = true;
}
get<T>(key: string): Promise<T> {
async get<T>(key: string): Promise<T> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
const val = this.db.get(key).value();
@@ -99,7 +109,8 @@ export class LowdbStorageService implements StorageService {
return this.get(key).then((v) => v != null);
}
save(key: string, obj: any): Promise<any> {
async save(key: string, obj: any): Promise<any> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
this.db.set(key, obj).write();
@@ -108,7 +119,8 @@ export class LowdbStorageService implements StorageService {
});
}
remove(key: string): Promise<any> {
async remove(key: string): Promise<any> {
await this.waitForReady();
return this.lockDbFile(() => {
this.readForNoCache();
this.db.unset(key).write();
@@ -127,4 +139,10 @@ export class LowdbStorageService implements StorageService {
this.db.read();
}
}
private async waitForReady() {
if (!this.ready) {
await this.init();
}
}
}

View File

@@ -2,11 +2,10 @@ import * as FormData from "form-data";
import { HttpsProxyAgent } from "https-proxy-agent";
import * as fe from "node-fetch";
import { ApiService } from "jslib-common/services/api.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { ApiService } from "jslib-common/services/api.service";
(global as any).fetch = fe.default;
(global as any).Request = fe.Request;

View File

@@ -1,13 +1,12 @@
import * as crypto from "crypto";
import * as forge from "node-forge";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { Utils } from "jslib-common/misc/utils";
import { DecryptParameters } from "jslib-common/models/domain/decryptParameters";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { Utils } from "jslib-common/misc/utils";
export class NodeCryptoFunctionService implements CryptoFunctionService {
pbkdf2(
password: string | ArrayBuffer,

View File

@@ -1,19 +1,6 @@
{
"extends": "../shared/tsconfig",
"compilerOptions": {
"pretty": true,
"moduleResolution": "node",
"noImplicitAny": true,
"target": "ES6",
"module": "commonjs",
"lib": ["es5", "es6", "es7", "dom"],
"sourceMap": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "dist/types",
"outDir": "dist",
"types": [],
"paths": {
"jslib-common/*": ["../common/src/*"]
}