diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index e4dd144fa20..59021a556e4 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -203,8 +203,16 @@ const safeProviders: SafeProvider[] = [ // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid // the TokenService having to inject the PlatformUtilsService which introduces a // circular dependency on Desktop only. + // + // For Windows portable builds, we disable secure storage to ensure tokens are + // stored on disk (in bitwarden-appdata) rather than in Windows Credential + // Manager, making them portable across machines. This allows users to move the USB drive + // between computers while maintaining authentication. + // + // Note: Portable mode does not use secure storage for read/write/clear operations, + // preventing any collision with tokens from a regular desktop installation. provide: SUPPORTS_SECURE_STORAGE, - useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, + useValue: ELECTRON_SUPPORTS_SECURE_STORAGE && !ipc.platform.isWindowsPortable, }), safeProvider({ provide: DEFAULT_VAULT_TIMEOUT, diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index a45ac753b3f..5f643242a9c 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -17,6 +17,7 @@ import { isFlatpak, isMacAppStore, isSnapStore, + isWindowsPortable, isWindowsStore, } from "../utils"; @@ -133,6 +134,7 @@ export default { isDev: isDev(), isMacAppStore: isMacAppStore(), isWindowsStore: isWindowsStore(), + isWindowsPortable: isWindowsPortable(), isFlatpak: isFlatpak(), isSnapStore: isSnapStore(), isAppImage: isAppImage(), diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c02bc85f124..ce272705341 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -445,13 +445,15 @@ export class TokenService implements TokenServiceAbstraction { // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout // but we can simply clear all locations to avoid the need to require those parameters. + // When secure storage is supported, clear the encryption key from secure storage. + // When not supported (e.g., portable builds), tokens are stored on disk and this step is skipped. if (this.platformSupportsSecureStorage) { - // Always clear the access token key when clearing the access token - // The next set of the access token will create a new access token key + // Always clear the access token key when clearing the access token. + // The next set of the access token will create a new access token key. await this.clearAccessTokenKey(userId); } - // Platform doesn't support secure storage, so use state provider implementation + // Clear tokens from disk storage (all platforms) await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).update((_) => null, { shouldUpdate: (previousValue) => previousValue !== null, }); @@ -478,6 +480,9 @@ export class TokenService implements TokenServiceAbstraction { return null; } + // When platformSupportsSecureStorage=true, tokens on disk are encrypted and require + // decryption keys from secure storage. When false (e.g., portable builds), tokens are + // stored on disk. if (this.platformSupportsSecureStorage) { let accessTokenKey: AccessTokenKey; try { @@ -1118,6 +1123,9 @@ export class TokenService implements TokenServiceAbstraction { ) { return TokenStorageLocation.Memory; } else { + // Secure storage (e.g., OS credential manager) is preferred when available. + // Desktop portable builds set platformSupportsSecureStorage=false to store tokens + // on disk for portability across machines. if (useSecureStorage && this.platformSupportsSecureStorage) { return TokenStorageLocation.SecureStorage; }