1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-23 11:56:00 +01:00

Some groundwork for Send (#192)

* send work

* New method to update the last used index (#184)

Instead of updating it every time you call getNext(), it will be updated in a separate call, to avoid updating the index when the cipher did not auto-fill correctly (e.g wrong frame)
Fixes #1392

* added OnlyOrg to PolicyType enum (#183)

* [Require SSO] Add policy type enumeration (#186)

* Added SsoAuthentication policy type

* Updated policy type name // added comments for clarification of what each type controls

* [SSO] New user provision flow (#173)

* Initial commit of new user sso flow

* Adjusted stateSplit conditional per review

* Add logging to lowdb storage service (#188)

* Fix lint errors/warnings (#187)

* remove password api

* access id

* makeSendKey

Co-authored-by: Josep Marí <xusoo@users.noreply.github.com>
Co-authored-by: Addison Beck <abeck@bitwarden.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
This commit is contained in:
Kyle Spearrin 2020-11-02 15:58:18 -05:00 committed by GitHub
parent 5e50aa1a19
commit 0e9e73ce95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 783 additions and 18 deletions

View File

@ -42,6 +42,8 @@ import { PreloginRequest } from '../models/request/preloginRequest';
import { RegisterRequest } from '../models/request/registerRequest'; import { RegisterRequest } from '../models/request/registerRequest';
import { SeatRequest } from '../models/request/seatRequest'; import { SeatRequest } from '../models/request/seatRequest';
import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; 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 { SetPasswordRequest } from '../models/request/setPasswordRequest';
import { StorageRequest } from '../models/request/storageRequest'; import { StorageRequest } from '../models/request/storageRequest';
import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest';
@ -92,6 +94,8 @@ import { PolicyResponse } from '../models/response/policyResponse';
import { PreloginResponse } from '../models/response/preloginResponse'; import { PreloginResponse } from '../models/response/preloginResponse';
import { ProfileResponse } from '../models/response/profileResponse'; import { ProfileResponse } from '../models/response/profileResponse';
import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; 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 { SubscriptionResponse } from '../models/response/subscriptionResponse';
import { SyncResponse } from '../models/response/syncResponse'; import { SyncResponse } from '../models/response/syncResponse';
import { TaxInfoResponse } from '../models/response/taxInfoResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse';
@ -155,6 +159,15 @@ export abstract class ApiService {
putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>; putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>;
deleteFolder: (id: string) => Promise<any>; deleteFolder: (id: string) => Promise<any>;
getSend: (id: string) => Promise<SendResponse>;
postSendAccess: (id: string, request: SendAccessRequest) => Promise<SendAccessResponse>;
getSends: () => Promise<ListResponse<SendResponse>>;
postSend: (request: SendRequest) => Promise<SendResponse>;
postSendFile: (data: FormData) => Promise<SendResponse>;
putSend: (id: string, request: SendRequest) => Promise<SendResponse>;
putSendRemovePassword: (id: string) => Promise<SendResponse>;
deleteSend: (id: string) => Promise<any>;
getCipher: (id: string) => Promise<CipherResponse>; getCipher: (id: string) => Promise<CipherResponse>;
getCipherAdmin: (id: string) => Promise<CipherResponse>; getCipherAdmin: (id: string) => Promise<CipherResponse>;
getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>; getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>;

View File

@ -35,6 +35,7 @@ export abstract class CryptoService {
makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>;
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>;
makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise<SymmetricCryptoKey>; makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
hashPassword: (password: string, key: SymmetricCryptoKey) => Promise<string>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise<string>;
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>;
remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>;

4
src/enums/sendType.ts Normal file
View File

@ -0,0 +1,4 @@
export enum SendType {
Text = 0,
File = 1,
}

View File

@ -43,6 +43,10 @@ export class Utils {
} }
} }
static fromUrlB64ToArray(str: string): Uint8Array {
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
}
static fromHexToArray(str: string): Uint8Array { static fromHexToArray(str: string): Uint8Array {
if (Utils.isNode || Utils.isNativeScript) { if (Utils.isNode || Utils.isNativeScript) {
return new Uint8Array(Buffer.from(str, 'hex')); return new Uint8Array(Buffer.from(str, 'hex'));
@ -90,11 +94,13 @@ export class Utils {
} }
static fromBufferToUrlB64(buffer: ArrayBuffer): string { static fromBufferToUrlB64(buffer: ArrayBuffer): string {
const output = this.fromBufferToB64(buffer) return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer))
.replace(/\+/g, '-') }
static fromB64toUrlB64(b64Str: string) {
return b64Str.replace(/\+/g, '-')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/=/g, ''); .replace(/=/g, '');
return output;
} }
static fromBufferToUtf8(buffer: ArrayBuffer): string { static fromBufferToUtf8(buffer: ArrayBuffer): string {
@ -121,8 +127,8 @@ export class Utils {
} }
} }
static fromUrlB64ToUtf8(b64Str: string): string { static fromUrlB64ToB64(urlB64Str: string): string {
let output = b64Str.replace(/-/g, '+').replace(/_/g, '/'); let output = urlB64Str.replace(/-/g, '+').replace(/_/g, '/');
switch (output.length % 4) { switch (output.length % 4) {
case 0: case 0:
break; break;
@ -136,7 +142,11 @@ export class Utils {
throw new Error('Illegal base64url string!'); 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 { static fromB64ToUtf8(b64Str: string): string {

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -4,6 +4,8 @@ import { CryptoService } from '../../abstractions/crypto.service';
import { Utils } from '../../misc/utils'; import { Utils } from '../../misc/utils';
import { SymmetricCryptoKey } from './symmetricCryptoKey';
export class CipherString { export class CipherString {
encryptedString?: string; encryptedString?: string;
encryptionType?: EncryptionType; encryptionType?: EncryptionType;
@ -89,7 +91,7 @@ export class CipherString {
} }
} }
async decrypt(orgId: string): Promise<string> { async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise<string> {
if (this.decryptedValue != null) { if (this.decryptedValue != null) {
return this.decryptedValue; return this.decryptedValue;
} }
@ -103,8 +105,10 @@ export class CipherString {
} }
try { try {
const orgKey = await cryptoService.getOrgKey(orgId); if (key == null) {
this.decryptedValue = await cryptoService.decryptToUtf8(this, orgKey); key = await cryptoService.getOrgKey(orgId);
}
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
} catch (e) { } catch (e) {
this.decryptedValue = '[error: cannot decrypt]'; this.decryptedValue = '[error: cannot decrypt]';
} }

View File

@ -2,6 +2,8 @@ import { CipherString } from './cipherString';
import { View } from '../view/view'; import { View } from '../view/view';
import { SymmetricCryptoKey } from './symmetricCryptoKey';
export default class Domain { export default class Domain {
protected buildDomainModel<D extends Domain>(domain: D, dataObj: any, map: any, protected buildDomainModel<D extends Domain>(domain: D, dataObj: any, map: any,
alreadyEncrypted: boolean, notEncList: any[] = []) { alreadyEncrypted: boolean, notEncList: any[] = []) {
@ -33,7 +35,8 @@ export default class Domain {
} }
} }
protected async decryptObj<T extends View>(viewModel: T, map: any, orgId: string): Promise<T> { protected async decryptObj<T extends View>(viewModel: T, map: any, orgId: string,
key: SymmetricCryptoKey = null): Promise<T> {
const promises = []; const promises = [];
const self: any = this; const self: any = this;
@ -47,7 +50,7 @@ export default class Domain {
const p = Promise.resolve().then(() => { const p = Promise.resolve().then(() => {
const mapProp = map[theProp] || theProp; const mapProp = map[theProp] || theProp;
if (self[mapProp]) { if (self[mapProp]) {
return self[mapProp].decrypt(orgId); return self[mapProp].decrypt(orgId, key);
} }
return null; return null;
}).then((val: any) => { }).then((val: any) => {

140
src/models/domain/send.ts Normal file
View File

@ -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<SendView> {
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;
}
}

View File

@ -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<SendAccessView> {
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;
}
}

View File

@ -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<SendFileView> {
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;
}
}

View File

@ -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<SendTextView> {
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;
}
}

View File

@ -0,0 +1,3 @@
export class SendAccessRequest {
password: string;
}

View File

@ -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;
}
}
}

View File

@ -1,5 +1,5 @@
export abstract class BaseResponse { export abstract class BaseResponse {
protected response: any; private response: any;
constructor(response: any) { constructor(response: any) {
this.response = response; this.response = response;

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -46,6 +46,8 @@ import { PreloginRequest } from '../models/request/preloginRequest';
import { RegisterRequest } from '../models/request/registerRequest'; import { RegisterRequest } from '../models/request/registerRequest';
import { SeatRequest } from '../models/request/seatRequest'; import { SeatRequest } from '../models/request/seatRequest';
import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; 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 { SetPasswordRequest } from '../models/request/setPasswordRequest';
import { StorageRequest } from '../models/request/storageRequest'; import { StorageRequest } from '../models/request/storageRequest';
import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest';
@ -97,6 +99,8 @@ import { PolicyResponse } from '../models/response/policyResponse';
import { PreloginResponse } from '../models/response/preloginResponse'; import { PreloginResponse } from '../models/response/preloginResponse';
import { ProfileResponse } from '../models/response/profileResponse'; import { ProfileResponse } from '../models/response/profileResponse';
import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; 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 { SubscriptionResponse } from '../models/response/subscriptionResponse';
import { SyncResponse } from '../models/response/syncResponse'; import { SyncResponse } from '../models/response/syncResponse';
import { TaxInfoResponse } from '../models/response/taxInfoResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse';
@ -377,6 +381,47 @@ export class ApiService implements ApiServiceAbstraction {
return this.send('DELETE', '/folders/' + id, null, true, false); return this.send('DELETE', '/folders/' + id, null, true, false);
} }
// Send APIs
async getSend(id: string): Promise<SendResponse> {
const r = await this.send('GET', '/sends/' + id, null, true, true);
return new SendResponse(r);
}
async postSendAccess(id: string, request: SendAccessRequest): Promise<SendAccessResponse> {
const r = await this.send('POST', '/sends/access/' + id, request, false, true);
return new SendAccessResponse(r);
}
async getSends(): Promise<ListResponse<SendResponse>> {
const r = await this.send('GET', '/sends', null, true, true);
return new ListResponse(r, SendResponse);
}
async postSend(request: SendRequest): Promise<SendResponse> {
const r = await this.send('POST', '/sends', request, true, true);
return new SendResponse(r);
}
async postSendFile(data: FormData): Promise<SendResponse> {
const r = await this.send('POST', '/sends/file', data, true, true);
return new SendResponse(r);
}
async putSend(id: string, request: SendRequest): Promise<SendResponse> {
const r = await this.send('PUT', '/sends/' + id, request, true, true);
return new SendResponse(r);
}
async putSendRemovePassword(id: string): Promise<SendResponse> {
const r = await this.send('PUT', '/sends/' + id + '/remove-password', null, true, true);
return new SendResponse(r);
}
deleteSend(id: string): Promise<any> {
return this.send('DELETE', '/sends/' + id, null, true, false);
}
// Cipher APIs // Cipher APIs
async getCipher(id: string): Promise<CipherResponse> { async getCipher(id: string): Promise<CipherResponse> {
@ -1040,7 +1085,6 @@ export class ApiService implements ApiServiceAbstraction {
} }
async preValidateSso(identifier: string): Promise<boolean> { async preValidateSso(identifier: string): Promise<boolean> {
if (identifier == null || identifier === '') { if (identifier == null || identifier === '') {
throw new Error('Organization Identifier was not provided.'); throw new Error('Organization Identifier was not provided.');
} }
@ -1063,7 +1107,7 @@ export class ApiService implements ApiServiceAbstraction {
if (response.status === 200) { if (response.status === 200) {
return true; return true;
} else { } else {
const error = await this.handleError(response, false); const error = await this.handleError(response, false, true);
return Promise.reject(error); return Promise.reject(error);
} }
} }
@ -1111,13 +1155,13 @@ export class ApiService implements ApiServiceAbstraction {
const responseJson = await response.json(); const responseJson = await response.json();
return responseJson; return responseJson;
} else if (response.status !== 200) { } else if (response.status !== 200) {
const error = await this.handleError(response, false); const error = await this.handleError(response, false, authed);
return Promise.reject(error); return Promise.reject(error);
} }
} }
private async handleError(response: Response, tokenError: boolean): Promise<ErrorResponse> { private async handleError(response: Response, tokenError: boolean, authed: boolean): Promise<ErrorResponse> {
if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { if (authed && ((tokenError && response.status === 400) || response.status === 401 || response.status === 403)) {
await this.logoutCallback(true); await this.logoutCallback(true);
return null; return null;
} }
@ -1163,7 +1207,7 @@ export class ApiService implements ApiServiceAbstraction {
await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken);
return tokenResponse; return tokenResponse;
} else { } else {
const error = await this.handleError(response, true); const error = await this.handleError(response, true, true);
return Promise.reject(error); return Promise.reject(error);
} }
} }

View File

@ -353,6 +353,11 @@ export class CryptoService implements CryptoServiceAbstraction {
return await this.stretchKey(pinKey); return await this.stretchKey(pinKey);
} }
async makeSendKey(keyMaterial: ArrayBuffer): Promise<SymmetricCryptoKey> {
const sendKey = await this.cryptoFunctionService.hkdf(keyMaterial, 'bitwarden-send', 'send', 64, 'sha256');
return new SymmetricCryptoKey(sendKey);
}
async hashPassword(password: string, key: SymmetricCryptoKey): Promise<string> { async hashPassword(password: string, key: SymmetricCryptoKey): Promise<string> {
if (key == null) { if (key == null) {
key = await this.getKey(); key = await this.getKey();