mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-16 10:45:20 +01:00
migrate login strategies to new key model
- decrypt and set user symmetric key if Master Key is available - rename keys where applicable - update unit tests
This commit is contained in:
parent
c195847439
commit
91ac281da0
@ -1,7 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { AuthRequestPushNotification } from "../../models/response/notification.response";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
import { AuthResult } from "../models/domain/auth-result";
|
||||
import {
|
||||
@ -32,7 +32,7 @@ export abstract class AuthService {
|
||||
captchaResponse: string
|
||||
) => Promise<AuthResult>;
|
||||
logOut: (callback: () => void) => void;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||
authingWithUserApiKey: () => boolean;
|
||||
authingWithSso: () => boolean;
|
||||
authingWithPassword: () => boolean;
|
||||
|
@ -2,7 +2,7 @@ import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
getAndSetKey: (url?: string) => Promise<void>;
|
||||
getAndSetMasterKey: (url?: string) => Promise<void>;
|
||||
getManagingOrganization: () => Promise<Organization>;
|
||||
getUsesKeyConnector: () => Promise<boolean>;
|
||||
migrateUser: () => Promise<void>;
|
||||
|
@ -15,6 +15,12 @@ import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "../../tools/password-strength";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserSymKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@ -37,7 +43,7 @@ const masterPassword = "password";
|
||||
const deviceId = Utils.newGuid();
|
||||
const accessToken = "ACCESS_TOKEN";
|
||||
const refreshToken = "REFRESH_TOKEN";
|
||||
const encKey = "ENC_KEY";
|
||||
const userSymKey = "USER_SYM_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const captchaSiteKey = "CAPTCHA_SITE_KEY";
|
||||
const kdf = 0;
|
||||
@ -64,7 +70,7 @@ export function identityTokenResponseFactory(
|
||||
ForcePasswordReset: false,
|
||||
Kdf: kdf,
|
||||
KdfIterations: kdfIterations,
|
||||
Key: encKey,
|
||||
Key: userSymKey,
|
||||
PrivateKey: privateKey,
|
||||
ResetMasterPassword: false,
|
||||
access_token: accessToken,
|
||||
@ -129,6 +135,20 @@ describe("LogInStrategy", () => {
|
||||
});
|
||||
|
||||
describe("base class", () => {
|
||||
const userSymKeyBytesLength = 64;
|
||||
const masterKeyBytesLength = 64;
|
||||
let userSymKey: UserSymKey;
|
||||
let masterKey: MasterKey;
|
||||
|
||||
beforeEach(() => {
|
||||
userSymKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray
|
||||
) as UserSymKey;
|
||||
masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(masterKeyBytesLength).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
@ -156,8 +176,6 @@ describe("LogInStrategy", () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey);
|
||||
expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||
});
|
||||
|
||||
@ -187,6 +205,8 @@ describe("LogInStrategy", () => {
|
||||
});
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
@ -204,13 +224,15 @@ describe("LogInStrategy", () => {
|
||||
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey
|
||||
expect(cryptoService.setKey).toHaveBeenCalled();
|
||||
// User symmetric key must be set before the new RSA keypair is generated
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalled();
|
||||
expect(cryptoService.makeKeyPair).toHaveBeenCalled();
|
||||
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
expect(cryptoService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
cryptoService.makeKeyPair.mock.invocationCallOrder[0]
|
||||
);
|
||||
|
||||
|
@ -53,9 +53,6 @@ export abstract class LogInStrategy {
|
||||
| PasswordlessLogInCredentials
|
||||
): Promise<AuthResult>;
|
||||
|
||||
// The user key comes from different sources depending on the login strategy
|
||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string = null
|
||||
@ -141,22 +138,34 @@ export abstract class LogInStrategy {
|
||||
await this.tokenService.setTwoFactorToken(response);
|
||||
}
|
||||
|
||||
await this.setMasterKey(response);
|
||||
|
||||
await this.setUserKey(response);
|
||||
|
||||
// Must come after the user Key is set, otherwise createKeyPairForOldAccount will fail
|
||||
const newSsoUser = response.key == null;
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setEncKey(response.key);
|
||||
await this.cryptoService.setEncPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
await this.setPrivateKey(response);
|
||||
|
||||
this.messagingService.send("loggedIn");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// The keys comes from different sources depending on the login strategy
|
||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected async createKeyPairForOldAccount() {
|
||||
try {
|
||||
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
|
||||
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
|
||||
return privateKey.encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.twoFactorProviders = response.twoFactorProviders2;
|
||||
@ -173,14 +182,4 @@ export abstract class LogInStrategy {
|
||||
result.captchaSiteKey = response.siteKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
private async createKeyPairForOldAccount() {
|
||||
try {
|
||||
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
|
||||
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
|
||||
return privateKey.encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,17 +10,23 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "../../tools/password-strength";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserSymKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
|
||||
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
|
||||
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
||||
|
||||
@ -31,11 +37,11 @@ const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
const hashedPassword = "HASHED_PASSWORD";
|
||||
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
|
||||
const preloginKey = new SymmetricCryptoKey(
|
||||
const masterKey = new SymmetricCryptoKey(
|
||||
Utils.fromB64ToArray(
|
||||
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
|
||||
)
|
||||
);
|
||||
) as MasterKey;
|
||||
const deviceId = Utils.newGuid();
|
||||
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
|
||||
EnforceOnLogin: true,
|
||||
@ -58,6 +64,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
@ -76,7 +83,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
authService.makePreloginKey.mockResolvedValue(preloginKey);
|
||||
authService.makePreloginKey.mockResolvedValue(masterKey);
|
||||
|
||||
cryptoService.hashPassword
|
||||
.calledWith(masterPassword, expect.anything(), undefined)
|
||||
@ -102,10 +109,9 @@ describe("PasswordLogInStrategy", () => {
|
||||
authService
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(
|
||||
identityTokenResponseFactory(masterPasswordPolicy)
|
||||
);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
});
|
||||
|
||||
it("sends master password credentials to the server", async () => {
|
||||
@ -127,15 +133,25 @@ describe("PasswordLogInStrategy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
it("sets keys after a successful authentication", async () => {
|
||||
const userSymKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as UserSymKey;
|
||||
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
|
||||
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
|
||||
expect(cryptoService.setUserSymKeyMasterKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when there are no requirements", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory(null));
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory());
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
|
@ -8,8 +8,8 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@ -36,7 +36,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
|
||||
private localHashedPassword: string;
|
||||
private key: SymmetricCryptoKey;
|
||||
private masterKey: MasterKey;
|
||||
|
||||
/**
|
||||
* Options to track if the user needs to update their password due to a password that does not meet an organization's
|
||||
@ -71,12 +71,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey() {
|
||||
await this.cryptoService.setKey(this.key);
|
||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
@ -96,18 +91,18 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
return result;
|
||||
}
|
||||
|
||||
async logIn(credentials: PasswordLogInCredentials) {
|
||||
override async logIn(credentials: PasswordLogInCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
|
||||
this.key = await this.authService.makePreloginKey(masterPassword, email);
|
||||
this.masterKey = await this.authService.makePreloginKey(masterPassword, email);
|
||||
|
||||
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
||||
this.localHashedPassword = await this.cryptoService.hashPassword(
|
||||
masterPassword,
|
||||
this.key,
|
||||
this.masterKey,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.masterKey);
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
@ -118,6 +113,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
|
||||
const [authResult, identityResponse] = await this.startLogIn();
|
||||
|
||||
const masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
@ -145,6 +141,27 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
await this.cryptoService.setMasterKey(this.masterKey);
|
||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setUserSymKeyMasterKey(response.key);
|
||||
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
|
||||
private getMasterPasswordPolicyOptionsFromResponse(
|
||||
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
|
||||
): MasterPasswordPolicyOptions {
|
||||
|
@ -0,0 +1,103 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../abstractions/appId.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { MessagingService } from "../../abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserSymKey,
|
||||
} from "../../models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { PasswordlessLogInStrategy } from "./passwordless-login.strategy";
|
||||
|
||||
describe("SsoLogInStrategy", () => {
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
|
||||
let passwordlessLoginStrategy: PasswordlessLogInStrategy;
|
||||
let credentials: PasswordlessLogInCredentials;
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
|
||||
const email = "EMAIL";
|
||||
const accessCode = "ACCESS_CODE";
|
||||
const authRequestId = "AUTH_REQUEST_ID";
|
||||
const decKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
const localPasswordHash = "LOCAL_PASSWORD_HASH";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
passwordlessLoginStrategy = new PasswordlessLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService
|
||||
);
|
||||
credentials = new PasswordlessLogInCredentials(
|
||||
email,
|
||||
accessCode,
|
||||
authRequestId,
|
||||
decKey,
|
||||
localPasswordHash
|
||||
);
|
||||
|
||||
tokenResponse = identityTokenResponseFactory();
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
});
|
||||
|
||||
it("sets keys after a successful authentication", async () => {
|
||||
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
const userSymKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as UserSymKey;
|
||||
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
|
||||
|
||||
await passwordlessLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localPasswordHash);
|
||||
expect(cryptoService.setUserSymKeyMasterKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
});
|
@ -5,13 +5,13 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { AuthResult } from "../models/domain/auth-result";
|
||||
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
import { LogInStrategy } from "./login.strategy";
|
||||
|
||||
@ -40,8 +40,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private authService: AuthService
|
||||
twoFactorService: TwoFactorService
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
@ -56,20 +55,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey() {
|
||||
await this.cryptoService.setKey(this.passwordlessCredentials.decKey);
|
||||
await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash);
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
async logIn(credentials: PasswordlessLogInCredentials) {
|
||||
override async logIn(credentials: PasswordlessLogInCredentials) {
|
||||
this.passwordlessCredentials = credentials;
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
@ -84,4 +70,33 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
await this.cryptoService.setMasterKey(this.passwordlessCredentials.decKey);
|
||||
await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash);
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setUserSymKeyMasterKey(response.key);
|
||||
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,17 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserSymKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { SsoLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { SsoLogInStrategy } from "./sso-login.strategy";
|
||||
@ -98,33 +105,60 @@ describe("SsoLogInStrategy", () => {
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setEncKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setPrivateKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets and sets KeyConnector key for enrolled user", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
describe("Key Connector", () => {
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
beforeEach(() => {
|
||||
tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
});
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
||||
const masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
|
||||
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
tokenResponse.key = null;
|
||||
expect(keyConnectorService.getAndSetMasterKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
tokenResponse.key = null;
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId
|
||||
);
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts and sets the user symmetric key if Key Connector is enabled", async () => {
|
||||
const userSymKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as UserSymKey;
|
||||
const masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserSymKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -49,18 +49,6 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey(tokenResponse: IdentityTokenResponse) {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
if (!newSsoUser) {
|
||||
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
|
||||
} else {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: SsoLogInCredentials) {
|
||||
this.orgId = credentials.orgId;
|
||||
this.tokenRequest = new SsoTokenRequest(
|
||||
@ -78,4 +66,43 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
|
||||
return ssoAuthResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
if (!newSsoUser) {
|
||||
await this.keyConnectorService.getAndSetMasterKey(tokenResponse.keyConnectorUrl);
|
||||
} else {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setUserSymKeyMasterKey(tokenResponse.key);
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (!masterKey) {
|
||||
throw new Error("Master key not found");
|
||||
}
|
||||
const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,12 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserSymKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@ -101,7 +107,18 @@ describe("UserApiLogInStrategy", () => {
|
||||
expect(stateService.addAccount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets and sets the Key Connector key from environmentUrl", async () => {
|
||||
it("sets the encrypted user symmetric key and private key from the identity token response", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setUserSymKeyMasterKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
|
||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
@ -110,6 +127,26 @@ describe("UserApiLogInStrategy", () => {
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
expect(keyConnectorService.getAndSetMasterKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
|
||||
it("decrypts and sets the user symmetric key if Key Connector is enabled", async () => {
|
||||
const userSymKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as UserSymKey;
|
||||
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserSymKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userSymKey);
|
||||
});
|
||||
});
|
||||
|
@ -44,14 +44,7 @@ export class UserApiLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey(tokenResponse: IdentityTokenResponse) {
|
||||
if (tokenResponse.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: UserApiLogInCredentials) {
|
||||
override async logIn(credentials: UserApiLogInCredentials) {
|
||||
this.tokenRequest = new UserApiTokenRequest(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
@ -63,6 +56,31 @@ export class UserApiLogInStrategy extends LogInStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
if (response.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.getAndSetMasterKey(keyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setUserSymKeyMasterKey(response.key);
|
||||
|
||||
if (response.apiUseKeyConnector) {
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserSymKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
await super.saveAccountInformation(tokenResponse);
|
||||
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthenticationType } from "../../enums/authentication-type";
|
||||
import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request";
|
||||
|
||||
@ -38,7 +38,7 @@ export class PasswordlessLogInCredentials {
|
||||
public email: string,
|
||||
public accessCode: string,
|
||||
public authRequestId: string,
|
||||
public decKey: SymmetricCryptoKey,
|
||||
public decKey: MasterKey,
|
||||
public localPasswordHash: string,
|
||||
public twoFactor?: TokenTwoFactorRequest
|
||||
) {}
|
||||
|
@ -16,8 +16,8 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
@ -262,7 +262,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
return AuthenticationStatus.Unlocked;
|
||||
}
|
||||
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
|
||||
email = email.trim().toLowerCase();
|
||||
let kdf: KdfType = null;
|
||||
let kdfConfig: KdfConfig = null;
|
||||
|
@ -7,7 +7,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { KdfConfig } from "../models/domain/kdf-config";
|
||||
@ -60,12 +60,13 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
await this.apiService.postConvertToKeyConnector();
|
||||
}
|
||||
|
||||
async getAndSetKey(url: string) {
|
||||
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
||||
async getAndSetMasterKey(url: string) {
|
||||
try {
|
||||
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
|
||||
const k = new SymmetricCryptoKey(keyArr);
|
||||
await this.cryptoService.setKey(k);
|
||||
const masterKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user