mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Tunnels act on complete requests
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { JsonObject } from "type-fest";
|
||||
|
||||
import {
|
||||
CollectionRequest,
|
||||
CollectionAccessDetailsResponse,
|
||||
@@ -74,7 +76,6 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { InitTunnelResponse } from "../auth/models/response/init-tunnel.response";
|
||||
import { KeyConnectorGetUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
import { PreloginResponse } from "../auth/models/response/prelogin.response";
|
||||
import { RegisterResponse } from "../auth/models/response/register.response";
|
||||
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
|
||||
@@ -115,6 +116,7 @@ import { EventResponse } from "../models/response/event.response";
|
||||
import { ListResponse } from "../models/response/list.response";
|
||||
import { ProfileResponse } from "../models/response/profile.response";
|
||||
import { UserKeyResponse } from "../models/response/user-key.response";
|
||||
import { TunneledRequest } from "../platform/communication-tunnel/tunneled.request";
|
||||
import { SyncResponse } from "../platform/sync";
|
||||
import { UserId } from "../types/guid";
|
||||
import { AttachmentRequest } from "../vault/models/request/attachment.request";
|
||||
@@ -507,14 +509,15 @@ export abstract class ApiService {
|
||||
*
|
||||
* @param keyConnectorUrl The URL of the key connector.
|
||||
* @param request The request to send to the key connector. If the shared key is null, falls back to an older GET endpoint that will respond in cleartext.
|
||||
* @returns The raw json body of the response. The response must be passed through a tunnel to be decrypted.
|
||||
*/
|
||||
getMasterKeyFromKeyConnector: (
|
||||
keyConnectorUrl: string,
|
||||
request: KeyConnectorGetUserKeyRequest,
|
||||
) => Promise<KeyConnectorGetUserKeyResponse>;
|
||||
request: KeyConnectorGetUserKeyRequest | TunneledRequest<KeyConnectorGetUserKeyRequest>,
|
||||
) => Promise<JsonObject>;
|
||||
postUserKeyToKeyConnector: (
|
||||
keyConnectorUrl: string,
|
||||
request: KeyConnectorSetUserKeyRequest,
|
||||
request: KeyConnectorSetUserKeyRequest | TunneledRequest<KeyConnectorSetUserKeyRequest>,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Negotiate a tunneled communication protocol with the supplied url.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TunnelVersion } from "../../../platform/communication-tunnel/communication-tunnel";
|
||||
|
||||
export class InitTunnelRequest {
|
||||
constructor(readonly supportedTunnelVersions: TunnelVersion[]) {}
|
||||
constructor(readonly supportedTunnelVersions: readonly TunnelVersion[]) {}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,17 @@
|
||||
import {
|
||||
TunnelVersion,
|
||||
CommunicationTunnel,
|
||||
} from "../../../platform/communication-tunnel/communication-tunnel";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
/**
|
||||
* @typedef { import("../response/key-connector-init-communication.response").KeyConnectorInitCommunicationResponse } KeyConnectorInitCommunicationResponse
|
||||
*/
|
||||
|
||||
export class KeyConnectorSetUserKeyRequest {
|
||||
readonly key: string | null;
|
||||
readonly encryptedKey: Uint8Array | null;
|
||||
readonly sharedKey: Uint8Array | null;
|
||||
readonly tunnelVersion: TunnelVersion;
|
||||
/**
|
||||
*
|
||||
* @param key The key to store, encrypted by the shared key
|
||||
* @param sharedKey The key used to encrypt {@link key}, encapsulated by the {@link KeyConnectorInitCommunicationResponse.encapsulationKey} or null.
|
||||
* If null, the communication is sent in cleartext.
|
||||
* @param key The key to store
|
||||
*/
|
||||
constructor(
|
||||
keyOrEncryptedKey:
|
||||
| string
|
||||
| {
|
||||
key: Uint8Array;
|
||||
sharedKey: Uint8Array;
|
||||
tunnelVersion: TunnelVersion;
|
||||
},
|
||||
) {
|
||||
if (typeof keyOrEncryptedKey === "string") {
|
||||
this.key = keyOrEncryptedKey;
|
||||
this.encryptedKey = undefined;
|
||||
this.sharedKey = undefined;
|
||||
this.tunnelVersion = undefined;
|
||||
} else {
|
||||
this.key = undefined;
|
||||
this.encryptedKey = keyOrEncryptedKey.key;
|
||||
this.sharedKey = keyOrEncryptedKey.sharedKey;
|
||||
this.tunnelVersion = keyOrEncryptedKey.tunnelVersion;
|
||||
}
|
||||
}
|
||||
|
||||
static async BuildForTunnel(tunnel: CommunicationTunnel, masterKey: SymmetricCryptoKey) {
|
||||
switch (tunnel.tunnelVersion) {
|
||||
case TunnelVersion.CLEAR_TEXT:
|
||||
return new KeyConnectorSetUserKeyRequest(masterKey.encKeyB64);
|
||||
case TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM:
|
||||
return new KeyConnectorSetUserKeyRequest({
|
||||
key: await tunnel.protect(masterKey.encKey),
|
||||
sharedKey: tunnel.encapsulatedKey.dataBytes,
|
||||
tunnelVersion: tunnel.tunnelVersion,
|
||||
});
|
||||
default:
|
||||
}
|
||||
}
|
||||
constructor(readonly key: string) {}
|
||||
}
|
||||
|
||||
export class KeyConnectorGetUserKeyRequest {
|
||||
/**
|
||||
* The key to use in encrypting the response, encapsulated by the {@link KeyConnectorInitCommunicationResponse.encapsulationKey}
|
||||
*/
|
||||
readonly sharedKey: Uint8Array;
|
||||
/**
|
||||
* The version of communication to use in encrypting the response
|
||||
*/
|
||||
readonly tunnelVersion: TunnelVersion;
|
||||
/**
|
||||
*
|
||||
* @fixme Once key connector server have been updated, this constructor should require a shared key and ApiService should drop support of the old GET request.
|
||||
*
|
||||
* @param sharedKey The key to use in encrypting the response, encapsulated by the {@link KeyConnectorInitCommunicationResponse.encapsulationKey}.
|
||||
* @param tunnelVersion The version of communication to use in encrypting the response.
|
||||
*/
|
||||
constructor(tunneledCommunication?: { sharedKey: Uint8Array; tunnelVersion: TunnelVersion }) {
|
||||
this.sharedKey = tunneledCommunication?.sharedKey;
|
||||
this.tunnelVersion = tunneledCommunication?.tunnelVersion;
|
||||
}
|
||||
|
||||
static BuildForTunnel(tunnel: CommunicationTunnel) {
|
||||
switch (tunnel.tunnelVersion) {
|
||||
case TunnelVersion.CLEAR_TEXT:
|
||||
return new KeyConnectorGetUserKeyRequest();
|
||||
case TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM:
|
||||
return new KeyConnectorGetUserKeyRequest({
|
||||
sharedKey: tunnel.encapsulatedKey.dataBytes,
|
||||
tunnelVersion: tunnel.tunnelVersion,
|
||||
});
|
||||
default:
|
||||
}
|
||||
}
|
||||
constructor(readonly userId: UserId) {}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import { TunnelVersion } from "../../../platform/communication-tunnel/communicat
|
||||
export class InitTunnelResponse extends BaseResponse {
|
||||
readonly encapsulationKey: Uint8Array;
|
||||
readonly tunnelVersion: TunnelVersion;
|
||||
readonly tunnelIdentifier: string;
|
||||
readonly tunnelDurationSeconds: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.encapsulationKey = new Uint8Array(this.getResponseProperty("EncapsulationKey"));
|
||||
this.tunnelVersion = this.getResponseProperty("TunnelVersion");
|
||||
this.tunnelIdentifier = this.getResponseProperty("TunnelIdentifier");
|
||||
this.tunnelDurationSeconds = this.getResponseProperty("TunnelDurationSeconds");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class KeyConnectorGetUserKeyResponse extends BaseResponse {
|
||||
key: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.key = this.getResponseProperty("Key");
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class KeyConnectorGetUserKeyResponse extends BaseResponse {
|
||||
// Base64 encoded key. This is present only in the clear text version of the response.
|
||||
key: string;
|
||||
// AES-256-GCM encrypted key. This does not exist in EncString and so is not sent or parsed as one.
|
||||
// Expected format is data + tag + iv
|
||||
encryptedKey: Uint8Array;
|
||||
tunnelVersion: KeyConnectorGetUserKeyResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.key = this.getResponseProperty("Key");
|
||||
const responseEncryptedKey = this.getResponseProperty("EncryptedKey");
|
||||
if (responseEncryptedKey) {
|
||||
this.encryptedKey = new Uint8Array(responseEncryptedKey);
|
||||
this.tunnelVersion = this.getResponseProperty("TunnelVersion");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
makeEncString,
|
||||
makeStaticByteArray,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../spec";
|
||||
@@ -27,7 +26,7 @@ import { OrganizationId, UserId } from "../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../types/key";
|
||||
import { KeyConnectorSetUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { KeyConnectorGetUserKeyResponse } from "../models/response/key-connector-user-key.response";
|
||||
import { KeyConnectorGetUserKeyResponse } from "../models/response/key-connector-get-user-key.response";
|
||||
|
||||
import {
|
||||
USES_KEY_CONNECTOR,
|
||||
@@ -61,16 +60,16 @@ describe("KeyConnectorService", () => {
|
||||
"eO9nVlVl3I3sU6O+CyK0kEkpGtl/auT84Hig2WTXmZtDTqYtKpDvUPfjhgMOHf+KQzx++TVS2AOLYq856Caa7w==",
|
||||
),
|
||||
) as MasterKey;
|
||||
let mockMasterKeyResponse: KeyConnectorGetUserKeyResponse;
|
||||
let mockRawMasterKeyResponse: {
|
||||
key: string;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockMasterKeyResponse = new KeyConnectorGetUserKeyResponse({
|
||||
mockRawMasterKeyResponse = {
|
||||
key: mockClearTextMasterKey.keyB64,
|
||||
encryptedKey: makeStaticByteArray(32),
|
||||
tunnelVersion: TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
});
|
||||
};
|
||||
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
@@ -228,10 +227,13 @@ describe("KeyConnectorService", () => {
|
||||
});
|
||||
|
||||
describe("setMasterKeyFromUrl", () => {
|
||||
const tunnel = mock<CommunicationTunnel>();
|
||||
const tunnel = mock<CommunicationTunnel<any>>();
|
||||
beforeEach(() => {
|
||||
communicationTunnelService.createTunnel.mockResolvedValue(tunnel);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockRawMasterKeyResponse);
|
||||
tunnel.unprotect.mockResolvedValue(
|
||||
new KeyConnectorGetUserKeyResponse(mockRawMasterKeyResponse),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -260,14 +262,16 @@ describe("KeyConnectorService", () => {
|
||||
it("should set unprotect and set the master key", async () => {
|
||||
// Arrange
|
||||
const url = "https://key-connector-url.com";
|
||||
mockMasterKeyResponse.key = null;
|
||||
tunnel.unprotect.mockResolvedValue(mockClearTextMasterKey.key);
|
||||
mockRawMasterKeyResponse.key = null;
|
||||
|
||||
// Act
|
||||
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(tunnel.unprotect).toHaveBeenCalledWith(mockMasterKeyResponse.encryptedKey);
|
||||
expect(tunnel.unprotect).toHaveBeenCalledWith(
|
||||
KeyConnectorGetUserKeyResponse,
|
||||
mockRawMasterKeyResponse,
|
||||
);
|
||||
expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
|
||||
mockClearTextMasterKey,
|
||||
mockUserId,
|
||||
@@ -278,7 +282,7 @@ describe("KeyConnectorService", () => {
|
||||
// Arrange
|
||||
const url = "https://key-connector-url.com";
|
||||
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockRawMasterKeyResponse);
|
||||
|
||||
// Act
|
||||
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||
@@ -307,10 +311,12 @@ describe("KeyConnectorService", () => {
|
||||
});
|
||||
|
||||
describe("migrateUser()", () => {
|
||||
const tunnel = mock<CommunicationTunnel>();
|
||||
const tunnel = mock<CommunicationTunnel<[TunnelVersion.CLEAR_TEXT]>>();
|
||||
const keyConnectorRequest = new KeyConnectorSetUserKeyRequest(mockClearTextMasterKey.keyB64);
|
||||
beforeEach(() => {
|
||||
communicationTunnelService.createTunnel.mockResolvedValue(tunnel);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockRawMasterKeyResponse);
|
||||
tunnel.protect.mockResolvedValue(keyConnectorRequest);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -338,10 +344,6 @@ describe("KeyConnectorService", () => {
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
const keyConnectorRequest = await KeyConnectorSetUserKeyRequest.BuildForTunnel(
|
||||
tunnel,
|
||||
masterKey,
|
||||
);
|
||||
|
||||
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
|
||||
@@ -358,15 +360,11 @@ describe("KeyConnectorService", () => {
|
||||
|
||||
it("should set protect and share the master key", async () => {
|
||||
// Arrange
|
||||
mockMasterKeyResponse.key = null;
|
||||
tunnel.unprotect.mockResolvedValue(mockClearTextMasterKey.key);
|
||||
mockRawMasterKeyResponse.key = null;
|
||||
tunnel.unprotectBytes.mockResolvedValue(mockClearTextMasterKey.key);
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = mockClearTextMasterKey;
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
const keyConnectorRequest = await KeyConnectorSetUserKeyRequest.BuildForTunnel(
|
||||
tunnel,
|
||||
masterKey,
|
||||
);
|
||||
|
||||
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
|
||||
@@ -386,10 +384,6 @@ describe("KeyConnectorService", () => {
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
const keyConnectorRequest = await KeyConnectorSetUserKeyRequest.BuildForTunnel(
|
||||
tunnel,
|
||||
masterKey,
|
||||
);
|
||||
|
||||
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
|
||||
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
|
||||
@@ -410,10 +404,6 @@ describe("KeyConnectorService", () => {
|
||||
// Arrange
|
||||
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
|
||||
const masterKey = getMockMasterKey();
|
||||
const keyConnectorRequest = await KeyConnectorSetUserKeyRequest.BuildForTunnel(
|
||||
tunnel,
|
||||
masterKey,
|
||||
);
|
||||
const error = new Error("Failed to post user key to key connector");
|
||||
organizationService.getAll.mockResolvedValue([organization]);
|
||||
|
||||
@@ -437,7 +427,7 @@ describe("KeyConnectorService", () => {
|
||||
});
|
||||
|
||||
describe("convertNewSsoUserToKeyConnector()", () => {
|
||||
const tunnel = mock<CommunicationTunnel>();
|
||||
const tunnel = mock<CommunicationTunnel<[TunnelVersion.CLEAR_TEXT]>>();
|
||||
const mockUserKey = makeSymmetricCryptoKey(64) as UserKey;
|
||||
const mockEncryptedUserKey = makeEncString("encryptedUserKey");
|
||||
const pubKey = "pubKey";
|
||||
@@ -454,7 +444,10 @@ describe("KeyConnectorService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
communicationTunnelService.createTunnel.mockResolvedValue(tunnel);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse);
|
||||
apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockRawMasterKeyResponse);
|
||||
tunnel.protect.mockResolvedValue(
|
||||
new KeyConnectorSetUserKeyRequest(mockClearTextMasterKey.keyB64),
|
||||
);
|
||||
|
||||
keyGenerationService.createKey.mockResolvedValue(mockClearTextMasterKey);
|
||||
keyService.makeMasterKey.mockResolvedValue(mockClearTextMasterKey);
|
||||
@@ -478,13 +471,7 @@ describe("KeyConnectorService", () => {
|
||||
expect(communicationTunnelService.createTunnel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should post user key to key connector", async () => {
|
||||
// Arrange
|
||||
const keyConnectorRequest = await KeyConnectorSetUserKeyRequest.BuildForTunnel(
|
||||
tunnel,
|
||||
mockClearTextMasterKey,
|
||||
);
|
||||
|
||||
it("should post user key to key connector through the tunnel", async () => {
|
||||
// Act
|
||||
await keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||
tokenResponse,
|
||||
@@ -493,6 +480,9 @@ describe("KeyConnectorService", () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(tunnel.protect).toHaveBeenCalledWith(
|
||||
new KeyConnectorSetUserKeyRequest(mockClearTextMasterKey.encKeyB64),
|
||||
);
|
||||
expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse.userDecryptionOptions.keyConnectorOption.keyConnectorUrl,
|
||||
keyConnectorRequest,
|
||||
@@ -580,7 +570,7 @@ describe("KeyConnectorService", () => {
|
||||
}
|
||||
|
||||
function getMockMasterKey(): MasterKey {
|
||||
const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key);
|
||||
const keyArr = Utils.fromB64ToArray(mockRawMasterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { KeyGenerationService } from "../../platform/abstractions/key-generation
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { TunnelVersion } from "../../platform/communication-tunnel/communication-tunnel";
|
||||
import { CommunicationTunnelService } from "../../platform/communication-tunnel/communication-tunnel.service";
|
||||
import { TunneledRequest } from "../../platform/communication-tunnel/tunneled.request";
|
||||
import { KdfType } from "../../platform/enums/kdf-type.enum";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
} from "../models/request/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { KeyConnectorGetUserKeyResponse } from "../models/response/key-connector-get-user-key.response";
|
||||
|
||||
const SUPPORTED_COMMUNICATION_VERSIONS = [
|
||||
TunnelVersion.CLEAR_TEXT,
|
||||
@@ -109,7 +111,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
|
||||
await this.apiService.postUserKeyToKeyConnector(
|
||||
organization.keyConnectorUrl,
|
||||
await KeyConnectorSetUserKeyRequest.BuildForTunnel(tunnel, masterKey),
|
||||
await tunnel.protect(new KeyConnectorSetUserKeyRequest(masterKey.encKeyB64)),
|
||||
);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
@@ -126,14 +128,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
SUPPORTED_COMMUNICATION_VERSIONS,
|
||||
);
|
||||
|
||||
const response = await this.apiService.getMasterKeyFromKeyConnector(
|
||||
const rawResponse = await this.apiService.getMasterKeyFromKeyConnector(
|
||||
url,
|
||||
KeyConnectorGetUserKeyRequest.BuildForTunnel(tunnel),
|
||||
await tunnel.protect(new KeyConnectorGetUserKeyRequest(userId)),
|
||||
);
|
||||
|
||||
// Decrypt the response with the shared key
|
||||
const masterKeyArray =
|
||||
Utils.fromB64ToArray(response.key) ?? (await tunnel.unprotect(response.encryptedKey));
|
||||
const response = await tunnel.unprotect(KeyConnectorGetUserKeyResponse, rawResponse);
|
||||
|
||||
const masterKeyArray = Utils.fromB64ToArray(response.key);
|
||||
|
||||
const masterKey = new SymmetricCryptoKey(masterKeyArray) as MasterKey;
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
@@ -182,14 +184,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
kdfConfig,
|
||||
);
|
||||
|
||||
let keyConnectorRequest: KeyConnectorSetUserKeyRequest;
|
||||
let keyConnectorRequest: TunneledRequest<KeyConnectorSetUserKeyRequest>;
|
||||
try {
|
||||
const tunnel = await this.communicationTunnelService.createTunnel(
|
||||
keyConnectorUrl,
|
||||
SUPPORTED_COMMUNICATION_VERSIONS,
|
||||
);
|
||||
|
||||
keyConnectorRequest = await KeyConnectorSetUserKeyRequest.BuildForTunnel(tunnel, masterKey);
|
||||
keyConnectorRequest = await tunnel.protect(
|
||||
new KeyConnectorSetUserKeyRequest(masterKey.encKeyB64),
|
||||
);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ export abstract class CommunicationTunnelService {
|
||||
* @returns an object that stores the cryptographic secrets to protect and unprotect data for the communication tunnel
|
||||
* @throws errors in the tunnel creation process, including unsupported communication versions or issues communicating with the server
|
||||
*/
|
||||
abstract createTunnel(
|
||||
abstract createTunnel<const SupportedTunnelVersions extends readonly TunnelVersion[]>(
|
||||
url: string,
|
||||
supportedTunnelVersions: TunnelVersion[],
|
||||
): Promise<CommunicationTunnel>;
|
||||
supportedTunnelVersions: SupportedTunnelVersions,
|
||||
): Promise<CommunicationTunnel<SupportedTunnelVersions>>;
|
||||
}
|
||||
|
||||
export class DefaultCommunicationTunnelService implements CommunicationTunnelService {
|
||||
@@ -23,10 +23,10 @@ export class DefaultCommunicationTunnelService implements CommunicationTunnelSer
|
||||
private readonly keyGenerationService: KeyGenerationService,
|
||||
private readonly encryptService: EncryptService,
|
||||
) {}
|
||||
async createTunnel(
|
||||
async createTunnel<const SupportedTunnelVersions extends readonly TunnelVersion[]>(
|
||||
url: string,
|
||||
supportedTunnelVersions: TunnelVersion[],
|
||||
): Promise<CommunicationTunnel> {
|
||||
supportedTunnelVersions: SupportedTunnelVersions,
|
||||
): Promise<CommunicationTunnel<SupportedTunnelVersions>> {
|
||||
const tunnel = new CommunicationTunnel(
|
||||
this.apiService,
|
||||
this.keyGenerationService,
|
||||
|
||||
@@ -3,194 +3,166 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { makeEncString, makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { InitTunnelResponse } from "../../auth/models/response/init-tunnel.response";
|
||||
import { BaseResponse } from "../../models/response/base.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
import { CommunicationTunnel, TunnelVersion } from "./communication-tunnel";
|
||||
import { TunneledRequest } from "./tunneled.request";
|
||||
|
||||
describe("communicationTunnel", () => {
|
||||
class TestRequest {
|
||||
constructor(readonly value: string) {}
|
||||
}
|
||||
|
||||
class TestResponse extends BaseResponse {
|
||||
readonly value: string;
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.value = this.getResponseProperty("Value");
|
||||
}
|
||||
}
|
||||
|
||||
describe("communicationTunnel with cleartext", () => {
|
||||
const url = "http://key-connector.example";
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let sut: CommunicationTunnel;
|
||||
const supportedTunnelVersions = [
|
||||
TunnelVersion.CLEAR_TEXT,
|
||||
TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
] as const;
|
||||
let sut: CommunicationTunnel<typeof supportedTunnelVersions>;
|
||||
const sharedKey = makeSymmetricCryptoKey(32);
|
||||
const encapsulationKey = makeStaticByteArray(32, 1);
|
||||
const encapsulatedKey = makeEncString("encapsulatedKey");
|
||||
const tunnelResponse = new InitTunnelResponse({
|
||||
EncapsulationKey: encapsulationKey,
|
||||
TunnelVersion: TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
TunnelIdentifier: "tunnelIdentifier",
|
||||
TunnelDurationSeconds: 60,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
|
||||
apiService.initCommunicationTunnel.mockResolvedValue(
|
||||
new InitTunnelResponse({
|
||||
EncapsulationKey: encapsulationKey,
|
||||
TunnelVersion: TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
}),
|
||||
);
|
||||
apiService.initCommunicationTunnel.mockResolvedValue(tunnelResponse);
|
||||
keyGenerationService.createKey.mockResolvedValue(sharedKey);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(encapsulatedKey);
|
||||
|
||||
sut = new CommunicationTunnel(
|
||||
apiService,
|
||||
keyGenerationService,
|
||||
encryptService,
|
||||
supportedTunnelVersions,
|
||||
);
|
||||
});
|
||||
|
||||
describe("negotiateTunnel", () => {
|
||||
it("negotiates with the provided server", async () => {
|
||||
const supportedTunnelVersions = [TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM];
|
||||
sut = new CommunicationTunnel(
|
||||
apiService,
|
||||
keyGenerationService,
|
||||
encryptService,
|
||||
supportedTunnelVersions,
|
||||
);
|
||||
it("negotiates with the provided server", async () => {
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
await sut.negotiateTunnel(url);
|
||||
expect(apiService.initCommunicationTunnel).toHaveBeenCalledWith(
|
||||
url,
|
||||
expect.objectContaining({
|
||||
supportedTunnelVersions: expect.arrayContaining([TunnelVersion.CLEAR_TEXT]),
|
||||
}),
|
||||
);
|
||||
expect(sut.tunnelVersion).toBe(TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM);
|
||||
});
|
||||
|
||||
expect(apiService.initCommunicationTunnel).toHaveBeenCalledWith(
|
||||
url,
|
||||
expect.objectContaining({ supportedTunnelVersions }),
|
||||
);
|
||||
it("allows cleartext requests", async () => {
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
// force to clear text tunnel
|
||||
sut["_tunnelVersion"] = TunnelVersion.CLEAR_TEXT;
|
||||
|
||||
const request = new TestRequest("test");
|
||||
const protectedRequest = (await sut.protect(request)) as TestRequest;
|
||||
expect(protectedRequest).toBe(request);
|
||||
});
|
||||
|
||||
it("encrypts the request", async () => {
|
||||
const encryptedData = makeStaticByteArray(32, 3);
|
||||
encryptService.aesGcmEncryptToBytes.mockResolvedValue(encryptedData);
|
||||
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
const request = new TestRequest("test");
|
||||
const protectedRequest = (await sut.protect(request)) as TunneledRequest<TestRequest>;
|
||||
expect(protectedRequest).toEqual({
|
||||
encryptedData: Utils.fromBufferToB64(encryptedData),
|
||||
encapsulatedKey: Utils.fromBufferToB64(encapsulatedKey.dataBytes),
|
||||
tunnelVersion: TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
tunnelIdentifier: tunnelResponse.tunnelIdentifier,
|
||||
});
|
||||
});
|
||||
|
||||
it("generates a shared key", async () => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
]);
|
||||
it("handles cleartext responses", async () => {
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
await sut.negotiateTunnel(url);
|
||||
// force to clear text tunnel
|
||||
sut["_tunnelVersion"] = TunnelVersion.CLEAR_TEXT;
|
||||
|
||||
expect(keyGenerationService.createKey).toHaveBeenCalledWith(256);
|
||||
});
|
||||
const response = { Value: "test" };
|
||||
const unprotectedResponse = await sut.unprotect(TestResponse, response);
|
||||
|
||||
it("encapsulates the shared key", async () => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
]);
|
||||
expect(unprotectedResponse).toEqual(new TestResponse(response));
|
||||
});
|
||||
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(sharedKey.key, encapsulationKey);
|
||||
expect(sut.encapsulatedKey).toBe(encapsulatedKey);
|
||||
});
|
||||
|
||||
it.each(Object.values(TunnelVersion).filter((v) => typeof v !== "number"))(
|
||||
"negotiates tunnel version %s",
|
||||
async (tunnelVersion: keyof typeof TunnelVersion) => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion[tunnelVersion],
|
||||
]);
|
||||
|
||||
apiService.initCommunicationTunnel.mockResolvedValue(
|
||||
new InitTunnelResponse({
|
||||
EncapsulationKey: [1, 2, 3],
|
||||
TunnelVersion: TunnelVersion[tunnelVersion],
|
||||
}),
|
||||
);
|
||||
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
expect(sut.tunnelVersion).toBe(TunnelVersion[tunnelVersion]);
|
||||
},
|
||||
it("decrypts the response", async () => {
|
||||
const clearTextResponse = { Value: "test" };
|
||||
const encryptedData = "encryptedData";
|
||||
encryptService.aesGcmDecryptToBytes.mockResolvedValue(
|
||||
Utils.fromUtf8ToArray(JSON.stringify(clearTextResponse)),
|
||||
);
|
||||
|
||||
it("throws an error if the communication version is not supported", async () => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
]);
|
||||
await sut.negotiateTunnel(url);
|
||||
|
||||
apiService.initCommunicationTunnel.mockResolvedValue(
|
||||
new InitTunnelResponse({
|
||||
EncapsulationKey: [1, 2, 3],
|
||||
TunnelVersion: TunnelVersion.CLEAR_TEXT,
|
||||
}),
|
||||
);
|
||||
const response = { EncryptedResponse: encryptedData };
|
||||
const unprotectedResponse = await sut.unprotect(TestResponse, response);
|
||||
|
||||
await expect(sut.negotiateTunnel(url)).rejects.toThrow("Unsupported communication version");
|
||||
});
|
||||
expect(encryptService.aesGcmDecryptToBytes).toHaveBeenCalledWith(
|
||||
Utils.fromB64ToArray(encryptedData),
|
||||
expect.any(Uint8Array),
|
||||
expect.any(Uint8Array),
|
||||
);
|
||||
expect(unprotectedResponse).toEqual(new TestResponse(clearTextResponse));
|
||||
});
|
||||
});
|
||||
|
||||
describe("communicationTunnel disallows cleartext", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
const supportedTunnelVersions = [TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM] as const;
|
||||
let sut: CommunicationTunnel<typeof supportedTunnelVersions>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
|
||||
sut = new CommunicationTunnel(
|
||||
apiService,
|
||||
keyGenerationService,
|
||||
encryptService,
|
||||
supportedTunnelVersions,
|
||||
);
|
||||
});
|
||||
|
||||
describe("tunnel encryption", () => {
|
||||
const clearText = makeStaticByteArray(32, 2);
|
||||
const protectedText = makeStaticByteArray(32, 3);
|
||||
describe("protect", () => {
|
||||
it("returns only a tunneled request", async () => {
|
||||
return; // this is a compiler test
|
||||
|
||||
beforeEach(() => {
|
||||
encryptService.aesGcmEncryptToBytes.mockResolvedValue(protectedText);
|
||||
encryptService.aesGcmDecryptToBytes.mockResolvedValue(clearText);
|
||||
});
|
||||
const request = new TestRequest("test");
|
||||
|
||||
it("throws an error if the tunnel is not initialized", async () => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
]);
|
||||
// @ts-expect-error -- this cast doesn't work because CLEAR_TEXT is not in the supported tunnel versions, so a TunneledRequest is always returned
|
||||
(await sut.protect(request)) as TestRequest;
|
||||
|
||||
await expect(sut.protect(clearText)).rejects.toThrow("Communication tunnel not initialized");
|
||||
await expect(sut.unprotect(protectedText)).rejects.toThrow(
|
||||
"Communication tunnel not initialized",
|
||||
);
|
||||
});
|
||||
|
||||
describe("CLEAR_TEXT", () => {
|
||||
beforeEach(async () => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion.CLEAR_TEXT,
|
||||
]);
|
||||
apiService.initCommunicationTunnel.mockResolvedValue(
|
||||
new InitTunnelResponse({
|
||||
TunnelVersion: TunnelVersion.CLEAR_TEXT,
|
||||
}),
|
||||
);
|
||||
await sut.negotiateTunnel(url);
|
||||
});
|
||||
|
||||
it("does not encrypt the clear text", async () => {
|
||||
const result = await sut.protect(clearText);
|
||||
|
||||
expect(result).toBe(clearText);
|
||||
expect(encryptService.aesGcmEncryptToBytes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not decrypt the protected text", async () => {
|
||||
const result = await sut.unprotect(protectedText);
|
||||
|
||||
expect(result).toBe(protectedText);
|
||||
expect(encryptService.aesGcmDecryptToBytes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("RSA_ENCAPSULATED_AES_256_GCM", () => {
|
||||
beforeEach(async () => {
|
||||
sut = new CommunicationTunnel(apiService, keyGenerationService, encryptService, [
|
||||
TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
]);
|
||||
apiService.initCommunicationTunnel.mockResolvedValue(
|
||||
new InitTunnelResponse({
|
||||
EncapsulationKey: encapsulationKey,
|
||||
TunnelVersion: TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM,
|
||||
}),
|
||||
);
|
||||
await sut.negotiateTunnel(url);
|
||||
});
|
||||
|
||||
it("encrypts the clear text", async () => {
|
||||
const result = await sut.protect(clearText);
|
||||
|
||||
expect(result).toBe(protectedText);
|
||||
expect(encryptService.aesGcmEncryptToBytes).toHaveBeenCalledWith(
|
||||
clearText,
|
||||
sharedKey.key,
|
||||
expect.toEqualBuffer(new Uint8Array([TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM])),
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts the protected text", async () => {
|
||||
const result = await sut.unprotect(protectedText);
|
||||
|
||||
expect(result).toBe(clearText);
|
||||
expect(encryptService.aesGcmDecryptToBytes).toHaveBeenCalledWith(
|
||||
protectedText,
|
||||
sharedKey.key,
|
||||
expect.toEqualBuffer(new Uint8Array([TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM])),
|
||||
);
|
||||
});
|
||||
});
|
||||
// @ts-expect-no-error -- Must be a TunneledRequest
|
||||
(await sut.protect(request)) as TunneledRequest<TestRequest>;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { Includes, JsonObject } from "type-fest";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { InitTunnelRequest } from "../../auth/models/request/init-tunnel.request";
|
||||
import { InitTunnelResponse } from "../../auth/models/response/init-tunnel.response";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
|
||||
import { TunneledRequest } from "./tunneled.request";
|
||||
import { TunneledResponse } from "./tunneled.response";
|
||||
|
||||
export enum TunnelVersion {
|
||||
CLEAR_TEXT = 0,
|
||||
/**
|
||||
@@ -17,7 +23,12 @@ export enum TunnelVersion {
|
||||
RSA_ENCAPSULATED_AES_256_GCM = 1,
|
||||
}
|
||||
|
||||
export class CommunicationTunnel {
|
||||
type SupportedRequestTypes<TRequest, SupportedTunnelVersions extends readonly TunnelVersion[]> =
|
||||
Includes<SupportedTunnelVersions, TunnelVersion.CLEAR_TEXT> extends true
|
||||
? TunneledRequest<TRequest> | TRequest
|
||||
: TunneledRequest<TRequest>;
|
||||
|
||||
export class CommunicationTunnel<const TSupportedTunnelVersions extends readonly TunnelVersion[]> {
|
||||
private negotiationComplete: boolean = false;
|
||||
private sharedKey: Uint8Array;
|
||||
private _encapsulatedKey: EncString;
|
||||
@@ -28,12 +39,16 @@ export class CommunicationTunnel {
|
||||
get tunnelVersion(): TunnelVersion {
|
||||
return this._tunnelVersion;
|
||||
}
|
||||
private _tunnelIdentifier: string;
|
||||
get tunnelIdentifier(): string {
|
||||
return this._tunnelIdentifier;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
private readonly keyGenerationService: KeyGenerationService,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly supportedTunnelVersions: TunnelVersion[],
|
||||
private readonly supportedTunnelVersions: TSupportedTunnelVersions,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -69,7 +84,49 @@ export class CommunicationTunnel {
|
||||
return await this.initWithVersion(response);
|
||||
}
|
||||
|
||||
async protect(clearText: Uint8Array): Promise<Uint8Array> {
|
||||
async protect<TRequest>(
|
||||
request: TRequest,
|
||||
): Promise<SupportedRequestTypes<TRequest, TSupportedTunnelVersions>> {
|
||||
if (!this.negotiationComplete) {
|
||||
throw new Error("Communication tunnel not initialized");
|
||||
}
|
||||
|
||||
if (this.tunnelVersion === TunnelVersion.CLEAR_TEXT) {
|
||||
// Should only be possible if the tunnel version is clear text
|
||||
return request as SupportedRequestTypes<TRequest, TSupportedTunnelVersions>;
|
||||
}
|
||||
|
||||
const requestBytes = Utils.fromUtf8ToArray(JSON.stringify(request));
|
||||
const protectedText = await this.protectBytes(requestBytes);
|
||||
return new TunneledRequest<TRequest>(
|
||||
protectedText,
|
||||
this.encapsulatedKey,
|
||||
this.tunnelVersion,
|
||||
this.tunnelIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
async unprotect<const TResponse>(
|
||||
responseConstructor: new (response: any) => TResponse,
|
||||
responseData: JsonObject,
|
||||
): Promise<TResponse> {
|
||||
if (!this.negotiationComplete) {
|
||||
throw new Error("Communication tunnel not initialized");
|
||||
}
|
||||
|
||||
if (this.tunnelVersion === TunnelVersion.CLEAR_TEXT) {
|
||||
return new responseConstructor(responseData);
|
||||
}
|
||||
|
||||
const tunneledResponse = new TunneledResponse<TResponse>(responseData);
|
||||
const clearBytes = await this.unprotectBytes(
|
||||
Utils.fromB64ToArray(tunneledResponse.encryptedResponse),
|
||||
);
|
||||
const clearText = Utils.fromBufferToUtf8(clearBytes);
|
||||
return new responseConstructor(JSON.parse(clearText));
|
||||
}
|
||||
|
||||
async protectBytes(clearText: Uint8Array): Promise<Uint8Array> {
|
||||
if (!this.negotiationComplete) {
|
||||
throw new Error("Communication tunnel not initialized");
|
||||
}
|
||||
@@ -92,7 +149,7 @@ export class CommunicationTunnel {
|
||||
return protectedText;
|
||||
}
|
||||
|
||||
async unprotect(protectedText: Uint8Array): Promise<Uint8Array> {
|
||||
async unprotectBytes(protectedText: Uint8Array): Promise<Uint8Array> {
|
||||
if (!this.negotiationComplete) {
|
||||
throw new Error("Communication tunnel not initialized");
|
||||
}
|
||||
@@ -126,6 +183,7 @@ export class CommunicationTunnel {
|
||||
case TunnelVersion.CLEAR_TEXT:
|
||||
break;
|
||||
case TunnelVersion.RSA_ENCAPSULATED_AES_256_GCM: {
|
||||
this._tunnelIdentifier = response.tunnelIdentifier;
|
||||
const encapsulationKey = response.encapsulationKey;
|
||||
|
||||
// use the encapsulation key to create an share a shared key
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Utils } from "../misc/utils";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
|
||||
import { TunnelVersion } from "./communication-tunnel";
|
||||
|
||||
declare const marker: unique symbol;
|
||||
|
||||
export class TunneledRequest<RequestType> {
|
||||
[marker]: RequestType;
|
||||
|
||||
readonly encryptedData: string;
|
||||
readonly encapsulatedKey: string;
|
||||
|
||||
constructor(
|
||||
encryptedData: Uint8Array,
|
||||
encapsulatedKey: EncString,
|
||||
readonly tunnelVersion: TunnelVersion,
|
||||
readonly tunnelIdentifier: string,
|
||||
) {
|
||||
if (encryptedData == null) {
|
||||
throw new Error("encryptedData is required");
|
||||
}
|
||||
if (encapsulatedKey == null) {
|
||||
throw new Error("encapsulatedKey is required");
|
||||
}
|
||||
if (tunnelVersion == null) {
|
||||
throw new Error("tunnelVersion is required");
|
||||
}
|
||||
if (tunnelIdentifier == null) {
|
||||
throw new Error("tunnelIdentifier is required");
|
||||
}
|
||||
this.encapsulatedKey = Utils.fromBufferToB64(encapsulatedKey.dataBytes);
|
||||
this.encryptedData = Utils.fromBufferToB64(encryptedData);
|
||||
}
|
||||
}
|
||||
|
||||
export function isTunneledRequest<RequestType = any>(
|
||||
request: any,
|
||||
): request is TunneledRequest<RequestType> {
|
||||
return (
|
||||
request.encryptedData !== undefined &&
|
||||
request.tunnelVersion !== undefined &&
|
||||
request.tunnelIdentifier !== undefined
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { BaseResponse } from "../../models/response/base.response";
|
||||
|
||||
declare const marker: unique symbol;
|
||||
|
||||
export class TunneledResponse<TResponse> extends BaseResponse {
|
||||
[marker]: TResponse;
|
||||
readonly encryptedResponse: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.encryptedResponse = this.getResponseProperty("EncryptedResponse");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import type { JsonObject } from "type-fest";
|
||||
|
||||
import {
|
||||
CollectionRequest,
|
||||
@@ -82,7 +83,6 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { InitTunnelResponse } from "../auth/models/response/init-tunnel.response";
|
||||
import { KeyConnectorGetUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
import { PreloginResponse } from "../auth/models/response/prelogin.response";
|
||||
import { RegisterResponse } from "../auth/models/response/register.response";
|
||||
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
|
||||
@@ -131,6 +131,10 @@ import { AppIdService } from "../platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "../platform/abstractions/environment.service";
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
isTunneledRequest,
|
||||
TunneledRequest,
|
||||
} from "../platform/communication-tunnel/tunneled.request";
|
||||
import { flagEnabled } from "../platform/misc/flags";
|
||||
import { Utils } from "../platform/misc/utils";
|
||||
import { SyncResponse } from "../platform/sync";
|
||||
@@ -1499,26 +1503,15 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
|
||||
async getMasterKeyFromKeyConnector(
|
||||
keyConnectorUrl: string,
|
||||
request: KeyConnectorGetUserKeyRequest,
|
||||
): Promise<KeyConnectorGetUserKeyResponse> {
|
||||
request: KeyConnectorGetUserKeyRequest | TunneledRequest<KeyConnectorGetUserKeyRequest>,
|
||||
): Promise<JsonObject> {
|
||||
const authHeader = await this.getActiveBearerToken();
|
||||
|
||||
// If shared key is null, we use the old GET endpoint to support older key connectors
|
||||
let response: Response;
|
||||
if (request.sharedKey == null) {
|
||||
if (isTunneledRequest(request)) {
|
||||
response = await this.fetch(
|
||||
new Request(keyConnectorUrl + "/user-keys", {
|
||||
cache: "no-store",
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer " + authHeader,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
response = await this.fetch(
|
||||
new Request(keyConnectorUrl + "/user-keys", {
|
||||
new Request(keyConnectorUrl + "/user-keys/get-user-key", {
|
||||
cache: "no-store",
|
||||
method: "POST",
|
||||
headers: new Headers({
|
||||
@@ -1529,6 +1522,17 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
response = await this.fetch(
|
||||
new Request(keyConnectorUrl + "/user-keys", {
|
||||
cache: "no-store",
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer " + authHeader,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
@@ -1536,7 +1540,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return new KeyConnectorGetUserKeyResponse(await response.json());
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async postUserKeyToKeyConnector(
|
||||
|
||||
Reference in New Issue
Block a user