1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-25 16:59:17 +01:00

api support for sharing

This commit is contained in:
Kyle Spearrin 2018-06-12 11:45:02 -04:00
parent 5db55496c2
commit b3f71ed8e4
27 changed files with 371 additions and 12 deletions

View File

@ -1,6 +1,7 @@
import { EnvironmentUrls } from '../models/domain/environmentUrls';
import { CipherRequest } from '../models/request/cipherRequest';
import { CipherShareRequest } from '../models/request/cipherShareRequest';
import { FolderRequest } from '../models/request/folderRequest';
import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest';
import { PasswordHintRequest } from '../models/request/passwordHintRequest';
@ -15,6 +16,8 @@ import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorR
import { ProfileResponse } from '../models/response/profileResponse';
import { SyncResponse } from '../models/response/syncResponse';
import { AttachmentView } from '../models/view/attachmentView';
export abstract class ApiService {
urlsSet: boolean;
baseUrl: string;
@ -34,8 +37,10 @@ export abstract class ApiService {
deleteFolder: (id: string) => Promise<any>;
postCipher: (request: CipherRequest) => Promise<CipherResponse>;
putCipher: (id: string, request: CipherRequest) => Promise<CipherResponse>;
shareCipher: (id: string, request: CipherShareRequest) => Promise<any>;
deleteCipher: (id: string) => Promise<any>;
postCipherAttachment: (id: string, data: FormData) => Promise<CipherResponse>;
shareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise<any>;
deleteCipherAttachment: (id: string, attachmentId: string) => Promise<any>;
getSync: () => Promise<SyncResponse>;
postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise<any>;

View File

@ -6,6 +6,7 @@ import { Cipher } from '../models/domain/cipher';
import { Field } from '../models/domain/field';
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
import { AttachmentView } from '../models/view/attachmentView';
import { CipherView } from '../models/view/cipherView';
import { FieldView } from '../models/view/fieldView';
@ -25,6 +26,9 @@ export abstract class CipherService {
updateLastUsedDate: (id: string) => Promise<void>;
saveNeverDomain: (domain: string) => Promise<void>;
saveWithServer: (cipher: Cipher) => Promise<any>;
shareWithServer: (cipher: Cipher) => Promise<any>;
shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string,
organizationId: string) => Promise<any>;
saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise<Cipher>;
saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer) => Promise<Cipher>;
upsert: (cipher: CipherData | CipherData[]) => Promise<any>;

View File

@ -7,7 +7,10 @@ export class AttachmentData {
size: number;
sizeName: string;
constructor(response: AttachmentResponse) {
constructor(response?: AttachmentResponse) {
if (response == null) {
return;
}
this.id = response.id;
this.url = response.url;
this.fileName = response.fileName;

View File

@ -8,7 +8,11 @@ export class CardData {
expYear: string;
code: string;
constructor(data: CardApi) {
constructor(data?: CardApi) {
if (data == null) {
return;
}
this.cardholderName = data.cardholderName;
this.brand = data.brand;
this.number = data.number;

View File

@ -17,7 +17,7 @@ export class CipherData {
edit: boolean;
organizationUseTotp: boolean;
favorite: boolean;
revisionDate: string;
revisionDate: Date;
type: CipherType;
sizeName: string;
name: string;
@ -30,7 +30,11 @@ export class CipherData {
attachments?: AttachmentData[];
collectionIds?: string[];
constructor(response: CipherResponse, userId: string, collectionIds?: string[]) {
constructor(response?: CipherResponse, userId?: string, collectionIds?: string[]) {
if (response == null) {
return;
}
this.id = response.id;
this.organizationId = response.organizationId;
this.folderId = response.folderId;

View File

@ -7,7 +7,10 @@ export class FieldData {
name: string;
value: string;
constructor(response: FieldApi) {
constructor(response?: FieldApi) {
if (response == null) {
return;
}
this.type = response.type;
this.name = response.name;
this.value = response.value;

View File

@ -20,7 +20,11 @@ export class IdentityData {
passportNumber: string;
licenseNumber: string;
constructor(data: IdentityApi) {
constructor(data?: IdentityApi) {
if (data == null) {
return;
}
this.title = data.title;
this.firstName = data.firstName;
this.middleName = data.middleName;

View File

@ -9,7 +9,11 @@ export class LoginData {
password: string;
totp: string;
constructor(data: LoginApi) {
constructor(data?: LoginApi) {
if (data == null) {
return;
}
this.username = data.username;
this.password = data.password;
this.totp = data.totp;

View File

@ -6,7 +6,10 @@ export class LoginUriData {
uri: string;
match: UriMatchType = null;
constructor(data: LoginUriApi) {
constructor(data?: LoginUriApi) {
if (data == null) {
return;
}
this.uri = data.uri;
this.match = data.match;
}

View File

@ -5,7 +5,11 @@ import { SecureNoteApi } from '../api/secureNoteApi';
export class SecureNoteData {
type: SecureNoteType;
constructor(data: SecureNoteApi) {
constructor(data?: SecureNoteApi) {
if (data == null) {
return;
}
this.type = data.type;
}
}

View File

@ -32,4 +32,15 @@ export class Attachment extends Domain {
fileName: null,
}, orgId);
}
toAttachmentData(): AttachmentData {
const a = new AttachmentData();
this.buildDataModel(this, a, {
id: null,
url: null,
sizeName: null,
fileName: null,
}, ['id', 'url', 'sizeName']);
return a;
}
}

View File

@ -39,4 +39,17 @@ export class Card extends Domain {
code: null,
}, orgId);
}
toCardData(): CardData {
const c = new CardData();
this.buildDataModel(this, c, {
cardholderName: null,
brand: null,
number: null,
expMonth: null,
expYear: null,
code: null,
});
return c;
}
}

View File

@ -23,6 +23,7 @@ export class Cipher extends Domain {
favorite: boolean;
organizationUseTotp: boolean;
edit: boolean;
revisionDate: Date;
localData: any;
login: Login;
identity: Identity;
@ -40,16 +41,18 @@ export class Cipher extends Domain {
this.buildDomainModel(this, obj, {
id: null,
userId: null,
organizationId: null,
folderId: null,
name: null,
notes: null,
}, alreadyEncrypted, ['id', 'organizationId', 'folderId']);
}, alreadyEncrypted, ['id', 'userId', 'organizationId', 'folderId']);
this.type = obj.type;
this.favorite = obj.favorite;
this.organizationUseTotp = obj.organizationUseTotp;
this.edit = obj.edit;
this.revisionDate = obj.revisionDate;
this.collectionIds = obj.collectionIds;
this.localData = localData;
@ -142,4 +145,55 @@ export class Cipher extends Domain {
return model;
}
toCipherData(userId: string): CipherData {
const c = new CipherData();
c.id = this.id;
c.organizationId = this.organizationId;
c.folderId = this.folderId;
c.userId = this.organizationId != null ? userId : null;
c.edit = this.edit;
c.organizationUseTotp = this.organizationUseTotp;
c.favorite = this.favorite;
c.revisionDate = this.revisionDate;
c.type = this.type;
c.collectionIds = this.collectionIds;
this.buildDataModel(this, c, {
name: null,
notes: null,
});
switch (c.type) {
case CipherType.Login:
c.login = this.login.toLoginData();
break;
case CipherType.SecureNote:
c.secureNote = this.secureNote.toSecureNoteData();
break;
case CipherType.Card:
c.card = this.card.toCardData();
break;
case CipherType.Identity:
c.identity = this.identity.toIdentityData();
break;
default:
break;
}
if (this.fields != null) {
c.fields = [];
this.fields.forEach((field) => {
c.fields.push(field.toFieldData());
});
}
if (this.attachments != null) {
c.attachments = [];
this.attachments.forEach((attachment) => {
c.attachments.push(attachment.toAttachmentData());
});
}
return c;
}
}

View File

@ -18,6 +18,20 @@ export default abstract class Domain {
}
}
}
protected buildDataModel<D extends Domain>(domain: D, dataObj: any, map: any, notCipherStringList: any[] = []) {
for (const prop in map) {
if (!map.hasOwnProperty(prop)) {
continue;
}
const objProp = (domain as any)[(map[prop] || prop)];
if (notCipherStringList.indexOf(prop) > -1) {
(dataObj as any)[prop] = objProp != null ? objProp : null;
} else {
(dataObj as any)[prop] = objProp != null ? (objProp as CipherString).encryptedString : null;
}
}
}
protected async decryptObj<T extends View>(viewModel: T, map: any, orgId: string): Promise<T> {
const promises = [];

View File

@ -31,4 +31,14 @@ export class Field extends Domain {
value: null,
}, orgId);
}
toFieldData(): FieldData {
const f = new FieldData();
this.buildDataModel(this, f, {
name: null,
value: null,
type: null,
}, ['type']);
return f;
}
}

View File

@ -75,4 +75,29 @@ export class Identity extends Domain {
licenseNumber: null,
}, orgId);
}
toIdentityData(): IdentityData {
const i = new IdentityData();
this.buildDataModel(this, i, {
title: null,
firstName: null,
middleName: null,
lastName: null,
address1: null,
address2: null,
address3: null,
city: null,
state: null,
postalCode: null,
country: null,
company: null,
email: null,
phone: null,
ssn: null,
username: null,
passportNumber: null,
licenseNumber: null,
});
return i;
}
}

View File

@ -51,4 +51,22 @@ export class Login extends Domain {
return view;
}
toLoginData(): LoginData {
const l = new LoginData();
this.buildDataModel(this, l, {
username: null,
password: null,
totp: null,
});
if (this.uris != null && this.uris.length > 0) {
l.uris = [];
this.uris.forEach((u) => {
l.uris.push(u.toLoginUriData());
});
}
return l;
}
}

View File

@ -28,4 +28,12 @@ export class LoginUri extends Domain {
uri: null,
}, orgId);
}
toLoginUriData(): LoginUriData {
const u = new LoginUriData();
this.buildDataModel(this, u, {
uri: null,
}, ['match']);
return u;
}
}

View File

@ -21,4 +21,10 @@ export class SecureNote extends Domain {
decrypt(orgId: string): Promise<SecureNoteView> {
return Promise.resolve(new SecureNoteView(this));
}
toSecureNoteData(): SecureNoteData {
const n = new SecureNoteData();
n.type = this.type;
return n;
}
}

View File

@ -0,0 +1,7 @@
export class CipherBulkDeleteRequest {
ids: string[];
constructor(ids: string[]) {
this.ids = ids;
}
}

View File

@ -0,0 +1,9 @@
export class CipherBulkMoveRequest {
ids: string[];
folderId: string;
constructor(ids: string[], folderId: string) {
this.ids = ids;
this.folderId = folderId;
}
}

View File

@ -0,0 +1,7 @@
export class CipherCollectionsRequest {
collectionIds: string[];
constructor(collectionIds: string[]) {
this.collectionIds = collectionIds;
}
}

View File

@ -21,6 +21,7 @@ export class CipherRequest {
card: CardApi;
identity: IdentityApi;
fields: FieldApi[];
attachments: { [id: string]: string; };
constructor(cipher: Cipher) {
this.type = cipher.type;
@ -101,5 +102,12 @@ export class CipherRequest {
});
});
}
if (cipher.attachments) {
this.attachments = {};
cipher.attachments.forEach((attachment) => {
this.attachments[attachment.id] = attachment.fileName ? attachment.fileName.encryptedString : null;
});
}
}
}

View File

@ -0,0 +1,13 @@
import { CipherRequest } from './cipherRequest';
import { Cipher } from '../domain/cipher';
export class CipherShareRequest {
cipher: CipherRequest;
collectionIds: string[];
constructor(cipher: Cipher) {
this.cipher = new CipherRequest(cipher);
this.collectionIds = cipher.collectionIds;
}
}

View File

@ -21,7 +21,7 @@ export class CipherResponse {
favorite: boolean;
edit: boolean;
organizationUseTotp: boolean;
revisionDate: string;
revisionDate: Date;
attachments: AttachmentResponse[];
collectionIds: string[];
@ -35,7 +35,7 @@ export class CipherResponse {
this.favorite = response.Favorite;
this.edit = response.Edit;
this.organizationUseTotp = response.OrganizationUseTotp;
this.revisionDate = response.RevisionDate;
this.revisionDate = new Date(response.RevisionDate);
if (response.Login != null) {
this.login = new LoginApi(response.Login);

View File

@ -7,6 +7,7 @@ import { TokenService } from '../abstractions/token.service';
import { EnvironmentUrls } from '../models/domain/environmentUrls';
import { CipherRequest } from '../models/request/cipherRequest';
import { CipherShareRequest } from '../models/request/cipherShareRequest';
import { FolderRequest } from '../models/request/folderRequest';
import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest';
import { PasswordHintRequest } from '../models/request/passwordHintRequest';
@ -334,6 +335,27 @@ export class ApiService implements ApiServiceAbstraction {
}
}
async shareCipher(id: string, request: CipherShareRequest): Promise<any> {
const authHeader = await this.handleTokenState();
const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/share', {
body: JSON.stringify(request),
cache: 'no-cache',
credentials: this.getCredentials(),
headers: new Headers({
'Accept': 'application/json',
'Authorization': authHeader,
'Content-Type': 'application/json; charset=utf-8',
'Device-Type': this.deviceType,
}),
method: 'PUT',
}));
if (response.status !== 200) {
const error = await this.handleError(response, false);
return Promise.reject(error);
}
}
async deleteCipher(id: string): Promise<any> {
const authHeader = await this.handleTokenState();
const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, {
@ -377,6 +399,28 @@ export class ApiService implements ApiServiceAbstraction {
}
}
async shareCipherAttachment(id: string, attachmentId: string, data: FormData,
organizationId: string): Promise<any> {
const authHeader = await this.handleTokenState();
const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' +
attachmentId + '/share?organizationId=' + organizationId, {
body: data,
cache: 'no-cache',
credentials: this.getCredentials(),
headers: new Headers({
'Accept': 'application/json',
'Authorization': authHeader,
'Device-Type': this.deviceType,
}),
method: 'POST',
}));
if (response.status !== 200) {
const error = await this.handleError(response, false);
return Promise.reject(error);
}
}
async deleteCipherAttachment(id: string, attachmentId: string): Promise<any> {
const authHeader = await this.handleTokenState();
const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, {

View File

@ -3,6 +3,7 @@ import { UriMatchType } from '../enums/uriMatchType';
import { CipherData } from '../models/data/cipherData';
import { Attachment } from '../models/domain/attachment';
import { Card } from '../models/domain/card';
import { Cipher } from '../models/domain/cipher';
import { CipherString } from '../models/domain/cipherString';
@ -19,6 +20,7 @@ import { CipherRequest } from '../models/request/cipherRequest';
import { CipherResponse } from '../models/response/cipherResponse';
import { ErrorResponse } from '../models/response/errorResponse';
import { AttachmentView } from '../models/view/attachmentView';
import { CardView } from '../models/view/cardView';
import { CipherView } from '../models/view/cipherView';
import { FieldView } from '../models/view/fieldView';
@ -38,6 +40,7 @@ import { StorageService } from '../abstractions/storage.service';
import { UserService } from '../abstractions/user.service';
import { Utils } from '../misc/utils';
import { CipherShareRequest } from '../models/request/cipherShareRequest';
const Keys = {
ciphersPrefix: 'ciphers_',
@ -77,11 +80,39 @@ export class CipherService implements CipherServiceAbstraction {
this.encryptFields(model.fields, key).then((fields) => {
cipher.fields = fields;
}),
this.encryptAttachments(model.attachments, key).then((attachments) => {
cipher.attachments = attachments;
}),
]);
return cipher;
}
async encryptAttachments(attachmentsModel: AttachmentView[], key: SymmetricCryptoKey): Promise<Attachment[]> {
if (attachmentsModel == null || attachmentsModel.length === 0) {
return null;
}
const promises: Array<Promise<any>> = [];
const encAttachments: Attachment[] = [];
attachmentsModel.forEach(async (model) => {
const attachment = new Attachment();
attachment.id = model.id;
attachment.size = model.size;
attachment.sizeName = model.sizeName;
attachment.url = model.url;
const promise = this.encryptObjProperty(model, attachment, {
fileName: null,
}, key).then(() => {
encAttachments.push(attachment);
});
promises.push(promise);
});
await Promise.all(promises);
return encAttachments;
}
async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]> {
if (!fieldsModel || !fieldsModel.length) {
return null;
@ -324,6 +355,49 @@ export class CipherService implements CipherServiceAbstraction {
await this.upsert(data);
}
async shareWithServer(cipher: Cipher): Promise<any> {
const request = new CipherShareRequest(cipher);
await this.apiService.shareCipher(cipher.id, request);
const userId = await this.userService.getUserId();
await this.upsert(cipher.toCipherData(userId));
}
async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string,
organizationId: string): Promise<any> {
const attachmentResponse = await fetch(new Request(attachmentView.url, { cache: 'no-cache' }));
if (attachmentResponse.status !== 200) {
throw Error('Failed to download attachment: ' + attachmentResponse.status.toString());
}
const buf = await attachmentResponse.arrayBuffer();
const decBuf = await this.cryptoService.decryptFromBytes(buf, null);
const key = await this.cryptoService.getOrgKey(organizationId);
const encData = await this.cryptoService.encryptToBytes(decBuf, key);
const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key);
const fd = new FormData();
try {
const blob = new Blob([encData], { type: 'application/octet-stream' });
fd.append('data', blob, encFileName.encryptedString);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append('data', new Buffer(encData) as any, {
filepath: encFileName.encryptedString,
contentType: 'application/octet-stream',
} as any);
} else {
throw e;
}
}
let response: CipherResponse;
try {
response = await this.apiService.shareCipherAttachment(cipherId, attachmentView.id, fd, organizationId);
} catch (e) {
throw new Error((e as ErrorResponse).getSingleMessage());
}
}
saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise<Cipher> {
return new Promise((resolve, reject) => {
const reader = new FileReader();