1
0
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:
Jacob Fink 2023-06-01 08:56:17 -04:00
parent c195847439
commit 91ac281da0
No known key found for this signature in database
GPG Key ID: C2F7ACF05859D008
15 changed files with 410 additions and 121 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

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 { 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())
);
}
}

View File

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

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) {
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())
);
}
}
}

View File

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

View File

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

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 { 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
) {}

View File

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

View File

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