mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-18 02:41:15 +02:00
Merge branch 'main' into autofill/pm-6426-create-alarms-manager-and-update-usage-of-long-lived-timeouts-rework
This commit is contained in:
commit
b9055cdc15
@ -208,6 +208,7 @@ import { BrowserStateService as StateServiceAbstraction } from "../platform/serv
|
|||||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||||
|
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||||
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
|
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
|
||||||
import BrowserMessagingService from "../platform/services/browser-messaging.service";
|
import BrowserMessagingService from "../platform/services/browser-messaging.service";
|
||||||
import { BrowserStateService } from "../platform/services/browser-state.service";
|
import { BrowserStateService } from "../platform/services/browser-state.service";
|
||||||
@ -232,7 +233,7 @@ import RuntimeBackground from "./runtime.background";
|
|||||||
|
|
||||||
export default class MainBackground {
|
export default class MainBackground {
|
||||||
messagingService: MessagingServiceAbstraction;
|
messagingService: MessagingServiceAbstraction;
|
||||||
storageService: AbstractStorageService;
|
storageService: AbstractStorageService & ObservableStorageService;
|
||||||
secureStorageService: AbstractStorageService;
|
secureStorageService: AbstractStorageService;
|
||||||
memoryStorageService: AbstractMemoryStorageService;
|
memoryStorageService: AbstractMemoryStorageService;
|
||||||
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
|
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
|
||||||
@ -368,22 +369,28 @@ export default class MainBackground {
|
|||||||
this.cryptoFunctionService = new WebCryptoFunctionService(self);
|
this.cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||||
this.storageService = new BrowserLocalStorageService();
|
this.storageService = new BrowserLocalStorageService();
|
||||||
|
|
||||||
|
const mv3MemoryStorageCreator = (partitionName: string) => {
|
||||||
|
// TODO: Consider using multithreaded encrypt service in popup only context
|
||||||
|
return new LocalBackedSessionStorageService(
|
||||||
|
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||||
|
this.keyGenerationService,
|
||||||
|
new BrowserLocalStorageService(),
|
||||||
|
new BrowserMemoryStorageService(),
|
||||||
|
partitionName,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
|
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
|
||||||
this.memoryStorageService = BrowserApi.isManifestVersion(3)
|
this.memoryStorageService = BrowserApi.isManifestVersion(3)
|
||||||
? new LocalBackedSessionStorageService(
|
? mv3MemoryStorageCreator("stateService")
|
||||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
|
||||||
this.keyGenerationService,
|
|
||||||
)
|
|
||||||
: new MemoryStorageService();
|
: new MemoryStorageService();
|
||||||
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
||||||
? new LocalBackedSessionStorageService(
|
? mv3MemoryStorageCreator("stateProviders")
|
||||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
|
||||||
this.keyGenerationService,
|
|
||||||
)
|
|
||||||
: new BackgroundMemoryStorageService();
|
: new BackgroundMemoryStorageService();
|
||||||
|
|
||||||
const storageServiceProvider = new StorageServiceProvider(
|
const storageServiceProvider = new StorageServiceProvider(
|
||||||
this.storageService as BrowserLocalStorageService,
|
this.storageService,
|
||||||
this.memoryStorageForStateProviders,
|
this.memoryStorageForStateProviders,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory
|
|||||||
|
|
||||||
import { BrowserApi } from "../../browser/browser-api";
|
import { BrowserApi } from "../../browser/browser-api";
|
||||||
import BrowserLocalStorageService from "../../services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../../services/browser-local-storage.service";
|
||||||
|
import BrowserMemoryStorageService from "../../services/browser-memory-storage.service";
|
||||||
import { LocalBackedSessionStorageService } from "../../services/local-backed-session-storage.service";
|
import { LocalBackedSessionStorageService } from "../../services/local-backed-session-storage.service";
|
||||||
import { BackgroundMemoryStorageService } from "../../storage/background-memory-storage.service";
|
import { BackgroundMemoryStorageService } from "../../storage/background-memory-storage.service";
|
||||||
|
|
||||||
@ -17,13 +18,14 @@ import {
|
|||||||
keyGenerationServiceFactory,
|
keyGenerationServiceFactory,
|
||||||
} from "./key-generation-service.factory";
|
} from "./key-generation-service.factory";
|
||||||
|
|
||||||
type StorageServiceFactoryOptions = FactoryOptions;
|
export type DiskStorageServiceInitOptions = FactoryOptions;
|
||||||
|
export type SecureStorageServiceInitOptions = FactoryOptions;
|
||||||
export type DiskStorageServiceInitOptions = StorageServiceFactoryOptions;
|
export type SessionStorageServiceInitOptions = FactoryOptions;
|
||||||
export type SecureStorageServiceInitOptions = StorageServiceFactoryOptions;
|
export type MemoryStorageServiceInitOptions = FactoryOptions &
|
||||||
export type MemoryStorageServiceInitOptions = StorageServiceFactoryOptions &
|
|
||||||
EncryptServiceInitOptions &
|
EncryptServiceInitOptions &
|
||||||
KeyGenerationServiceInitOptions;
|
KeyGenerationServiceInitOptions &
|
||||||
|
DiskStorageServiceInitOptions &
|
||||||
|
SessionStorageServiceInitOptions;
|
||||||
|
|
||||||
export function diskStorageServiceFactory(
|
export function diskStorageServiceFactory(
|
||||||
cache: { diskStorageService?: AbstractStorageService } & CachedServices,
|
cache: { diskStorageService?: AbstractStorageService } & CachedServices,
|
||||||
@ -47,6 +49,13 @@ export function secureStorageServiceFactory(
|
|||||||
return factory(cache, "secureStorageService", opts, () => new BrowserLocalStorageService());
|
return factory(cache, "secureStorageService", opts, () => new BrowserLocalStorageService());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sessionStorageServiceFactory(
|
||||||
|
cache: { sessionStorageService?: AbstractStorageService } & CachedServices,
|
||||||
|
opts: SessionStorageServiceInitOptions,
|
||||||
|
): Promise<AbstractStorageService> {
|
||||||
|
return factory(cache, "sessionStorageService", opts, () => new BrowserMemoryStorageService());
|
||||||
|
}
|
||||||
|
|
||||||
export function memoryStorageServiceFactory(
|
export function memoryStorageServiceFactory(
|
||||||
cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
|
cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
|
||||||
opts: MemoryStorageServiceInitOptions,
|
opts: MemoryStorageServiceInitOptions,
|
||||||
@ -56,6 +65,9 @@ export function memoryStorageServiceFactory(
|
|||||||
return new LocalBackedSessionStorageService(
|
return new LocalBackedSessionStorageService(
|
||||||
await encryptServiceFactory(cache, opts),
|
await encryptServiceFactory(cache, opts),
|
||||||
await keyGenerationServiceFactory(cache, opts),
|
await keyGenerationServiceFactory(cache, opts),
|
||||||
|
await diskStorageServiceFactory(cache, opts),
|
||||||
|
await sessionStorageServiceFactory(cache, opts),
|
||||||
|
"serviceFactories",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new MemoryStorageService();
|
return new MemoryStorageService();
|
||||||
|
@ -2,45 +2,70 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
|
import {
|
||||||
|
AbstractMemoryStorageService,
|
||||||
|
AbstractStorageService,
|
||||||
|
StorageUpdate,
|
||||||
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
import BrowserLocalStorageService from "./browser-local-storage.service";
|
|
||||||
import BrowserMemoryStorageService from "./browser-memory-storage.service";
|
|
||||||
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
|
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
|
||||||
|
|
||||||
describe("Browser Session Storage Service", () => {
|
describe("LocalBackedSessionStorage", () => {
|
||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||||
|
let localStorageService: MockProxy<AbstractStorageService>;
|
||||||
|
let sessionStorageService: MockProxy<AbstractMemoryStorageService>;
|
||||||
|
|
||||||
let cache: Map<string, any>;
|
let cache: Map<string, any>;
|
||||||
const testObj = { a: 1, b: 2 };
|
const testObj = { a: 1, b: 2 };
|
||||||
|
|
||||||
let localStorage: BrowserLocalStorageService;
|
|
||||||
let sessionStorage: BrowserMemoryStorageService;
|
|
||||||
|
|
||||||
const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000"));
|
const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000"));
|
||||||
let getSessionKeySpy: jest.SpyInstance;
|
let getSessionKeySpy: jest.SpyInstance;
|
||||||
|
let sendUpdateSpy: jest.SpyInstance<void, [storageUpdate: StorageUpdate]>;
|
||||||
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
|
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
|
||||||
|
|
||||||
let sut: LocalBackedSessionStorageService;
|
let sut: LocalBackedSessionStorageService;
|
||||||
|
|
||||||
|
const mockExistingSessionKey = (key: SymmetricCryptoKey) => {
|
||||||
|
sessionStorageService.get.mockImplementation((storageKey) => {
|
||||||
|
if (storageKey === "localEncryptionKey_test") {
|
||||||
|
return Promise.resolve(key?.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject("No implementation for " + storageKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
encryptService = mock<EncryptService>();
|
encryptService = mock<EncryptService>();
|
||||||
keyGenerationService = mock<KeyGenerationService>();
|
keyGenerationService = mock<KeyGenerationService>();
|
||||||
|
localStorageService = mock<AbstractStorageService>();
|
||||||
|
sessionStorageService = mock<AbstractMemoryStorageService>();
|
||||||
|
|
||||||
sut = new LocalBackedSessionStorageService(encryptService, keyGenerationService);
|
sut = new LocalBackedSessionStorageService(
|
||||||
|
encryptService,
|
||||||
|
keyGenerationService,
|
||||||
|
localStorageService,
|
||||||
|
sessionStorageService,
|
||||||
|
"test",
|
||||||
|
);
|
||||||
|
|
||||||
cache = sut["cache"];
|
cache = sut["cache"];
|
||||||
localStorage = sut["localStorage"];
|
|
||||||
sessionStorage = sut["sessionStorage"];
|
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||||
|
derivedKey: key,
|
||||||
|
salt: "bitwarden-ephemeral",
|
||||||
|
material: null, // Not used
|
||||||
|
});
|
||||||
|
|
||||||
getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey");
|
getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey");
|
||||||
getSessionKeySpy.mockResolvedValue(key);
|
getSessionKeySpy.mockResolvedValue(key);
|
||||||
});
|
|
||||||
|
|
||||||
it("should exist", () => {
|
sendUpdateSpy = jest.spyOn(sut, "sendUpdate");
|
||||||
expect(sut).toBeInstanceOf(LocalBackedSessionStorageService);
|
sendUpdateSpy.mockReturnValue();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("get", () => {
|
describe("get", () => {
|
||||||
@ -54,7 +79,7 @@ describe("Browser Session Storage Service", () => {
|
|||||||
const session = { test: testObj };
|
const session = { test: testObj };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(sut, "getSessionEncKey").mockResolvedValue(key);
|
mockExistingSessionKey(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("no session retrieved", () => {
|
describe("no session retrieved", () => {
|
||||||
@ -62,6 +87,7 @@ describe("Browser Session Storage Service", () => {
|
|||||||
let spy: jest.SpyInstance;
|
let spy: jest.SpyInstance;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
|
spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
|
||||||
|
localStorageService.get.mockResolvedValue(null);
|
||||||
result = await sut.get("test");
|
result = await sut.get("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -123,31 +149,31 @@ describe("Browser Session Storage Service", () => {
|
|||||||
|
|
||||||
describe("remove", () => {
|
describe("remove", () => {
|
||||||
it("should save null", async () => {
|
it("should save null", async () => {
|
||||||
const spy = jest.spyOn(sut, "save");
|
|
||||||
spy.mockResolvedValue(null);
|
|
||||||
await sut.remove("test");
|
await sut.remove("test");
|
||||||
expect(spy).toHaveBeenCalledWith("test", null);
|
expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("save", () => {
|
describe("save", () => {
|
||||||
describe("caching", () => {
|
describe("caching", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(localStorage, "get").mockResolvedValue(null);
|
localStorageService.get.mockResolvedValue(null);
|
||||||
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
|
sessionStorageService.get.mockResolvedValue(null);
|
||||||
jest.spyOn(localStorage, "save").mockResolvedValue();
|
|
||||||
jest.spyOn(sessionStorage, "save").mockResolvedValue();
|
localStorageService.save.mockResolvedValue();
|
||||||
|
sessionStorageService.save.mockResolvedValue();
|
||||||
|
|
||||||
encryptService.encrypt.mockResolvedValue(mockEnc("{}"));
|
encryptService.encrypt.mockResolvedValue(mockEnc("{}"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove key from cache if value is null", async () => {
|
it("should remove key from cache if value is null", async () => {
|
||||||
cache.set("test", {});
|
cache.set("test", {});
|
||||||
const deleteSpy = jest.spyOn(cache, "delete");
|
const cacheSetSpy = jest.spyOn(cache, "set");
|
||||||
expect(cache.has("test")).toBe(true);
|
expect(cache.has("test")).toBe(true);
|
||||||
await sut.save("test", null);
|
await sut.save("test", null);
|
||||||
expect(cache.has("test")).toBe(false);
|
// Don't remove from cache, just replace with null
|
||||||
expect(deleteSpy).toHaveBeenCalledWith("test");
|
expect(cache.get("test")).toBe(null);
|
||||||
|
expect(cacheSetSpy).toHaveBeenCalledWith("test", null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set cache if value is non-null", async () => {
|
it("should set cache if value is non-null", async () => {
|
||||||
@ -197,7 +223,7 @@ describe("Browser Session Storage Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the stored symmetric crypto key", async () => {
|
it("should return the stored symmetric crypto key", async () => {
|
||||||
jest.spyOn(sessionStorage, "get").mockResolvedValue({ ...key });
|
sessionStorageService.get.mockResolvedValue({ ...key });
|
||||||
const result = await sut.getSessionEncKey();
|
const result = await sut.getSessionEncKey();
|
||||||
|
|
||||||
expect(result).toStrictEqual(key);
|
expect(result).toStrictEqual(key);
|
||||||
@ -205,7 +231,6 @@ describe("Browser Session Storage Service", () => {
|
|||||||
|
|
||||||
describe("new key creation", () => {
|
describe("new key creation", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
|
|
||||||
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||||
salt: "salt",
|
salt: "salt",
|
||||||
material: null,
|
material: null,
|
||||||
@ -218,25 +243,24 @@ describe("Browser Session Storage Service", () => {
|
|||||||
const result = await sut.getSessionEncKey();
|
const result = await sut.getSessionEncKey();
|
||||||
|
|
||||||
expect(result).toStrictEqual(key);
|
expect(result).toStrictEqual(key);
|
||||||
expect(keyGenerationService.createKeyWithPurpose).toBeCalledTimes(1);
|
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should store a symmetric crypto key if it makes one", async () => {
|
it("should store a symmetric crypto key if it makes one", async () => {
|
||||||
const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
||||||
await sut.getSessionEncKey();
|
await sut.getSessionEncKey();
|
||||||
|
|
||||||
expect(spy).toBeCalledWith(key);
|
expect(spy).toHaveBeenCalledWith(key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getLocalSession", () => {
|
describe("getLocalSession", () => {
|
||||||
it("should return null if session is null", async () => {
|
it("should return null if session is null", async () => {
|
||||||
const spy = jest.spyOn(localStorage, "get").mockResolvedValue(null);
|
|
||||||
const result = await sut.getLocalSession(key);
|
const result = await sut.getLocalSession(key);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(spy).toBeCalledWith("session");
|
expect(localStorageService.get).toHaveBeenCalledWith("session_test");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("non-null sessions", () => {
|
describe("non-null sessions", () => {
|
||||||
@ -245,7 +269,7 @@ describe("Browser Session Storage Service", () => {
|
|||||||
const decryptedSession = JSON.stringify(session);
|
const decryptedSession = JSON.stringify(session);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(localStorage, "get").mockResolvedValue(encSession.encryptedString);
|
localStorageService.get.mockResolvedValue(encSession.encryptedString);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should decrypt returned sessions", async () => {
|
it("should decrypt returned sessions", async () => {
|
||||||
@ -267,13 +291,12 @@ describe("Browser Session Storage Service", () => {
|
|||||||
it("should remove state if decryption fails", async () => {
|
it("should remove state if decryption fails", async () => {
|
||||||
encryptService.decryptToUtf8.mockResolvedValue(null);
|
encryptService.decryptToUtf8.mockResolvedValue(null);
|
||||||
const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
||||||
const removeLocalSessionSpy = jest.spyOn(localStorage, "remove").mockResolvedValue();
|
|
||||||
|
|
||||||
const result = await sut.getLocalSession(key);
|
const result = await sut.getLocalSession(key);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(setSessionEncKeySpy).toHaveBeenCalledWith(null);
|
expect(setSessionEncKeySpy).toHaveBeenCalledWith(null);
|
||||||
expect(removeLocalSessionSpy).toHaveBeenCalledWith("session");
|
expect(localStorageService.remove).toHaveBeenCalledWith("session_test");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -284,7 +307,7 @@ describe("Browser Session Storage Service", () => {
|
|||||||
|
|
||||||
it("should encrypt a stringified session", async () => {
|
it("should encrypt a stringified session", async () => {
|
||||||
encryptService.encrypt.mockImplementation(mockEnc);
|
encryptService.encrypt.mockImplementation(mockEnc);
|
||||||
jest.spyOn(localStorage, "save").mockResolvedValue();
|
localStorageService.save.mockResolvedValue();
|
||||||
await sut.setLocalSession(testSession, key);
|
await sut.setLocalSession(testSession, key);
|
||||||
|
|
||||||
expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key);
|
expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key);
|
||||||
@ -292,32 +315,31 @@ describe("Browser Session Storage Service", () => {
|
|||||||
|
|
||||||
it("should remove local session if null", async () => {
|
it("should remove local session if null", async () => {
|
||||||
encryptService.encrypt.mockResolvedValue(null);
|
encryptService.encrypt.mockResolvedValue(null);
|
||||||
const spy = jest.spyOn(localStorage, "remove").mockResolvedValue();
|
|
||||||
await sut.setLocalSession(null, key);
|
await sut.setLocalSession(null, key);
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith("session");
|
expect(localStorageService.remove).toHaveBeenCalledWith("session_test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save encrypted string", async () => {
|
it("should save encrypted string", async () => {
|
||||||
encryptService.encrypt.mockImplementation(mockEnc);
|
encryptService.encrypt.mockImplementation(mockEnc);
|
||||||
const spy = jest.spyOn(localStorage, "save").mockResolvedValue();
|
|
||||||
await sut.setLocalSession(testSession, key);
|
await sut.setLocalSession(testSession, key);
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith("session", (await mockEnc(testJSON)).encryptedString);
|
expect(localStorageService.save).toHaveBeenCalledWith(
|
||||||
|
"session_test",
|
||||||
|
(await mockEnc(testJSON)).encryptedString,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setSessionKey", () => {
|
describe("setSessionKey", () => {
|
||||||
it("should remove if null", async () => {
|
it("should remove if null", async () => {
|
||||||
const spy = jest.spyOn(sessionStorage, "remove").mockResolvedValue();
|
|
||||||
await sut.setSessionEncKey(null);
|
await sut.setSessionEncKey(null);
|
||||||
expect(spy).toHaveBeenCalledWith("localEncryptionKey");
|
expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save key when not null", async () => {
|
it("should save key when not null", async () => {
|
||||||
const spy = jest.spyOn(sessionStorage, "save").mockResolvedValue();
|
|
||||||
await sut.setSessionEncKey(key);
|
await sut.setSessionEncKey(key);
|
||||||
expect(spy).toHaveBeenCalledWith("localEncryptionKey", key);
|
expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,40 +1,60 @@
|
|||||||
import { Subject } from "rxjs";
|
import { Observable, Subject, filter, map, merge, share, tap } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractMemoryStorageService,
|
||||||
|
AbstractStorageService,
|
||||||
|
ObservableStorageService,
|
||||||
StorageUpdate,
|
StorageUpdate,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
|
||||||
|
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||||
import { devFlag } from "../decorators/dev-flag.decorator";
|
import { devFlag } from "../decorators/dev-flag.decorator";
|
||||||
import { devFlagEnabled } from "../flags";
|
import { devFlagEnabled } from "../flags";
|
||||||
|
|
||||||
import BrowserLocalStorageService from "./browser-local-storage.service";
|
export class LocalBackedSessionStorageService
|
||||||
import BrowserMemoryStorageService from "./browser-memory-storage.service";
|
extends AbstractMemoryStorageService
|
||||||
|
implements ObservableStorageService
|
||||||
const keys = {
|
{
|
||||||
encKey: "localEncryptionKey",
|
|
||||||
sessionKey: "session",
|
|
||||||
};
|
|
||||||
|
|
||||||
export class LocalBackedSessionStorageService extends AbstractMemoryStorageService {
|
|
||||||
private cache = new Map<string, unknown>();
|
private cache = new Map<string, unknown>();
|
||||||
private localStorage = new BrowserLocalStorageService();
|
|
||||||
private sessionStorage = new BrowserMemoryStorageService();
|
|
||||||
private updatesSubject = new Subject<StorageUpdate>();
|
private updatesSubject = new Subject<StorageUpdate>();
|
||||||
updates$;
|
|
||||||
|
private commandName = `localBackedSessionStorage_${this.name}`;
|
||||||
|
private encKey = `localEncryptionKey_${this.name}`;
|
||||||
|
private sessionKey = `session_${this.name}`;
|
||||||
|
|
||||||
|
updates$: Observable<StorageUpdate>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private keyGenerationService: KeyGenerationService,
|
private keyGenerationService: KeyGenerationService,
|
||||||
|
private localStorage: AbstractStorageService,
|
||||||
|
private sessionStorage: AbstractStorageService,
|
||||||
|
private name: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.updates$ = this.updatesSubject.asObservable();
|
|
||||||
|
const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe(
|
||||||
|
filter(([msg]) => msg.command === this.commandName),
|
||||||
|
map(([msg]) => msg.update as StorageUpdate),
|
||||||
|
tap((update) => {
|
||||||
|
if (update.updateType === "remove") {
|
||||||
|
this.cache.set(update.key, null);
|
||||||
|
} else {
|
||||||
|
this.cache.delete(update.key);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
share(),
|
||||||
|
);
|
||||||
|
|
||||||
|
remoteObservable.subscribe();
|
||||||
|
|
||||||
|
this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable);
|
||||||
}
|
}
|
||||||
|
|
||||||
get valuesRequireDeserialization(): boolean {
|
get valuesRequireDeserialization(): boolean {
|
||||||
@ -70,23 +90,37 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||||||
|
|
||||||
async save<T>(key: string, obj: T): Promise<void> {
|
async save<T>(key: string, obj: T): Promise<void> {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
this.cache.delete(key);
|
return await this.remove(key);
|
||||||
} else {
|
|
||||||
this.cache.set(key, obj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cache.set(key, obj);
|
||||||
|
await this.updateLocalSessionValue(key, obj);
|
||||||
|
this.sendUpdate({ key, updateType: "save" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(key: string): Promise<void> {
|
||||||
|
this.cache.set(key, null);
|
||||||
|
await this.updateLocalSessionValue(key, null);
|
||||||
|
this.sendUpdate({ key, updateType: "remove" });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendUpdate(storageUpdate: StorageUpdate) {
|
||||||
|
this.updatesSubject.next(storageUpdate);
|
||||||
|
void chrome.runtime.sendMessage({
|
||||||
|
command: this.commandName,
|
||||||
|
update: storageUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateLocalSessionValue<T>(key: string, obj: T) {
|
||||||
const sessionEncKey = await this.getSessionEncKey();
|
const sessionEncKey = await this.getSessionEncKey();
|
||||||
const localSession = (await this.getLocalSession(sessionEncKey)) ?? {};
|
const localSession = (await this.getLocalSession(sessionEncKey)) ?? {};
|
||||||
localSession[key] = obj;
|
localSession[key] = obj;
|
||||||
await this.setLocalSession(localSession, sessionEncKey);
|
await this.setLocalSession(localSession, sessionEncKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(key: string): Promise<void> {
|
|
||||||
await this.save(key, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> {
|
async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> {
|
||||||
const local = await this.localStorage.get<string>(keys.sessionKey);
|
const local = await this.localStorage.get<string>(this.sessionKey);
|
||||||
|
|
||||||
if (local == null) {
|
if (local == null) {
|
||||||
return null;
|
return null;
|
||||||
@ -100,7 +134,7 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||||||
if (sessionJson == null) {
|
if (sessionJson == null) {
|
||||||
// Error with decryption -- session is lost, delete state and key and start over
|
// Error with decryption -- session is lost, delete state and key and start over
|
||||||
await this.setSessionEncKey(null);
|
await this.setSessionEncKey(null);
|
||||||
await this.localStorage.remove(keys.sessionKey);
|
await this.localStorage.remove(this.sessionKey);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return JSON.parse(sessionJson);
|
return JSON.parse(sessionJson);
|
||||||
@ -119,9 +153,9 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||||||
// Make sure we're storing the jsonified version of the session
|
// Make sure we're storing the jsonified version of the session
|
||||||
const jsonSession = JSON.parse(JSON.stringify(session));
|
const jsonSession = JSON.parse(JSON.stringify(session));
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
await this.localStorage.remove(keys.sessionKey);
|
await this.localStorage.remove(this.sessionKey);
|
||||||
} else {
|
} else {
|
||||||
await this.localStorage.save(keys.sessionKey, jsonSession);
|
await this.localStorage.save(this.sessionKey, jsonSession);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,13 +164,13 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||||||
const encSession = await this.encryptService.encrypt(jsonSession, key);
|
const encSession = await this.encryptService.encrypt(jsonSession, key);
|
||||||
|
|
||||||
if (encSession == null) {
|
if (encSession == null) {
|
||||||
return await this.localStorage.remove(keys.sessionKey);
|
return await this.localStorage.remove(this.sessionKey);
|
||||||
}
|
}
|
||||||
await this.localStorage.save(keys.sessionKey, encSession.encryptedString);
|
await this.localStorage.save(this.sessionKey, encSession.encryptedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSessionEncKey(): Promise<SymmetricCryptoKey> {
|
async getSessionEncKey(): Promise<SymmetricCryptoKey> {
|
||||||
let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(keys.encKey);
|
let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(this.encKey);
|
||||||
if (storedKey == null || Object.keys(storedKey).length == 0) {
|
if (storedKey == null || Object.keys(storedKey).length == 0) {
|
||||||
const generatedKey = await this.keyGenerationService.createKeyWithPurpose(
|
const generatedKey = await this.keyGenerationService.createKeyWithPurpose(
|
||||||
128,
|
128,
|
||||||
@ -153,9 +187,9 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||||||
|
|
||||||
async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> {
|
async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> {
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
await this.sessionStorage.remove(keys.encKey);
|
await this.sessionStorage.remove(this.encKey);
|
||||||
} else {
|
} else {
|
||||||
await this.sessionStorage.save(keys.encKey, input);
|
await this.sessionStorage.save(this.encKey, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
apps/desktop/desktop_native/Cargo.lock
generated
69
apps/desktop/desktop_native/Cargo.lock
generated
@ -45,9 +45,9 @@ checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.3.0"
|
version = "3.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08"
|
checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clipboard-win",
|
"clipboard-win",
|
||||||
"log",
|
"log",
|
||||||
@ -56,7 +56,6 @@ dependencies = [
|
|||||||
"objc_id",
|
"objc_id",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"winapi",
|
|
||||||
"wl-clipboard-rs",
|
"wl-clipboard-rs",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
@ -176,13 +175,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "4.5.0"
|
version = "5.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
|
checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"error-code",
|
"error-code",
|
||||||
"str-buf",
|
|
||||||
"winapi",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -348,7 +345,7 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading 0.7.4",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -375,13 +372,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "error-code"
|
name = "error-code"
|
||||||
version = "2.3.1"
|
version = "3.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
|
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"str-buf",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
@ -476,12 +469,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gethostname"
|
name = "gethostname"
|
||||||
version = "0.3.0"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177"
|
checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"winapi",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -659,16 +652,6 @@ version = "0.2.152"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
|
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libloading"
|
|
||||||
version = "0.7.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@ -830,7 +813,7 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b"
|
checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading 0.8.3",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1211,12 +1194,6 @@ version = "1.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "str-buf"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
@ -1516,15 +1493,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-wsapoll"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -1714,22 +1682,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11rb"
|
name = "x11rb"
|
||||||
version = "0.12.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a"
|
checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gethostname",
|
"gethostname",
|
||||||
"nix",
|
"rustix",
|
||||||
"winapi",
|
|
||||||
"winapi-wsapoll",
|
|
||||||
"x11rb-protocol",
|
"x11rb-protocol",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11rb-protocol"
|
name = "x11rb-protocol"
|
||||||
version = "0.12.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc"
|
checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34"
|
||||||
dependencies = [
|
|
||||||
"nix",
|
|
||||||
]
|
|
||||||
|
@ -15,7 +15,7 @@ manual_test = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "=0.8.4"
|
aes = "=0.8.4"
|
||||||
anyhow = "=1.0.80"
|
anyhow = "=1.0.80"
|
||||||
arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] }
|
arboard = { version = "=3.3.2", default-features = false, features = ["wayland-data-control"] }
|
||||||
base64 = "=0.22.0"
|
base64 = "=0.22.0"
|
||||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||||
napi = { version = "=2.16.0", features = ["async"] }
|
napi = { version = "=2.16.0", features = ["async"] }
|
||||||
|
@ -25,7 +25,13 @@
|
|||||||
*ngIf="subscriptionMarkedForCancel"
|
*ngIf="subscriptionMarkedForCancel"
|
||||||
>
|
>
|
||||||
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
|
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
|
||||||
<button bitButton buttonType="secondary" [bitAction]="reinstate" type="button">
|
<button
|
||||||
|
*ngIf="userOrg.canEditSubscription"
|
||||||
|
bitButton
|
||||||
|
buttonType="secondary"
|
||||||
|
[bitAction]="reinstate"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
{{ "reinstateSubscription" | i18n }}
|
{{ "reinstateSubscription" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
|
Loading…
Reference in New Issue
Block a user