diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index a8faa65bb7b..189b4c9c7ce 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -42,6 +42,8 @@ import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; +import { SendAccessRequest } from '../models/request/sendAccessRequest'; +import { SendRequest } from '../models/request/sendRequest'; import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; @@ -92,6 +94,8 @@ import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; +import { SendAccessResponse } from '../models/response/sendAccessResponse'; +import { SendResponse } from '../models/response/sendResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse'; @@ -155,6 +159,15 @@ export abstract class ApiService { putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; + getSend: (id: string) => Promise; + postSendAccess: (id: string, request: SendAccessRequest) => Promise; + getSends: () => Promise>; + postSend: (request: SendRequest) => Promise; + postSendFile: (data: FormData) => Promise; + putSend: (id: string, request: SendRequest) => Promise; + putSendRemovePassword: (id: string) => Promise; + deleteSend: (id: string) => Promise; + getCipher: (id: string) => Promise; getCipherAdmin: (id: string) => Promise; getCiphersOrganization: (organizationId: string) => Promise>; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 2805e1e9831..53376e19e9d 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -35,6 +35,7 @@ export abstract class CryptoService { makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; + makeSendKey: (keyMaterial: ArrayBuffer) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; diff --git a/src/enums/sendType.ts b/src/enums/sendType.ts new file mode 100644 index 00000000000..f5715d869bc --- /dev/null +++ b/src/enums/sendType.ts @@ -0,0 +1,4 @@ +export enum SendType { + Text = 0, + File = 1, +} diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 4d9bf8533e0..cacda81e3ee 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -43,6 +43,10 @@ export class Utils { } } + static fromUrlB64ToArray(str: string): Uint8Array { + return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str)); + } + static fromHexToArray(str: string): Uint8Array { if (Utils.isNode || Utils.isNativeScript) { return new Uint8Array(Buffer.from(str, 'hex')); @@ -90,11 +94,13 @@ export class Utils { } static fromBufferToUrlB64(buffer: ArrayBuffer): string { - const output = this.fromBufferToB64(buffer) - .replace(/\+/g, '-') + return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)) + } + + static fromB64toUrlB64(b64Str: string) { + return b64Str.replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); - return output; } static fromBufferToUtf8(buffer: ArrayBuffer): string { @@ -121,8 +127,8 @@ export class Utils { } } - static fromUrlB64ToUtf8(b64Str: string): string { - let output = b64Str.replace(/-/g, '+').replace(/_/g, '/'); + static fromUrlB64ToB64(urlB64Str: string): string { + let output = urlB64Str.replace(/-/g, '+').replace(/_/g, '/'); switch (output.length % 4) { case 0: break; @@ -136,7 +142,11 @@ export class Utils { throw new Error('Illegal base64url string!'); } - return Utils.fromB64ToUtf8(output); + return output; + } + + static fromUrlB64ToUtf8(urlB64Str: string): string { + return Utils.fromB64ToUtf8(Utils.fromUrlB64ToB64(urlB64Str)); } static fromB64ToUtf8(b64Str: string): string { diff --git a/src/models/api/sendFileApi.ts b/src/models/api/sendFileApi.ts new file mode 100644 index 00000000000..96d386711dd --- /dev/null +++ b/src/models/api/sendFileApi.ts @@ -0,0 +1,23 @@ +import { BaseResponse } from '../response/baseResponse'; + +export class SendFileApi extends BaseResponse { + id: string; + url: string; + fileName: string; + key: string; + size: string; + sizeName: string; + + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.id = this.getResponseProperty('Id'); + this.url = this.getResponseProperty('Url'); + this.fileName = this.getResponseProperty('FileName'); + this.key = this.getResponseProperty('Key'); + this.size = this.getResponseProperty('Size'); + this.sizeName = this.getResponseProperty('SizeName'); + } +} diff --git a/src/models/api/sendTextApi.ts b/src/models/api/sendTextApi.ts new file mode 100644 index 00000000000..083077c3892 --- /dev/null +++ b/src/models/api/sendTextApi.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from '../response/baseResponse'; + +export class SendTextApi extends BaseResponse { + text: string; + hidden: boolean; + + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.text = this.getResponseProperty('Text'); + this.hidden = this.getResponseProperty('Hidden') || false; + } +} diff --git a/src/models/data/sendData.ts b/src/models/data/sendData.ts new file mode 100644 index 00000000000..2d60a371156 --- /dev/null +++ b/src/models/data/sendData.ts @@ -0,0 +1,57 @@ +import { SendType } from '../../enums/sendType'; + +import { SendFileData } from './sendFileData'; +import { SendTextData } from './sendTextData'; + +import { SendResponse } from '../response/sendResponse'; + +export class SendData { + id: string; + accessId: string; + userId: string; + type: SendType; + name: string; + notes: string; + file: SendFileData; + text: SendTextData; + key: string; + maxAccessCount?: number; + accessCount: number; + revisionDate: string; + expirationDate: string; + deletionDate: string; + password: string; + disabled: boolean; + + constructor(response?: SendResponse, userId?: string) { + if (response == null) { + return; + } + + this.id = response.id; + this.accessId = response.accessId; + this.userId = userId; + this.type = response.type; + this.name = response.name; + this.notes = response.notes; + this.key = response.key; + this.maxAccessCount = response.maxAccessCount; + this.accessCount = response.accessCount; + this.revisionDate = response.revisionDate; + this.expirationDate = response.expirationDate; + this.deletionDate = response.deletionDate; + this.password = response.password; + this.disabled = response.disable; + + switch (this.type) { + case SendType.Text: + this.text = new SendTextData(response.text); + break; + case SendType.File: + this.file = new SendFileData(response.file); + break; + default: + break; + } + } +} diff --git a/src/models/data/sendFileData.ts b/src/models/data/sendFileData.ts new file mode 100644 index 00000000000..5301f7d11ed --- /dev/null +++ b/src/models/data/sendFileData.ts @@ -0,0 +1,23 @@ +import { SendFileApi } from '../api/sendFileApi'; + +export class SendFileData { + id: string; + url: string; + fileName: string; + key: string; + size: string; + sizeName: string; + + constructor(data?: SendFileApi) { + if (data == null) { + return; + } + + this.id = data.id; + this.url = data.url; + this.fileName = data.fileName; + this.key = data.key; + this.size = data.size; + this.sizeName = data.sizeName; + } +} diff --git a/src/models/data/sendTextData.ts b/src/models/data/sendTextData.ts new file mode 100644 index 00000000000..1d34addea26 --- /dev/null +++ b/src/models/data/sendTextData.ts @@ -0,0 +1,15 @@ +import { SendTextApi } from '../api/sendTextApi'; + +export class SendTextData { + text: string; + hidden: boolean; + + constructor(data?: SendTextApi) { + if (data == null) { + return; + } + + this.text = data.text; + this.hidden = data.hidden; + } +} diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 703945bd5b4..d3fd879b7cd 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -4,6 +4,8 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { Utils } from '../../misc/utils'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + export class CipherString { encryptedString?: string; encryptionType?: EncryptionType; @@ -89,7 +91,7 @@ export class CipherString { } } - async decrypt(orgId: string): Promise { + async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise { if (this.decryptedValue != null) { return this.decryptedValue; } @@ -103,8 +105,10 @@ export class CipherString { } try { - const orgKey = await cryptoService.getOrgKey(orgId); - this.decryptedValue = await cryptoService.decryptToUtf8(this, orgKey); + if (key == null) { + key = await cryptoService.getOrgKey(orgId); + } + this.decryptedValue = await cryptoService.decryptToUtf8(this, key); } catch (e) { this.decryptedValue = '[error: cannot decrypt]'; } diff --git a/src/models/domain/domainBase.ts b/src/models/domain/domainBase.ts index b73297bcd51..bac499c6b2f 100644 --- a/src/models/domain/domainBase.ts +++ b/src/models/domain/domainBase.ts @@ -2,6 +2,8 @@ import { CipherString } from './cipherString'; import { View } from '../view/view'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + export default class Domain { protected buildDomainModel(domain: D, dataObj: any, map: any, alreadyEncrypted: boolean, notEncList: any[] = []) { @@ -33,7 +35,8 @@ export default class Domain { } } - protected async decryptObj(viewModel: T, map: any, orgId: string): Promise { + protected async decryptObj(viewModel: T, map: any, orgId: string, + key: SymmetricCryptoKey = null): Promise { const promises = []; const self: any = this; @@ -47,7 +50,7 @@ export default class Domain { const p = Promise.resolve().then(() => { const mapProp = map[theProp] || theProp; if (self[mapProp]) { - return self[mapProp].decrypt(orgId); + return self[mapProp].decrypt(orgId, key); } return null; }).then((val: any) => { diff --git a/src/models/domain/send.ts b/src/models/domain/send.ts new file mode 100644 index 00000000000..c80ceac56d0 --- /dev/null +++ b/src/models/domain/send.ts @@ -0,0 +1,140 @@ +import { CryptoService } from '../../abstractions/crypto.service'; + +import { SendType } from '../../enums/sendType'; + +import { Utils } from '../../misc/utils'; + +import { SendData } from '../data/sendData'; + +import { SendView } from '../view/sendView'; + +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SendFile } from './sendFile'; +import { SendText } from './sendText'; + +export class Send extends Domain { + id: string; + accessId: string; + userId: string; + type: SendType; + name: CipherString; + notes: CipherString; + file: SendFile; + text: SendText; + key: CipherString; + maxAccessCount?: number; + accessCount: number; + revisionDate: Date; + expirationDate: Date; + deletionDate: Date; + password: string; + disabled: boolean; + + constructor(obj?: SendData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + accessId: null, + userId: null, + name: null, + notes: null, + key: null, + }, alreadyEncrypted, ['id', 'accessId', 'userId']); + + this.type = obj.type; + this.maxAccessCount = obj.maxAccessCount; + this.accessCount = obj.accessCount; + this.password = obj.password; + this.disabled = obj.disabled; + this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; + this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null; + this.expirationDate = obj.expirationDate != null ? new Date(obj.expirationDate) : null; + + switch (this.type) { + case SendType.Text: + this.text = new SendText(obj.text, alreadyEncrypted); + break; + case SendType.File: + this.file = new SendFile(obj.file, alreadyEncrypted); + break; + default: + break; + } + } + + async decrypt(): Promise { + const model = new SendView(this); + + let cryptoService: CryptoService; + const containerService = (Utils.global as any).bitwardenContainerService; + if (containerService) { + cryptoService = containerService.getCryptoService(); + } else { + throw new Error('global bitwardenContainerService not initialized.'); + } + + try { + model.key = await cryptoService.decryptToBytes(this.key, null); + model.cryptoKey = await cryptoService.makeSendKey(model.key); + } catch (e) { + // TODO: error? + } + + await this.decryptObj(model, { + name: null, + notes: null, + }, null, model.cryptoKey); + + switch (this.type) { + case SendType.File: + model.file = await this.file.decrypt(model.cryptoKey); + break; + case SendType.Text: + model.text = await this.text.decrypt(model.cryptoKey); + break; + default: + break; + } + + return model; + } + + toSendData(userId: string): SendData { + const s = new SendData(); + s.id = this.id; + s.accessId = this.accessId; + s.userId = userId; + s.maxAccessCount = this.maxAccessCount; + s.accessCount = this.accessCount; + s.disabled = this.disabled; + s.password = this.password; + s.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; + s.deletionDate = this.deletionDate != null ? this.deletionDate.toISOString() : null; + s.expirationDate = this.expirationDate != null ? this.expirationDate.toISOString() : null; + s.type = this.type; + + this.buildDataModel(this, s, { + name: null, + notes: null, + key: null, + }); + + switch (s.type) { + case SendType.File: + s.text = this.text.toSendTextData(); + break; + case SendType.Text: + s.file = this.file.toSendFileData(); + break; + default: + break; + } + + return s; + } +} diff --git a/src/models/domain/sendAccess.ts b/src/models/domain/sendAccess.ts new file mode 100644 index 00000000000..155f0c4fbc5 --- /dev/null +++ b/src/models/domain/sendAccess.ts @@ -0,0 +1,65 @@ +import { SendType } from '../../enums/sendType'; + +import { SendAccessResponse } from '../response/sendAccessResponse'; + +import { SendAccessView } from '../view/sendAccessView'; + +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SendFile } from './sendFile'; +import { SendText } from './sendText'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +export class SendAccess extends Domain { + id: string; + type: SendType; + name: CipherString; + file: SendFile; + text: SendText; + + constructor(obj?: SendAccessResponse, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + name: null, + }, alreadyEncrypted, ['id']); + + this.type = obj.type; + + switch (this.type) { + case SendType.Text: + this.text = new SendText(obj.text, alreadyEncrypted); + break; + case SendType.File: + this.file = new SendFile(obj.file, alreadyEncrypted); + break; + default: + break; + } + } + + async decrypt(key: SymmetricCryptoKey): Promise { + const model = new SendAccessView(this); + + await this.decryptObj(model, { + name: null, + }, null, key); + + switch (this.type) { + case SendType.File: + model.file = await this.file.decrypt(key); + break; + case SendType.Text: + model.text = await this.text.decrypt(key); + break; + default: + break; + } + + return model; + } +} diff --git a/src/models/domain/sendFile.ts b/src/models/domain/sendFile.ts new file mode 100644 index 00000000000..ff462ce8239 --- /dev/null +++ b/src/models/domain/sendFile.ts @@ -0,0 +1,49 @@ +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +import { SendFileData } from '../data/sendFileData'; + +import { SendFileView } from '../view/sendFileView'; + +export class SendFile extends Domain { + id: string; + url: string; + size: string; + sizeName: string; + fileName: CipherString; + + constructor(obj?: SendFileData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.size = obj.size; + this.buildDomainModel(this, obj, { + id: null, + url: null, + sizeName: null, + fileName: null, + }, alreadyEncrypted, ['id', 'url', 'sizeName']); + } + + async decrypt(key: SymmetricCryptoKey): Promise { + const view = await this.decryptObj(new SendFileView(this), { + fileName: null, + }, null, key); + return view; + } + + toSendFileData(): SendFileData { + const f = new SendFileData(); + f.size = this.size; + this.buildDataModel(this, f, { + id: null, + url: null, + sizeName: null, + fileName: null, + }, ['id', 'url', 'sizeName']); + return f; + } +} diff --git a/src/models/domain/sendText.ts b/src/models/domain/sendText.ts new file mode 100644 index 00000000000..592a085290d --- /dev/null +++ b/src/models/domain/sendText.ts @@ -0,0 +1,39 @@ +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +import { SendTextData } from '../data/sendTextData'; + +import { SendTextView } from '../view/sendTextView'; + +export class SendText extends Domain { + text: CipherString; + hidden: boolean; + + constructor(obj?: SendTextData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.hidden = obj.hidden; + this.buildDomainModel(this, obj, { + text: null, + }, alreadyEncrypted, []); + } + + decrypt(key: SymmetricCryptoKey): Promise { + return this.decryptObj(new SendTextView(this), { + text: null, + }, null, key); + } + + toSendTextData(): SendTextData { + const t = new SendTextData(); + this.buildDataModel(this, t, { + text: null, + hidden: null, + }, ['hidden']); + return t; + } +} diff --git a/src/models/request/sendAccessRequest.ts b/src/models/request/sendAccessRequest.ts new file mode 100644 index 00000000000..3d49c0c218e --- /dev/null +++ b/src/models/request/sendAccessRequest.ts @@ -0,0 +1,3 @@ +export class SendAccessRequest { + password: string; +} diff --git a/src/models/request/sendRequest.ts b/src/models/request/sendRequest.ts new file mode 100644 index 00000000000..a2c64a2ca08 --- /dev/null +++ b/src/models/request/sendRequest.ts @@ -0,0 +1,46 @@ +import { SendType } from '../../enums/sendType'; + +import { SendFileApi } from '../api/sendFileApi' +import { SendTextApi } from '../api/sendTextApi'; + +import { Send } from '../domain/send'; + +export class SendRequest { + type: SendType; + name: string; + notes: string; + key: string; + maxAccessCount?: number; + expirationDate: string; + deletionDate: string; + text: SendTextApi; + file: SendFileApi; + password: string; + disabled: boolean; + + constructor(send: Send) { + this.type = send.type; + this.name = send.name ? send.name.encryptedString : null; + this.notes = send.notes ? send.notes.encryptedString : null; + this.maxAccessCount = send.maxAccessCount; + this.expirationDate = send.expirationDate != null ? send.expirationDate.toISOString() : null; + this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null; + this.key = send.key != null ? send.key.encryptedString : null; + this.password = send.password; + this.disabled = send.disabled; + + switch (this.type) { + case SendType.Text: + this.text = new SendTextApi(); + this.text.text = send.text.text != null ? send.text.text.encryptedString : null; + this.text.hidden = send.text.hidden; + break; + case SendType.File: + this.file = new SendFileApi(); + this.file.fileName = send.file.fileName != null ? send.file.fileName.encryptedString : null; + break; + default: + break; + } + } +} diff --git a/src/models/response/baseResponse.ts b/src/models/response/baseResponse.ts index 645689f8f89..3818432237b 100644 --- a/src/models/response/baseResponse.ts +++ b/src/models/response/baseResponse.ts @@ -1,5 +1,5 @@ export abstract class BaseResponse { - protected response: any; + private response: any; constructor(response: any) { this.response = response; diff --git a/src/models/response/sendAccessResponse.ts b/src/models/response/sendAccessResponse.ts new file mode 100644 index 00000000000..89ae9aba9b5 --- /dev/null +++ b/src/models/response/sendAccessResponse.ts @@ -0,0 +1,31 @@ +import { BaseResponse } from './baseResponse'; + +import { SendType } from '../../enums/sendType'; + +import { SendFileApi } from '../api/sendFileApi'; +import { SendTextApi } from '../api/sendTextApi'; + +export class SendAccessResponse extends BaseResponse { + id: string; + type: SendType; + name: string; + file: SendFileApi; + text: SendTextApi; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.type = this.getResponseProperty('Type'); + this.name = this.getResponseProperty('Name'); + + const text = this.getResponseProperty('Text'); + if (text != null) { + this.text = new SendTextApi(text); + } + + const file = this.getResponseProperty('File'); + if (file != null) { + this.file = new SendFileApi(file); + } + } +} diff --git a/src/models/response/sendResponse.ts b/src/models/response/sendResponse.ts new file mode 100644 index 00000000000..039efee4fe5 --- /dev/null +++ b/src/models/response/sendResponse.ts @@ -0,0 +1,51 @@ +import { BaseResponse } from './baseResponse'; + +import { SendType } from '../../enums/sendType'; + +import { SendFileApi } from '../api/sendFileApi'; +import { SendTextApi } from '../api/sendTextApi'; + +export class SendResponse extends BaseResponse { + id: string; + accessId: string; + type: SendType; + name: string; + notes: string; + file: SendFileApi; + text: SendTextApi; + key: string; + maxAccessCount?: number; + accessCount: number; + revisionDate: string; + expirationDate: string; + deletionDate: string; + password: string; + disable: boolean; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.accessId = this.getResponseProperty('AccessId'); + this.type = this.getResponseProperty('Type'); + this.name = this.getResponseProperty('Name'); + this.notes = this.getResponseProperty('Notes'); + this.key = this.getResponseProperty('Key'); + this.maxAccessCount = this.getResponseProperty('MaxAccessCount'); + this.accessCount = this.getResponseProperty('AccessCount'); + this.revisionDate = this.getResponseProperty('RevisionDate'); + this.expirationDate = this.getResponseProperty('ExpirationDate'); + this.deletionDate = this.getResponseProperty('DeletionDate'); + this.password = this.getResponseProperty('Password'); + this.disable = this.getResponseProperty('Disabled') || false; + + const text = this.getResponseProperty('Text'); + if (text != null) { + this.text = new SendTextApi(text); + } + + const file = this.getResponseProperty('File'); + if (file != null) { + this.file = new SendFileApi(file); + } + } +} diff --git a/src/models/view/sendAccessView.ts b/src/models/view/sendAccessView.ts new file mode 100644 index 00000000000..03bc67f10c4 --- /dev/null +++ b/src/models/view/sendAccessView.ts @@ -0,0 +1,24 @@ +import { SendType } from '../../enums/sendType'; + +import { SendAccess } from '../domain/sendAccess'; + +import { SendFileView } from './sendFileView'; +import { SendTextView } from './sendTextView'; +import { View } from './view'; + +export class SendAccessView implements View { + id: string = null; + name: string = null; + type: SendType = null; + text = new SendTextView(); + file = new SendFileView(); + + constructor(s?: SendAccess) { + if (!s) { + return; + } + + this.id = s.id; + this.type = s.type; + } +} diff --git a/src/models/view/sendFileView.ts b/src/models/view/sendFileView.ts new file mode 100644 index 00000000000..07ddb8c8572 --- /dev/null +++ b/src/models/view/sendFileView.ts @@ -0,0 +1,31 @@ +import { View } from './view'; + +import { SendFile } from '../domain/sendFile'; + +export class SendFileView implements View { + id: string = null; + url: string = null; + size: string = null; + sizeName: string = null; + fileName: string = null; + + constructor(f?: SendFile) { + if (!f) { + return; + } + + this.id = f.id; + this.url = f.url; + this.size = f.size; + this.sizeName = f.sizeName; + } + + get fileSize(): number { + try { + if (this.size != null) { + return parseInt(this.size, null); + } + } catch { } + return 0; + } +} diff --git a/src/models/view/sendTextView.ts b/src/models/view/sendTextView.ts new file mode 100644 index 00000000000..b6ec45ee859 --- /dev/null +++ b/src/models/view/sendTextView.ts @@ -0,0 +1,20 @@ +import { View } from './view'; + +import { SendText } from '../domain/sendText'; + +export class SendTextView implements View { + text: string = null; + hidden: boolean; + + constructor(t?: SendText) { + if (!t) { + return; + } + + this.hidden = t.hidden; + } + + get maskedText(): string { + return this.text != null ? '••••••••' : null; + } +} diff --git a/src/models/view/sendView.ts b/src/models/view/sendView.ts new file mode 100644 index 00000000000..61c565dcf0b --- /dev/null +++ b/src/models/view/sendView.ts @@ -0,0 +1,49 @@ +import { SendType } from '../../enums/sendType'; +import { Utils } from '../../misc/utils'; + +import { Send } from '../domain/send'; +import { SymmetricCryptoKey } from '../domain/symmetricCryptoKey'; + +import { SendFileView } from './sendFileView'; +import { SendTextView } from './sendTextView'; +import { View } from './view'; + +export class SendView implements View { + id: string = null; + accessId: string = null; + name: string = null; + notes: string = null; + key: ArrayBuffer; + cryptoKey: SymmetricCryptoKey; + type: SendType = null; + text = new SendTextView(); + file = new SendFileView(); + maxAccessCount?: number = null; + accessCount: number = 0; + revisionDate: Date = null; + deletionDate: Date = null; + expirationDate: Date = null; + password: string = null; + disabled: boolean = false; + + constructor(s?: Send) { + if (!s) { + return; + } + + this.id = s.id; + this.accessId = s.accessId; + this.type = s.type; + this.maxAccessCount = s.maxAccessCount; + this.accessCount = s.accessCount; + this.revisionDate = s.revisionDate; + this.deletionDate = s.deletionDate; + this.expirationDate = s.expirationDate; + this.disabled = s.disabled; + this.password = s.password; + } + + get urlB64Key(): string { + return Utils.fromBufferToUrlB64(this.key); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a7ba94045e5..e5656ab602a 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -46,6 +46,8 @@ import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; +import { SendAccessRequest } from '../models/request/sendAccessRequest'; +import { SendRequest } from '../models/request/sendRequest'; import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; @@ -97,6 +99,8 @@ import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; +import { SendAccessResponse } from '../models/response/sendAccessResponse'; +import { SendResponse } from '../models/response/sendResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse'; @@ -377,6 +381,47 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/folders/' + id, null, true, false); } + // Send APIs + + async getSend(id: string): Promise { + const r = await this.send('GET', '/sends/' + id, null, true, true); + return new SendResponse(r); + } + + async postSendAccess(id: string, request: SendAccessRequest): Promise { + const r = await this.send('POST', '/sends/access/' + id, request, false, true); + return new SendAccessResponse(r); + } + + async getSends(): Promise> { + const r = await this.send('GET', '/sends', null, true, true); + return new ListResponse(r, SendResponse); + } + + async postSend(request: SendRequest): Promise { + const r = await this.send('POST', '/sends', request, true, true); + return new SendResponse(r); + } + + async postSendFile(data: FormData): Promise { + const r = await this.send('POST', '/sends/file', data, true, true); + return new SendResponse(r); + } + + async putSend(id: string, request: SendRequest): Promise { + const r = await this.send('PUT', '/sends/' + id, request, true, true); + return new SendResponse(r); + } + + async putSendRemovePassword(id: string): Promise { + const r = await this.send('PUT', '/sends/' + id + '/remove-password', null, true, true); + return new SendResponse(r); + } + + deleteSend(id: string): Promise { + return this.send('DELETE', '/sends/' + id, null, true, false); + } + // Cipher APIs async getCipher(id: string): Promise { @@ -1040,7 +1085,6 @@ export class ApiService implements ApiServiceAbstraction { } async preValidateSso(identifier: string): Promise { - if (identifier == null || identifier === '') { throw new Error('Organization Identifier was not provided.'); } @@ -1063,7 +1107,7 @@ export class ApiService implements ApiServiceAbstraction { if (response.status === 200) { return true; } else { - const error = await this.handleError(response, false); + const error = await this.handleError(response, false, true); return Promise.reject(error); } } @@ -1111,13 +1155,13 @@ export class ApiService implements ApiServiceAbstraction { const responseJson = await response.json(); return responseJson; } else if (response.status !== 200) { - const error = await this.handleError(response, false); + const error = await this.handleError(response, false, authed); return Promise.reject(error); } } - private async handleError(response: Response, tokenError: boolean): Promise { - if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { + private async handleError(response: Response, tokenError: boolean, authed: boolean): Promise { + if (authed && ((tokenError && response.status === 400) || response.status === 401 || response.status === 403)) { await this.logoutCallback(true); return null; } @@ -1163,7 +1207,7 @@ export class ApiService implements ApiServiceAbstraction { await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); return tokenResponse; } else { - const error = await this.handleError(response, true); + const error = await this.handleError(response, true, true); return Promise.reject(error); } } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index df44ef7789c..a65402b322f 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -353,6 +353,11 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.stretchKey(pinKey); } + async makeSendKey(keyMaterial: ArrayBuffer): Promise { + const sendKey = await this.cryptoFunctionService.hkdf(keyMaterial, 'bitwarden-send', 'send', 64, 'sha256'); + return new SymmetricCryptoKey(sendKey); + } + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { if (key == null) { key = await this.getKey();