1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 08:43:33 +00: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:
Jacob Fink
2023-06-01 08:56:17 -04:00
parent c195847439
commit 91ac281da0
15 changed files with 410 additions and 121 deletions

View File

@@ -1,7 +1,7 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { AuthRequestPushNotification } from "../../models/response/notification.response"; 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 { AuthenticationStatus } from "../enums/authentication-status";
import { AuthResult } from "../models/domain/auth-result"; import { AuthResult } from "../models/domain/auth-result";
import { import {
@@ -32,7 +32,7 @@ export abstract class AuthService {
captchaResponse: string captchaResponse: string
) => Promise<AuthResult>; ) => Promise<AuthResult>;
logOut: (callback: () => void) => void; logOut: (callback: () => void) => void;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>; makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
authingWithUserApiKey: () => boolean; authingWithUserApiKey: () => boolean;
authingWithSso: () => boolean; authingWithSso: () => boolean;
authingWithPassword: () => boolean; authingWithPassword: () => boolean;

View File

@@ -2,7 +2,7 @@ import { Organization } from "../../admin-console/models/domain/organization";
import { IdentityTokenResponse } from "../models/response/identity-token.response"; import { IdentityTokenResponse } from "../models/response/identity-token.response";
export abstract class KeyConnectorService { export abstract class KeyConnectorService {
getAndSetKey: (url?: string) => Promise<void>; getAndSetMasterKey: (url?: string) => Promise<void>;
getManagingOrganization: () => Promise<Organization>; getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>; getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>; migrateUser: () => Promise<void>;

View File

@@ -15,6 +15,12 @@ import {
PasswordStrengthService, PasswordStrengthService,
PasswordStrengthServiceAbstraction, PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength"; } 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 { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service"; import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -37,7 +43,7 @@ const masterPassword = "password";
const deviceId = Utils.newGuid(); const deviceId = Utils.newGuid();
const accessToken = "ACCESS_TOKEN"; const accessToken = "ACCESS_TOKEN";
const refreshToken = "REFRESH_TOKEN"; const refreshToken = "REFRESH_TOKEN";
const encKey = "ENC_KEY"; const userSymKey = "USER_SYM_KEY";
const privateKey = "PRIVATE_KEY"; const privateKey = "PRIVATE_KEY";
const captchaSiteKey = "CAPTCHA_SITE_KEY"; const captchaSiteKey = "CAPTCHA_SITE_KEY";
const kdf = 0; const kdf = 0;
@@ -64,7 +70,7 @@ export function identityTokenResponseFactory(
ForcePasswordReset: false, ForcePasswordReset: false,
Kdf: kdf, Kdf: kdf,
KdfIterations: kdfIterations, KdfIterations: kdfIterations,
Key: encKey, Key: userSymKey,
PrivateKey: privateKey, PrivateKey: privateKey,
ResetMasterPassword: false, ResetMasterPassword: false,
access_token: accessToken, access_token: accessToken,
@@ -129,6 +135,20 @@ describe("LogInStrategy", () => {
}); });
describe("base class", () => { 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 () => { it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); 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"); expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
}); });
@@ -187,6 +205,8 @@ describe("LogInStrategy", () => {
}); });
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
const result = await passwordLogInStrategy.logIn(credentials); const result = await passwordLogInStrategy.logIn(credentials);
@@ -204,13 +224,15 @@ describe("LogInStrategy", () => {
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserSymKeyWithMasterKey.mockResolvedValue(userSymKey);
await passwordLogInStrategy.logIn(credentials); await passwordLogInStrategy.logIn(credentials);
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey // User symmetric key must be set before the new RSA keypair is generated
expect(cryptoService.setKey).toHaveBeenCalled(); expect(cryptoService.setUserKey).toHaveBeenCalled();
expect(cryptoService.makeKeyPair).toHaveBeenCalled(); expect(cryptoService.makeKeyPair).toHaveBeenCalled();
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan( expect(cryptoService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
cryptoService.makeKeyPair.mock.invocationCallOrder[0] cryptoService.makeKeyPair.mock.invocationCallOrder[0]
); );

View File

@@ -53,9 +53,6 @@ export abstract class LogInStrategy {
| PasswordlessLogInCredentials | PasswordlessLogInCredentials
): Promise<AuthResult>; ): Promise<AuthResult>;
// The user key comes from different sources depending on the login strategy
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
async logInTwoFactor( async logInTwoFactor(
twoFactor: TokenTwoFactorRequest, twoFactor: TokenTwoFactorRequest,
captchaResponse: string = null captchaResponse: string = null
@@ -141,22 +138,34 @@ export abstract class LogInStrategy {
await this.tokenService.setTwoFactorToken(response); await this.tokenService.setTwoFactorToken(response);
} }
await this.setMasterKey(response);
await this.setUserKey(response); await this.setUserKey(response);
// Must come after the user Key is set, otherwise createKeyPairForOldAccount will fail await this.setPrivateKey(response);
const newSsoUser = response.key == null;
if (!newSsoUser) {
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
this.messagingService.send("loggedIn"); this.messagingService.send("loggedIn");
return result; 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> { private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult(); const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2; result.twoFactorProviders = response.twoFactorProviders2;
@@ -173,14 +182,4 @@ export abstract class LogInStrategy {
result.captchaSiteKey = response.siteKey; result.captchaSiteKey = response.siteKey;
return result; 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);
}
}
} }

View File

@@ -10,17 +10,23 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils"; import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { import {
PasswordStrengthService, PasswordStrengthService,
PasswordStrengthServiceAbstraction, PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength"; } 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 { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service"; import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type"; import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason"; import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials"; 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 { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response"; import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
@@ -31,11 +37,11 @@ const email = "hello@world.com";
const masterPassword = "password"; const masterPassword = "password";
const hashedPassword = "HASHED_PASSWORD"; const hashedPassword = "HASHED_PASSWORD";
const localHashedPassword = "LOCAL_HASHED_PASSWORD"; const localHashedPassword = "LOCAL_HASHED_PASSWORD";
const preloginKey = new SymmetricCryptoKey( const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray( Utils.fromB64ToArray(
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==" "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
) )
); ) as MasterKey;
const deviceId = Utils.newGuid(); const deviceId = Utils.newGuid();
const masterPasswordPolicy = new MasterPasswordPolicyResponse({ const masterPasswordPolicy = new MasterPasswordPolicyResponse({
EnforceOnLogin: true, EnforceOnLogin: true,
@@ -58,6 +64,7 @@ describe("PasswordLogInStrategy", () => {
let passwordLogInStrategy: PasswordLogInStrategy; let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials; let credentials: PasswordLogInCredentials;
let tokenResponse: IdentityTokenResponse;
beforeEach(async () => { beforeEach(async () => {
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
@@ -76,7 +83,7 @@ describe("PasswordLogInStrategy", () => {
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({}); tokenService.decodeToken.mockResolvedValue({});
authService.makePreloginKey.mockResolvedValue(preloginKey); authService.makePreloginKey.mockResolvedValue(masterKey);
cryptoService.hashPassword cryptoService.hashPassword
.calledWith(masterPassword, expect.anything(), undefined) .calledWith(masterPassword, expect.anything(), undefined)
@@ -102,10 +109,9 @@ describe("PasswordLogInStrategy", () => {
authService authService
); );
credentials = new PasswordLogInCredentials(email, masterPassword); credentials = new PasswordLogInCredentials(email, masterPassword);
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
apiService.postIdentityToken.mockResolvedValue( apiService.postIdentityToken.mockResolvedValue(tokenResponse);
identityTokenResponseFactory(masterPasswordPolicy)
);
}); });
it("sends master password credentials to the server", async () => { 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); await passwordLogInStrategy.logIn(credentials);
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey); expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword); 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 () => { 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); const result = await passwordLogInStrategy.logIn(credentials);

View File

@@ -8,8 +8,8 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service"; import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthService } from "../abstractions/auth.service"; import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service"; import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -36,7 +36,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
tokenRequest: PasswordTokenRequest; tokenRequest: PasswordTokenRequest;
private localHashedPassword: string; 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 * 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() { override async logInTwoFactor(
await this.cryptoService.setKey(this.key);
await this.cryptoService.setKeyHash(this.localHashedPassword);
}
async logInTwoFactor(
twoFactor: TokenTwoFactorRequest, twoFactor: TokenTwoFactorRequest,
captchaResponse: string captchaResponse: string
): Promise<AuthResult> { ): Promise<AuthResult> {
@@ -96,18 +91,18 @@ export class PasswordLogInStrategy extends LogInStrategy {
return result; return result;
} }
async logIn(credentials: PasswordLogInCredentials) { override async logIn(credentials: PasswordLogInCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials; 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 // Hash the password early (before authentication) so we don't persist it in memory in plaintext
this.localHashedPassword = await this.cryptoService.hashPassword( this.localHashedPassword = await this.cryptoService.hashPassword(
masterPassword, masterPassword,
this.key, this.masterKey,
HashPurpose.LocalAuthorization HashPurpose.LocalAuthorization
); );
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.masterKey);
this.tokenRequest = new PasswordTokenRequest( this.tokenRequest = new PasswordTokenRequest(
email, email,
@@ -118,6 +113,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
); );
const [authResult, identityResponse] = await this.startLogIn(); const [authResult, identityResponse] = await this.startLogIn();
const masterPasswordPolicyOptions = const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
@@ -145,6 +141,27 @@ export class PasswordLogInStrategy extends LogInStrategy {
return authResult; 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( private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
): MasterPasswordPolicyOptions { ): MasterPasswordPolicyOptions {

View File

@@ -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);
});
});

View File

@@ -5,13 +5,13 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service"; import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service"; import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result"; import { AuthResult } from "../models/domain/auth-result";
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials"; import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request"; import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.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"; import { LogInStrategy } from "./login.strategy";
@@ -40,8 +40,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
messagingService: MessagingService, messagingService: MessagingService,
logService: LogService, logService: LogService,
stateService: StateService, stateService: StateService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService
private authService: AuthService
) { ) {
super( super(
cryptoService, cryptoService,
@@ -56,20 +55,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
); );
} }
async setUserKey() { override async logIn(credentials: PasswordlessLogInCredentials) {
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) {
this.passwordlessCredentials = credentials; this.passwordlessCredentials = credentials;
this.tokenRequest = new PasswordTokenRequest( this.tokenRequest = new PasswordTokenRequest(
@@ -84,4 +70,33 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
const [authResult] = await this.startLogIn(); const [authResult] = await this.startLogIn();
return authResult; 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())
);
}
} }

View File

@@ -8,10 +8,17 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils"; 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 { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service"; import { TwoFactorService } from "../abstractions/two-factor.service";
import { SsoLogInCredentials } from "../models/domain/log-in-credentials"; import { SsoLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { identityTokenResponseFactory } from "./login.strategy.spec"; import { identityTokenResponseFactory } from "./login.strategy.spec";
import { SsoLogInStrategy } from "./sso-login.strategy"; import { SsoLogInStrategy } from "./sso-login.strategy";
@@ -98,33 +105,60 @@ describe("SsoLogInStrategy", () => {
await ssoLogInStrategy.logIn(credentials); await ssoLogInStrategy.logIn(credentials);
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled(); expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setEncKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled();
expect(cryptoService.setPrivateKey).not.toHaveBeenCalled();
}); });
it("gets and sets KeyConnector key for enrolled user", async () => { describe("Key Connector", () => {
const tokenResponse = identityTokenResponseFactory(); let tokenResponse: IdentityTokenResponse;
tokenResponse.keyConnectorUrl = keyConnectorUrl; 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 () => { expect(keyConnectorService.getAndSetMasterKey).toHaveBeenCalledWith(keyConnectorUrl);
const tokenResponse = identityTokenResponseFactory(); });
tokenResponse.keyConnectorUrl = keyConnectorUrl;
tokenResponse.key = null;
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( await ssoLogInStrategy.logIn(credentials);
tokenResponse,
ssoOrgId 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);
});
}); });
}); });

View File

@@ -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) { async logIn(credentials: SsoLogInCredentials) {
this.orgId = credentials.orgId; this.orgId = credentials.orgId;
this.tokenRequest = new SsoTokenRequest( this.tokenRequest = new SsoTokenRequest(
@@ -78,4 +66,43 @@ export class SsoLogInStrategy extends LogInStrategy {
return ssoAuthResult; 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())
);
}
}
} }

View File

@@ -9,6 +9,12 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils"; 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 { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service"; import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -101,7 +107,18 @@ describe("UserApiLogInStrategy", () => {
expect(stateService.addAccount).toHaveBeenCalled(); 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(); const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true; tokenResponse.apiUseKeyConnector = true;
@@ -110,6 +127,26 @@ describe("UserApiLogInStrategy", () => {
await apiLogInStrategy.logIn(credentials); 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);
}); });
}); });

View File

@@ -44,14 +44,7 @@ export class UserApiLogInStrategy extends LogInStrategy {
); );
} }
async setUserKey(tokenResponse: IdentityTokenResponse) { override async logIn(credentials: UserApiLogInCredentials) {
if (tokenResponse.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
}
}
async logIn(credentials: UserApiLogInCredentials) {
this.tokenRequest = new UserApiTokenRequest( this.tokenRequest = new UserApiTokenRequest(
credentials.clientId, credentials.clientId,
credentials.clientSecret, credentials.clientSecret,
@@ -63,6 +56,31 @@ export class UserApiLogInStrategy extends LogInStrategy {
return authResult; 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) { protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
await super.saveAccountInformation(tokenResponse); await super.saveAccountInformation(tokenResponse);
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId); await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);

View File

@@ -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 { AuthenticationType } from "../../enums/authentication-type";
import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request";
@@ -38,7 +38,7 @@ export class PasswordlessLogInCredentials {
public email: string, public email: string,
public accessCode: string, public accessCode: string,
public authRequestId: string, public authRequestId: string,
public decKey: SymmetricCryptoKey, public decKey: MasterKey,
public localPasswordHash: string, public localPasswordHash: string,
public twoFactor?: TokenTwoFactorRequest public twoFactor?: TokenTwoFactorRequest
) {} ) {}

View File

@@ -16,8 +16,8 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils"; import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { KeyConnectorService } from "../abstractions/key-connector.service"; import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
@@ -262,7 +262,7 @@ export class AuthService implements AuthServiceAbstraction {
return AuthenticationStatus.Unlocked; return AuthenticationStatus.Unlocked;
} }
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> { async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
email = email.trim().toLowerCase(); email = email.trim().toLowerCase();
let kdf: KdfType = null; let kdf: KdfType = null;
let kdfConfig: KdfConfig = null; let kdfConfig: KdfConfig = null;

View File

@@ -7,7 +7,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service"; import { LogService } from "../../platform/abstractions/log.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils"; 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 { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { KdfConfig } from "../models/domain/kdf-config"; import { KdfConfig } from "../models/domain/kdf-config";
@@ -60,12 +60,13 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.apiService.postConvertToKeyConnector(); await this.apiService.postConvertToKeyConnector();
} }
async getAndSetKey(url: string) { // TODO: UserKey should be renamed to MasterKey and typed accordingly
async getAndSetMasterKey(url: string) {
try { try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url); const masterKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key); const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr); const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
await this.cryptoService.setKey(k); await this.cryptoService.setMasterKey(masterKey);
} catch (e) { } catch (e) {
this.handleKeyConnectorError(e); this.handleKeyConnectorError(e);
} }