|
|
|
@ -42,8 +42,8 @@ function hmacToHex(hmac: byte[]): string {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class Credentials {
|
|
|
|
|
email: string;
|
|
|
|
|
authToken: string;
|
|
|
|
|
public email: string;
|
|
|
|
|
public authToken: string;
|
|
|
|
|
|
|
|
|
|
constructor(email: string, authToken: string) {
|
|
|
|
|
this.email = email;
|
|
|
|
@ -52,11 +52,11 @@ export class Credentials {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class CollectionInfo {
|
|
|
|
|
uid: string;
|
|
|
|
|
type: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
description: string;
|
|
|
|
|
color: number;
|
|
|
|
|
public uid: string;
|
|
|
|
|
public type: string;
|
|
|
|
|
public displayName: string;
|
|
|
|
|
public description: string;
|
|
|
|
|
public color: number;
|
|
|
|
|
|
|
|
|
|
constructor(json?: any) {
|
|
|
|
|
CastJson(json, this);
|
|
|
|
@ -76,13 +76,13 @@ class BaseItem<T extends BaseItemJson> {
|
|
|
|
|
this._json = {} as any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deserialize(json: T) {
|
|
|
|
|
public deserialize(json: T) {
|
|
|
|
|
this._json = Object.assign({}, json);
|
|
|
|
|
this._encrypted = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(json.content));
|
|
|
|
|
this._content = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serialize(): T {
|
|
|
|
|
public serialize(): T {
|
|
|
|
|
return Object.assign(
|
|
|
|
|
{},
|
|
|
|
|
this._json,
|
|
|
|
@ -140,33 +140,33 @@ export class Journal extends BaseJournal<JournalJson> {
|
|
|
|
|
return this._json.version;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setInfo(cryptoManager: CryptoManager, info: CollectionInfo) {
|
|
|
|
|
public setInfo(cryptoManager: CryptoManager, info: CollectionInfo) {
|
|
|
|
|
this._json.uid = info.uid;
|
|
|
|
|
this._content = info;
|
|
|
|
|
const encrypted = cryptoManager.encrypt(JSON.stringify(this._content));
|
|
|
|
|
this._encrypted = this.calculateHmac(cryptoManager, encrypted).concat(encrypted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getInfo(cryptoManager: CryptoManager): CollectionInfo {
|
|
|
|
|
public getInfo(cryptoManager: CryptoManager): CollectionInfo {
|
|
|
|
|
this.verify(cryptoManager);
|
|
|
|
|
|
|
|
|
|
if (this._content === undefined) {
|
|
|
|
|
this._content = JSON.parse(cryptoManager.decrypt(this.encryptedContent()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ret = new CollectionInfo(this._content);
|
|
|
|
|
const ret = new CollectionInfo(this._content);
|
|
|
|
|
ret.uid = this.uid;
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
calculateHmac(cryptoManager: CryptoManager, encrypted: byte[]): byte[] {
|
|
|
|
|
let prefix = stringToByteArray(this.uid);
|
|
|
|
|
public calculateHmac(cryptoManager: CryptoManager, encrypted: byte[]): byte[] {
|
|
|
|
|
const prefix = stringToByteArray(this.uid);
|
|
|
|
|
return cryptoManager.hmac(prefix.concat(encrypted));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verify(cryptoManager: CryptoManager) {
|
|
|
|
|
let calculated = this.calculateHmac(cryptoManager, this.encryptedContent());
|
|
|
|
|
let hmac = this._encrypted.slice(0, HMAC_SIZE_BYTES);
|
|
|
|
|
public verify(cryptoManager: CryptoManager) {
|
|
|
|
|
const calculated = this.calculateHmac(cryptoManager, this.encryptedContent());
|
|
|
|
|
const hmac = this._encrypted.slice(0, HMAC_SIZE_BYTES);
|
|
|
|
|
|
|
|
|
|
super.verifyBase(hmac, calculated);
|
|
|
|
|
}
|
|
|
|
@ -183,9 +183,9 @@ export enum SyncEntryAction {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class SyncEntry {
|
|
|
|
|
uid?: string;
|
|
|
|
|
action: SyncEntryAction;
|
|
|
|
|
content: string;
|
|
|
|
|
public uid?: string;
|
|
|
|
|
public action: SyncEntryAction;
|
|
|
|
|
public content: string;
|
|
|
|
|
|
|
|
|
|
constructor(json?: any, uid?: string) {
|
|
|
|
|
CastJson(json, this);
|
|
|
|
@ -193,17 +193,16 @@ export class SyncEntry {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EntryJson extends BaseJson {
|
|
|
|
|
}
|
|
|
|
|
export type EntryJson = BaseJson;
|
|
|
|
|
|
|
|
|
|
export class Entry extends BaseJournal<EntryJson> {
|
|
|
|
|
setSyncEntry(cryptoManager: CryptoManager, info: SyncEntry, prevUid: string | null) {
|
|
|
|
|
public setSyncEntry(cryptoManager: CryptoManager, info: SyncEntry, prevUid: string | null) {
|
|
|
|
|
this._content = info;
|
|
|
|
|
this._encrypted = cryptoManager.encrypt(JSON.stringify(this._content));
|
|
|
|
|
this._json.uid = hmacToHex(this.calculateHmac(cryptoManager, this._encrypted, prevUid));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSyncEntry(cryptoManager: CryptoManager, prevUid: string | null): SyncEntry {
|
|
|
|
|
public getSyncEntry(cryptoManager: CryptoManager, prevUid: string | null): SyncEntry {
|
|
|
|
|
this.verify(cryptoManager, prevUid);
|
|
|
|
|
|
|
|
|
|
if (this._content === undefined) {
|
|
|
|
@ -213,15 +212,15 @@ export class Entry extends BaseJournal<EntryJson> {
|
|
|
|
|
return new SyncEntry(this._content, this.uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verify(cryptoManager: CryptoManager, prevUid: string | null) {
|
|
|
|
|
let calculated = this.calculateHmac(cryptoManager, this._encrypted, prevUid);
|
|
|
|
|
let hmac = sjcl.codec.bytes.fromBits(sjcl.codec.hex.toBits(this.uid));
|
|
|
|
|
public verify(cryptoManager: CryptoManager, prevUid: string | null) {
|
|
|
|
|
const calculated = this.calculateHmac(cryptoManager, this._encrypted, prevUid);
|
|
|
|
|
const hmac = sjcl.codec.bytes.fromBits(sjcl.codec.hex.toBits(this.uid));
|
|
|
|
|
|
|
|
|
|
super.verifyBase(hmac, calculated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private calculateHmac(cryptoManager: CryptoManager, encrypted: byte[], prevUid: string | null): byte[] {
|
|
|
|
|
let prefix = (prevUid !== null) ? stringToByteArray(prevUid) : [];
|
|
|
|
|
const prefix = (prevUid !== null) ? stringToByteArray(prevUid) : [];
|
|
|
|
|
return cryptoManager.hmac(prefix.concat(encrypted));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -233,7 +232,7 @@ export interface UserInfoJson extends BaseItemJson {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class UserInfo extends BaseItem<UserInfoJson> {
|
|
|
|
|
_owner: string;
|
|
|
|
|
public _owner: string;
|
|
|
|
|
|
|
|
|
|
constructor(owner: string, version: number = Constants.CURRENT_VERSION) {
|
|
|
|
|
super();
|
|
|
|
@ -253,20 +252,20 @@ export class UserInfo extends BaseItem<UserInfoJson> {
|
|
|
|
|
return this._json.pubkey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serialize(): UserInfoJson {
|
|
|
|
|
let ret = super.serialize();
|
|
|
|
|
public serialize(): UserInfoJson {
|
|
|
|
|
const ret = super.serialize();
|
|
|
|
|
ret.owner = this._owner;
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setKeyPair(cryptoManager: CryptoManager, keyPair: AsymmetricKeyPair) {
|
|
|
|
|
public setKeyPair(cryptoManager: CryptoManager, keyPair: AsymmetricKeyPair) {
|
|
|
|
|
this._json.pubkey = sjcl.codec.base64.fromBits(sjcl.codec.bytes.toBits(keyPair.publicKey));
|
|
|
|
|
this._content = keyPair.privateKey;
|
|
|
|
|
const encrypted = cryptoManager.encryptBytes(keyPair.privateKey);
|
|
|
|
|
this._encrypted = this.calculateHmac(cryptoManager, encrypted).concat(encrypted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getKeyPair(cryptoManager: CryptoManager): AsymmetricKeyPair {
|
|
|
|
|
public getKeyPair(cryptoManager: CryptoManager): AsymmetricKeyPair {
|
|
|
|
|
this.verify(cryptoManager);
|
|
|
|
|
|
|
|
|
|
if (this._content === undefined) {
|
|
|
|
@ -277,14 +276,14 @@ export class UserInfo extends BaseItem<UserInfoJson> {
|
|
|
|
|
return new AsymmetricKeyPair(pubkey, this._content as byte[]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
calculateHmac(cryptoManager: CryptoManager, encrypted: byte[]): byte[] {
|
|
|
|
|
let postfix = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(this._json.pubkey));
|
|
|
|
|
public calculateHmac(cryptoManager: CryptoManager, encrypted: byte[]): byte[] {
|
|
|
|
|
const postfix = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(this._json.pubkey));
|
|
|
|
|
return cryptoManager.hmac(encrypted.concat(postfix));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verify(cryptoManager: CryptoManager) {
|
|
|
|
|
let calculated = this.calculateHmac(cryptoManager, this.encryptedContent());
|
|
|
|
|
let hmac = this._encrypted.slice(0, HMAC_SIZE_BYTES);
|
|
|
|
|
public verify(cryptoManager: CryptoManager) {
|
|
|
|
|
const calculated = this.calculateHmac(cryptoManager, this.encryptedContent());
|
|
|
|
|
const hmac = this._encrypted.slice(0, HMAC_SIZE_BYTES);
|
|
|
|
|
|
|
|
|
|
super.verifyBase(hmac, calculated);
|
|
|
|
|
}
|
|
|
|
@ -297,9 +296,8 @@ export class UserInfo extends BaseItem<UserInfoJson> {
|
|
|
|
|
// FIXME: baseUrl and apiBase should be the right type all around.
|
|
|
|
|
|
|
|
|
|
class BaseNetwork {
|
|
|
|
|
apiBase: any; // FIXME
|
|
|
|
|
|
|
|
|
|
static urlExtend(_baseUrl: URL, segments: Array<string>): URL {
|
|
|
|
|
public static urlExtend(_baseUrl: URL, segments: string[]): URL {
|
|
|
|
|
let baseUrl = _baseUrl as any;
|
|
|
|
|
baseUrl = baseUrl.clone();
|
|
|
|
|
for (const segment of segments) {
|
|
|
|
@ -307,18 +305,19 @@ class BaseNetwork {
|
|
|
|
|
}
|
|
|
|
|
return baseUrl.normalize();
|
|
|
|
|
}
|
|
|
|
|
public apiBase: any; // FIXME
|
|
|
|
|
|
|
|
|
|
constructor(apiBase: string) {
|
|
|
|
|
this.apiBase = URI(apiBase).normalize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME: Get the correct type for extra
|
|
|
|
|
newCall(segments: Array<string> = [], extra: any = {}, _apiBase: URL = this.apiBase): Promise<{} | Array<any>> {
|
|
|
|
|
let apiBase = BaseNetwork.urlExtend(_apiBase, segments);
|
|
|
|
|
public newCall(segments: string[] = [], extra: any = {}, _apiBase: URL = this.apiBase): Promise<{} | any[]> {
|
|
|
|
|
const apiBase = BaseNetwork.urlExtend(_apiBase, segments);
|
|
|
|
|
|
|
|
|
|
extra = Object.assign({}, extra);
|
|
|
|
|
extra.headers = Object.assign(
|
|
|
|
|
{ 'Accept': 'application/json' },
|
|
|
|
|
{ Accept: 'application/json' },
|
|
|
|
|
extra.headers);
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
@ -358,17 +357,17 @@ export class Authenticator extends BaseNetwork {
|
|
|
|
|
this.apiBase = BaseNetwork.urlExtend(this.apiBase, ['api-token-auth', '']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAuthToken(username: string, password: string): Promise<string> {
|
|
|
|
|
public getAuthToken(username: string, password: string): Promise<string> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
// FIXME: should be FormData but doesn't work for whatever reason
|
|
|
|
|
let form = 'username=' + encodeURIComponent(username) +
|
|
|
|
|
const form = 'username=' + encodeURIComponent(username) +
|
|
|
|
|
'&password=' + encodeURIComponent(password);
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'post',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
|
|
|
},
|
|
|
|
|
body: form
|
|
|
|
|
body: form,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.newCall([], extra).then((json: {token: string}) => {
|
|
|
|
@ -383,19 +382,19 @@ export class Authenticator extends BaseNetwork {
|
|
|
|
|
export class BaseManager extends BaseNetwork {
|
|
|
|
|
protected credentials: Credentials;
|
|
|
|
|
|
|
|
|
|
constructor(credentials: Credentials, apiBase: string, segments: Array<string>) {
|
|
|
|
|
constructor(credentials: Credentials, apiBase: string, segments: string[]) {
|
|
|
|
|
super(apiBase);
|
|
|
|
|
this.credentials = credentials;
|
|
|
|
|
this.apiBase = BaseNetwork.urlExtend(this.apiBase, ['api', 'v1'].concat(segments));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME: Get the correct type for extra
|
|
|
|
|
newCall(segments: Array<string> = [], extra: any = {}, apiBase: any = this.apiBase): Promise<{} | Array<any>> {
|
|
|
|
|
public newCall(segments: string[] = [], extra: any = {}, apiBase: any = this.apiBase): Promise<{} | any[]> {
|
|
|
|
|
extra = Object.assign({}, extra);
|
|
|
|
|
extra.headers = Object.assign(
|
|
|
|
|
{
|
|
|
|
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
|
|
|
'Authorization': 'Token ' + this.credentials.authToken
|
|
|
|
|
'Authorization': 'Token ' + this.credentials.authToken,
|
|
|
|
|
},
|
|
|
|
|
extra.headers);
|
|
|
|
|
|
|
|
|
@ -408,10 +407,10 @@ export class JournalManager extends BaseManager {
|
|
|
|
|
super(credentials, apiBase, ['journals', '']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetch(journalUid: string): Promise<Journal> {
|
|
|
|
|
public fetch(journalUid: string): Promise<Journal> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.newCall([journalUid, '']).then((json: JournalJson) => {
|
|
|
|
|
let journal = new Journal(json.version);
|
|
|
|
|
const journal = new Journal(json.version);
|
|
|
|
|
journal.deserialize(json);
|
|
|
|
|
resolve(journal);
|
|
|
|
|
}).catch((error: Error) => {
|
|
|
|
@ -420,11 +419,11 @@ export class JournalManager extends BaseManager {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list(): Promise<Journal[]> {
|
|
|
|
|
public list(): Promise<Journal[]> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.newCall().then((json: Array<{}>) => {
|
|
|
|
|
resolve(json.map((val: JournalJson) => {
|
|
|
|
|
let journal = new Journal(val.version);
|
|
|
|
|
const journal = new Journal(val.version);
|
|
|
|
|
journal.deserialize(val);
|
|
|
|
|
return journal;
|
|
|
|
|
}));
|
|
|
|
@ -434,7 +433,7 @@ export class JournalManager extends BaseManager {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create(journal: Journal): Promise<{}> {
|
|
|
|
|
public create(journal: Journal): Promise<{}> {
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'post',
|
|
|
|
|
body: JSON.stringify(journal.serialize()),
|
|
|
|
@ -443,7 +442,7 @@ export class JournalManager extends BaseManager {
|
|
|
|
|
return this.newCall([], extra);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(journal: Journal): Promise<{}> {
|
|
|
|
|
public update(journal: Journal): Promise<{}> {
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'put',
|
|
|
|
|
body: JSON.stringify(journal.serialize()),
|
|
|
|
@ -452,7 +451,7 @@ export class JournalManager extends BaseManager {
|
|
|
|
|
return this.newCall([journal.uid, ''], extra);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete(journal: Journal): Promise<{}> {
|
|
|
|
|
public delete(journal: Journal): Promise<{}> {
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'delete',
|
|
|
|
|
};
|
|
|
|
@ -466,7 +465,7 @@ export class EntryManager extends BaseManager {
|
|
|
|
|
super(credentials, apiBase, ['journals', journalId, 'entries', '']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list(lastUid: string | null, limit: number = 0): Promise<Entry[]> {
|
|
|
|
|
public list(lastUid: string | null, limit: number = 0): Promise<Entry[]> {
|
|
|
|
|
let apiBase = this.apiBase.clone();
|
|
|
|
|
apiBase = apiBase.search({
|
|
|
|
|
last: (lastUid !== null) ? lastUid : undefined,
|
|
|
|
@ -476,7 +475,7 @@ export class EntryManager extends BaseManager {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.newCall(undefined, undefined, apiBase).then((json: Array<{}>) => {
|
|
|
|
|
resolve(json.map((val: any) => {
|
|
|
|
|
let entry = new Entry();
|
|
|
|
|
const entry = new Entry();
|
|
|
|
|
entry.deserialize(val);
|
|
|
|
|
return entry;
|
|
|
|
|
}));
|
|
|
|
@ -486,7 +485,7 @@ export class EntryManager extends BaseManager {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create(entries: Entry[], lastUid: string | null): Promise<{}> {
|
|
|
|
|
public create(entries: Entry[], lastUid: string | null): Promise<{}> {
|
|
|
|
|
let apiBase = this.apiBase.clone();
|
|
|
|
|
apiBase = apiBase.search({
|
|
|
|
|
last: (lastUid !== null) ? lastUid : undefined,
|
|
|
|
@ -511,7 +510,7 @@ export class JournalMembersManager extends BaseManager {
|
|
|
|
|
super(credentials, apiBase, ['journals', journalId, 'members', '']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list(): Promise<JournalMemberJson[]> {
|
|
|
|
|
public list(): Promise<JournalMemberJson[]> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.newCall().then((json: Array<{}>) => {
|
|
|
|
|
resolve(json.map((val: JournalMemberJson) => {
|
|
|
|
@ -529,10 +528,10 @@ export class UserInfoManager extends BaseManager {
|
|
|
|
|
super(credentials, apiBase, ['user', '']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fetch(owner: string): Promise<UserInfo> {
|
|
|
|
|
public fetch(owner: string): Promise<UserInfo> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.newCall([owner, '']).then((json: UserInfoJson) => {
|
|
|
|
|
let userInfo = new UserInfo(owner, json.version);
|
|
|
|
|
const userInfo = new UserInfo(owner, json.version);
|
|
|
|
|
userInfo.deserialize(json);
|
|
|
|
|
resolve(userInfo);
|
|
|
|
|
}).catch((error: Error) => {
|
|
|
|
@ -541,7 +540,7 @@ export class UserInfoManager extends BaseManager {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create(userInfo: UserInfo): Promise<{}> {
|
|
|
|
|
public create(userInfo: UserInfo): Promise<{}> {
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'post',
|
|
|
|
|
body: JSON.stringify(userInfo.serialize()),
|
|
|
|
@ -550,7 +549,7 @@ export class UserInfoManager extends BaseManager {
|
|
|
|
|
return this.newCall([], extra);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(userInfo: UserInfo): Promise<{}> {
|
|
|
|
|
public update(userInfo: UserInfo): Promise<{}> {
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'put',
|
|
|
|
|
body: JSON.stringify(userInfo.serialize()),
|
|
|
|
@ -559,7 +558,7 @@ export class UserInfoManager extends BaseManager {
|
|
|
|
|
return this.newCall([userInfo.owner, ''], extra);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete(userInfo: UserInfo): Promise<{}> {
|
|
|
|
|
public delete(userInfo: UserInfo): Promise<{}> {
|
|
|
|
|
const extra = {
|
|
|
|
|
method: 'delete',
|
|
|
|
|
};
|
|
|
|
|