PM-7392 - TokenSvc - add checks when setting and retrieving the access token to improve handling around the access token encryption.

This commit is contained in:
Jared Snider 2024-04-24 20:37:24 -04:00
parent 093f9cdeb9
commit aba9dee686
No known key found for this signature in database
GPG Key ID: A149DDD612516286
7 changed files with 73 additions and 22 deletions

View File

@ -22,6 +22,10 @@ import {
LogServiceInitOptions, LogServiceInitOptions,
logServiceFactory, logServiceFactory,
} from "../../../platform/background/service-factories/log-service.factory"; } from "../../../platform/background/service-factories/log-service.factory";
import {
MessagingServiceInitOptions,
messagingServiceFactory,
} from "../../../platform/background/service-factories/messaging-service.factory";
import { import {
PlatformUtilsServiceInitOptions, PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory, platformUtilsServiceFactory,
@ -44,7 +48,8 @@ export type TokenServiceInitOptions = TokenServiceFactoryOptions &
SecureStorageServiceInitOptions & SecureStorageServiceInitOptions &
KeyGenerationServiceInitOptions & KeyGenerationServiceInitOptions &
EncryptServiceInitOptions & EncryptServiceInitOptions &
LogServiceInitOptions; LogServiceInitOptions &
MessagingServiceInitOptions;
export function tokenServiceFactory( export function tokenServiceFactory(
cache: { tokenService?: AbstractTokenService } & CachedServices, cache: { tokenService?: AbstractTokenService } & CachedServices,
@ -63,6 +68,7 @@ export function tokenServiceFactory(
await keyGenerationServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts), await encryptServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await messagingServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -517,6 +517,7 @@ export default class MainBackground {
this.keyGenerationService, this.keyGenerationService,
this.encryptService, this.encryptService,
this.logService, this.logService,
this.messagingService,
); );
const migrationRunner = new MigrationRunner( const migrationRunner = new MigrationRunner(

View File

@ -335,6 +335,7 @@ export class Main {
this.keyGenerationService, this.keyGenerationService,
this.encryptService, this.encryptService,
this.logService, this.logService,
this.messagingService,
); );
const migrationRunner = new MigrationRunner( const migrationRunner = new MigrationRunner(

View File

@ -176,6 +176,16 @@ export class Main {
// Note: secure storage service is not available and should not be called in the main background process. // Note: secure storage service is not available and should not be called in the main background process.
const illegalSecureStorageService = new IllegalSecureStorageService(); const illegalSecureStorageService = new IllegalSecureStorageService();
const messageSubject = new Subject<Message<object>>();
this.messagingService = MessageSender.combine(
new SubjectMessageSender(messageSubject), // For local messages
new ElectronMainMessagingService(this.windowMain),
);
messageSubject.asObservable().subscribe((message) => {
this.messagingMain.onMessage(message);
});
this.tokenService = new TokenService( this.tokenService = new TokenService(
singleUserStateProvider, singleUserStateProvider,
globalStateProvider, globalStateProvider,
@ -184,6 +194,7 @@ export class Main {
this.keyGenerationService, this.keyGenerationService,
this.encryptService, this.encryptService,
this.logService, this.logService,
this.messagingService,
); );
this.migrationRunner = new MigrationRunner( this.migrationRunner = new MigrationRunner(
@ -224,16 +235,6 @@ export class Main {
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
const messageSubject = new Subject<Message<object>>();
this.messagingService = MessageSender.combine(
new SubjectMessageSender(messageSubject), // For local messages
new ElectronMainMessagingService(this.windowMain),
);
messageSubject.asObservable().subscribe((message) => {
this.messagingMain.onMessage(message);
});
this.powerMonitorMain = new PowerMonitorMain(this.messagingService); this.powerMonitorMain = new PowerMonitorMain(this.messagingService);
this.menuMain = new MenuMain( this.menuMain = new MenuMain(
this.i18nService, this.i18nService,

View File

@ -523,6 +523,7 @@ const safeProviders: SafeProvider[] = [
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
EncryptService, EncryptService,
LogService, LogService,
MessagingServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({

View File

@ -6,6 +6,7 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service"; import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service";
import { StorageLocation } from "../../platform/enums"; import { StorageLocation } from "../../platform/enums";
import { StorageOptions } from "../../platform/models/domain/storage-options"; import { StorageOptions } from "../../platform/models/domain/storage-options";
@ -35,6 +36,7 @@ describe("TokenService", () => {
let keyGenerationService: MockProxy<KeyGenerationService>; let keyGenerationService: MockProxy<KeyGenerationService>;
let encryptService: MockProxy<EncryptService>; let encryptService: MockProxy<EncryptService>;
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
let messagingService: MockProxy<MessagingService>;
const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut; const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut;
const memoryVaultTimeout = 30; const memoryVaultTimeout = 30;
@ -92,6 +94,7 @@ describe("TokenService", () => {
keyGenerationService = mock<KeyGenerationService>(); keyGenerationService = mock<KeyGenerationService>();
encryptService = mock<EncryptService>(); encryptService = mock<EncryptService>();
logService = mock<LogService>(); logService = mock<LogService>();
messagingService = mock<MessagingService>();
const supportsSecureStorage = false; // default to false; tests will override as needed const supportsSecureStorage = false; // default to false; tests will override as needed
tokenService = createTokenService(supportsSecureStorage); tokenService = createTokenService(supportsSecureStorage);
@ -2280,6 +2283,7 @@ describe("TokenService", () => {
keyGenerationService, keyGenerationService,
encryptService, encryptService,
logService, logService,
messagingService,
); );
} }
}); });

View File

@ -7,6 +7,7 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service"; import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service";
import { StorageLocation } from "../../platform/enums"; import { StorageLocation } from "../../platform/enums";
import { EncString, EncryptedString } from "../../platform/models/domain/enc-string"; import { EncString, EncryptedString } from "../../platform/models/domain/enc-string";
@ -114,6 +115,8 @@ type AccessTokenKey = Opaque<SymmetricCryptoKey, "AccessTokenKey">;
export class TokenService implements TokenServiceAbstraction { export class TokenService implements TokenServiceAbstraction {
private readonly accessTokenKeySecureStorageKey: string = "_accessTokenKey"; private readonly accessTokenKeySecureStorageKey: string = "_accessTokenKey";
private readonly accessTokenKeyFailedToSaveToSecureStorageError =
"Access token key not saved to secure storage.";
private readonly refreshTokenSecureStorageKey: string = "_refreshToken"; private readonly refreshTokenSecureStorageKey: string = "_refreshToken";
@ -131,6 +134,7 @@ export class TokenService implements TokenServiceAbstraction {
private keyGenerationService: KeyGenerationService, private keyGenerationService: KeyGenerationService,
private encryptService: EncryptService, private encryptService: EncryptService,
private logService: LogService, private logService: LogService,
private messagingService: MessagingService,
) { ) {
this.initializeState(); this.initializeState();
} }
@ -208,6 +212,17 @@ export class TokenService implements TokenServiceAbstraction {
this.getSecureStorageOptions(userId), this.getSecureStorageOptions(userId),
); );
// We are having intermittent issues with access token keys not saving into secure storage on windows 10/11.
// So, let's add a check to ensure we can read the value after writing it.
const accessTokenKey = await this.getAccessTokenKey(userId); // replace a
// accessTokenKey = null; // TODO: remove this after testing
if (!accessTokenKey) {
throw new Error(this.accessTokenKeyFailedToSaveToSecureStorageError);
}
return newAccessTokenKey; return newAccessTokenKey;
} }
@ -287,17 +302,31 @@ export class TokenService implements TokenServiceAbstraction {
// store the access token directly. Instead, we encrypt with accessTokenKey and store that // store the access token directly. Instead, we encrypt with accessTokenKey and store that
// in secure storage. // in secure storage.
const encryptedAccessToken: EncString = await this.encryptAccessToken(accessToken, userId); try {
const encryptedAccessToken: EncString = await this.encryptAccessToken(
accessToken,
userId,
);
// Save the encrypted access token to disk
await this.singleUserStateProvider
.get(userId, ACCESS_TOKEN_DISK)
.update((_) => encryptedAccessToken.encryptedString);
// Save the encrypted access token to disk // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408
await this.singleUserStateProvider // 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time.
.get(userId, ACCESS_TOKEN_DISK) // Remove this call to remove the access token from memory after 3 releases.
.update((_) => encryptedAccessToken.encryptedString); await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null);
} catch (error) {
// TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 // check if error is Access token key not saved to secure storage.
// 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time. if (error.message === this.accessTokenKeyFailedToSaveToSecureStorageError) {
// Remove this call to remove the access token from memory after 3 releases. // Fall back to disk storage for unecrypted access token
await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); await this.singleUserStateProvider
.get(userId, ACCESS_TOKEN_DISK)
.update((_) => accessToken);
}
// re-throw the error if it's not the expected error
throw error;
}
return; return;
} }
@ -383,7 +412,15 @@ export class TokenService implements TokenServiceAbstraction {
const accessTokenKey = await this.getAccessTokenKey(userId); const accessTokenKey = await this.getAccessTokenKey(userId);
if (!accessTokenKey) { if (!accessTokenKey) {
// We know this is an unencrypted access token because we don't have an access token key if (this.encryptService.stringIsEncString(accessTokenDisk)) {
// We have to log the user out if we can't decrypt the access token
this.logService.error(
"Access token key not found to decrypt encrypted access token. Logging user out.",
);
this.messagingService.send("logout", { userId });
}
// We know this is an unencrypted access token
return accessTokenDisk; return accessTokenDisk;
} }