Move the etesync-js API to its own repo.

master
Tom Hacohen 5 years ago
parent f3de2a641a
commit c7d4a15f5f

@ -5,11 +5,11 @@
"dependencies": { "dependencies": {
"@material-ui/core": "^3.9.2", "@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.2", "@material-ui/icons": "^3.0.2",
"etesync": "^0.1.1",
"ical.js": "^1.2.2", "ical.js": "^1.2.2",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
"localforage": "^1.7.3", "localforage": "^1.7.3",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-rsa": "^1.0.3",
"react": "^16.10.2", "react": "^16.10.2",
"react-big-calendar": "^0.20.3", "react-big-calendar": "^0.20.3",
"react-datetime": "^2.16.3", "react-datetime": "^2.16.3",
@ -25,8 +25,6 @@
"redux-persist": "^5.10.0", "redux-persist": "^5.10.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"reselect": "^3.0.1", "reselect": "^3.0.1",
"sjcl": "git+https://github.com/etesync/sjcl",
"urijs": "^1.19.1",
"uuid": "^3.1.0" "uuid": "^3.1.0"
}, },
"scripts": { "scripts": {

@ -16,7 +16,7 @@ import { SyncInfoJournal } from '../SyncGate';
import { store, CredentialsData, UserInfoData } from '../store'; import { store, CredentialsData, UserInfoData } from '../store';
import { addEntries } from '../store/actions'; import { addEntries } from '../store/actions';
import { createJournalEntry } from '../etesync-helpers'; import { createJournalEntry } from '../etesync-helpers';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import * as ICAL from 'ical.js'; import * as ICAL from 'ical.js';
import { ContactType, EventType, TaskType, PimType } from '../pim-types'; import { ContactType, EventType, TaskType, PimType } from '../pim-types';

@ -15,7 +15,7 @@ import AppBarOverride from '../widgets/AppBarOverride';
import Container from '../widgets/Container'; import Container from '../widgets/Container';
import ConfirmationDialog from '../widgets/ConfirmationDialog'; import ConfirmationDialog from '../widgets/ConfirmationDialog';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { SyncInfo } from '../SyncGate'; import { SyncInfo } from '../SyncGate';
import { handleInputChange } from '../helpers'; import { handleInputChange } from '../helpers';

@ -6,7 +6,7 @@ import LoadingIndicator from '../widgets/LoadingIndicator';
import ConfirmationDialog from '../widgets/ConfirmationDialog'; import ConfirmationDialog from '../widgets/ConfirmationDialog';
import PrettyFingerprint from '../widgets/PrettyFingerprint'; import PrettyFingerprint from '../widgets/PrettyFingerprint';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { CredentialsData } from '../store'; import { CredentialsData } from '../store';
import { handleInputChange } from '../helpers'; import { handleInputChange } from '../helpers';

@ -13,7 +13,7 @@ import ConfirmationDialog from '../widgets/ConfirmationDialog';
import JournalMemberAddDialog from './JournalMemberAddDialog'; import JournalMemberAddDialog from './JournalMemberAddDialog';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { CredentialsData, UserInfoData } from '../store'; import { CredentialsData, UserInfoData } from '../store';
import { SyncInfoJournal } from '../SyncGate'; import { SyncInfoJournal } from '../SyncGate';

@ -13,7 +13,7 @@ import { store, JournalsData, UserInfoData, CredentialsData } from '../store';
import { addJournal, deleteJournal, updateJournal } from '../store/actions'; import { addJournal, deleteJournal, updateJournal } from '../store/actions';
import { SyncInfo } from '../SyncGate'; import { SyncInfo } from '../SyncGate';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
class Journals extends React.PureComponent { class Journals extends React.PureComponent {
public props: { public props: {

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Route, Switch, withRouter } from 'react-router'; import { Route, Switch, withRouter } from 'react-router';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { routeResolver } from '../App'; import { routeResolver } from '../App';

@ -9,7 +9,7 @@ import { RouteComponentProps, withRouter } from 'react-router';
import { Action } from 'redux-actions'; import { Action } from 'redux-actions';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';

@ -19,8 +19,8 @@ import Journals from './Journals';
import Settings from './Settings'; import Settings from './Settings';
import Pim from './Pim'; import Pim from './Pim';
import * as EteSync from './api/EteSync'; import * as EteSync from 'etesync';
import { CURRENT_VERSION } from './api/Constants'; import { CURRENT_VERSION } from 'etesync';
import { store, SettingsType, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoType } from './store'; import { store, SettingsType, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoType } from './store';
import { addJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions'; import { addJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions';

@ -1 +0,0 @@
export const CURRENT_VERSION = 2;

@ -1,41 +0,0 @@
import { CryptoManager, AsymmetricCryptoManager, deriveKey } from './Crypto';
import { USER, PASSWORD, keyBase64 } from './TestConstants';
import { stringToByteArray } from './Helpers';
import sjcl from 'sjcl';
sjcl.random.addEntropy('seedForTheTests', 1024, 'FakeSeed');
it('Derive key', () => {
const derived = deriveKey(USER, PASSWORD);
expect(derived).toBe(keyBase64);
});
it('Symmetric encryption v1', () => {
const cryptoManager = new CryptoManager(keyBase64, 'TestSaltShouldBeJournalId', 1);
const clearText = 'This Is Some Test Cleartext.';
const cipher = cryptoManager.encrypt(clearText);
expect(clearText).toBe(cryptoManager.decrypt(cipher));
const expected = 'Lz+HUFzh1HdjxuGdQrBwBG1IzHT0ug6mO8fwePSbXtc=';
expect(expected).toBe(cryptoManager.hmac64(stringToByteArray('Some test data')));
});
it('Symmetric encryption v2', () => {
const cryptoManager = new CryptoManager(keyBase64, 'TestSaltShouldBeJournalId', 2);
const clearText = 'This Is Some Test Cleartext.';
const cipher = cryptoManager.encrypt(clearText);
expect(clearText).toBe(cryptoManager.decrypt(cipher));
const expected = 'XQ/A0gentOaE98R9wzf3zEIAHj4OH1GF8J4C6JiJupo=';
expect(expected).toBe(cryptoManager.hmac64(stringToByteArray('Some test data')));
});
it('Asymmetric encryption', () => {
const keyPair = AsymmetricCryptoManager.generateKeyPair();
const cryptoManager = new AsymmetricCryptoManager(keyPair);
const clearText = [1, 2, 4, 5];
const cipher = cryptoManager.encryptBytes(keyPair.publicKey, clearText);
expect(clearText).toEqual(cryptoManager.decryptBytes(cipher));
});

@ -1,165 +0,0 @@
import sjcl from 'sjcl';
import NodeRSA from 'node-rsa';
import * as Constants from './Constants';
import { byte, base64 } from './Helpers';
(sjcl as any).beware['CBC mode is dangerous because it doesn\'t protect message integrity.']();
export const HMAC_SIZE_BYTES = 32;
export class AsymmetricKeyPair {
public publicKey: byte[];
public privateKey: byte[];
constructor(publicKey: byte[], privateKey: byte[]) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
}
export function deriveKey(salt: string, password: string): string {
const keySize = 190 * 8;
return sjcl.codec.base64.fromBits((sjcl.misc as any).scrypt(password, salt, 16384, 8, 1, keySize));
}
export function genUid() {
const rand = sjcl.random.randomWords(4);
return sjcl.codec.hex.fromBits(hmac256(rand, rand));
}
function hmac256(salt: sjcl.BitArray, key: sjcl.BitArray) {
const hmac = new sjcl.misc.hmac(salt);
return hmac.encrypt(key);
}
export class CryptoManager {
public static fromDerivedKey(key: byte[], version: number = Constants.CURRENT_VERSION) {
// FIXME: Cleanup this hack
const ret = new CryptoManager('', '', version);
ret.key = sjcl.codec.bytes.toBits(key);
ret._updateDerivedKeys();
return ret;
}
public version: number;
public key: sjcl.BitArray;
public cipherKey: sjcl.BitArray;
public hmacKey: sjcl.BitArray;
public cipherWords = 4;
constructor(_keyBase64: base64, salt: string, version: number = Constants.CURRENT_VERSION) {
this.version = version;
const key = sjcl.codec.base64.toBits(_keyBase64);
// FIXME: Clean up all exeptions
if (version > Constants.CURRENT_VERSION) {
throw new Error('VersionTooNewException');
} else if (version === 1) {
this.key = key;
} else {
this.key = hmac256(sjcl.codec.utf8String.toBits(salt), key);
}
this._updateDerivedKeys();
}
public _updateDerivedKeys() {
this.cipherKey = hmac256(sjcl.codec.utf8String.toBits('aes'), this.key);
this.hmacKey = hmac256(sjcl.codec.utf8String.toBits('hmac'), this.key);
}
public encryptBits(content: sjcl.BitArray): byte[] {
const iv = sjcl.random.randomWords(this.cipherWords);
const prp = new sjcl.cipher.aes(this.cipherKey);
const cipherText = sjcl.mode.cbc.encrypt(prp, content, iv);
return sjcl.codec.bytes.fromBits(iv.concat(cipherText));
}
public decryptBits(content: byte[]): sjcl.BitArray {
const cipherText = sjcl.codec.bytes.toBits(content);
const iv = cipherText.splice(0, this.cipherWords);
const prp = new sjcl.cipher.aes(this.cipherKey);
const clearText = sjcl.mode.cbc.decrypt(prp, cipherText, iv);
return clearText;
}
public encryptBytes(content: byte[]): byte[] {
return this.encryptBits(sjcl.codec.bytes.toBits(content));
}
public decryptBytes(content: byte[]): byte[] {
return sjcl.codec.bytes.fromBits(this.decryptBits(content));
}
public encrypt(content: string): byte[] {
return this.encryptBits(sjcl.codec.utf8String.toBits(content));
}
public decrypt(content: byte[]): string {
return sjcl.codec.utf8String.fromBits(this.decryptBits(content));
}
public getEncryptedKey(keyPair: AsymmetricKeyPair, publicKey: byte[]) {
const cryptoManager = new AsymmetricCryptoManager(keyPair);
return cryptoManager.encryptBytes(publicKey, sjcl.codec.bytes.fromBits(this.key));
}
public hmac(content: byte[]): byte[] {
return sjcl.codec.bytes.fromBits(this.hmacBase(content));
}
public hmac64(content: byte[]): base64 {
return sjcl.codec.base64.fromBits(this.hmacBase(content));
}
public hmacHex(content: byte[]): string {
return sjcl.codec.hex.fromBits(this.hmacBase(content));
}
private hmacBase(_content: byte[]): sjcl.BitArray {
let content;
if (this.version === 1) {
content = sjcl.codec.bytes.toBits(_content);
} else {
content = sjcl.codec.bytes.toBits(_content.concat([this.version]));
}
return hmac256(this.hmacKey, content);
}
}
function bufferToArray(buffer: Buffer) {
return Array.prototype.slice.call(buffer);
}
export class AsymmetricCryptoManager {
public static generateKeyPair() {
const keyPair = new NodeRSA();
keyPair.generateKeyPair(3072, 65537);
const pubkey = keyPair.exportKey('pkcs8-public-der') as Buffer;
const privkey = keyPair.exportKey('pkcs8-private-der') as Buffer;
return new AsymmetricKeyPair(
bufferToArray(pubkey), bufferToArray(privkey));
}
public keyPair: NodeRSA;
constructor(keyPair: AsymmetricKeyPair) {
this.keyPair = new NodeRSA();
this.keyPair.importKey(Buffer.from(keyPair.privateKey), 'pkcs8-der');
}
public encryptBytes(publicKey: byte[], content: byte[]): byte[] {
const key = new NodeRSA();
key.importKey(Buffer.from(publicKey), 'pkcs8-public-der');
return bufferToArray(key.encrypt(Buffer.from(content), 'buffer'));
}
public decryptBytes(content: byte[]): byte[] {
return bufferToArray(this.keyPair.decrypt(Buffer.from(content), 'buffer'));
}
}

@ -1,168 +0,0 @@
import * as EteSync from './EteSync';
import sjcl from 'sjcl';
import { USER, PASSWORD, keyBase64 } from './TestConstants';
const testApiBase = 'http://localhost:8000';
sjcl.random.addEntropy('seedForTheTests', 1024, 'FakeSeed');
let credentials: EteSync.Credentials;
beforeEach(async () => {
const authenticator = new EteSync.Authenticator(testApiBase);
const authToken = await authenticator.getAuthToken(USER, PASSWORD);
credentials = new EteSync.Credentials(USER, authToken);
await fetch(testApiBase + '/reset/', {
method: 'post',
headers: { Authorization: 'Token ' + credentials.authToken },
});
});
it('Simple sync', async () => {
const journalManager = new EteSync.JournalManager(credentials, testApiBase);
let journals = await journalManager.list();
expect(journals.length).toBe(0);
const uid1 = sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash('id1'));
const cryptoManager = new EteSync.CryptoManager(keyBase64, USER);
const info1 = new EteSync.CollectionInfo({ uid: uid1, content: 'test', displayName: 'Dislpay 1' });
const journal = new EteSync.Journal();
journal.setInfo(cryptoManager, info1);
await expect(journalManager.create(journal)).resolves.toBeDefined();
// Uid clash
await expect(journalManager.create(journal)).rejects.toBeInstanceOf(EteSync.HTTPError);
journals = await journalManager.list();
expect(journals.length).toBe(1);
expect(journals[0].uid).toBe(journal.uid);
// Update
const info2 = new EteSync.CollectionInfo(info1);
info2.displayName = 'Display 2';
journal.setInfo(cryptoManager, info2);
await expect(journalManager.update(journal)).resolves.toBeDefined();
journals = await journalManager.list();
expect(journals.length).toBe(1);
expect(journals[0].getInfo(cryptoManager).displayName).toBe(info2.displayName);
// Delete
await expect(journalManager.delete(journal)).resolves.toBeDefined();
journals = await journalManager.list();
expect(journals.length).toBe(0);
});
it('Journal Entry sync', async () => {
const journalManager = new EteSync.JournalManager(credentials, testApiBase);
const uid1 = sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash('id1'));
const cryptoManager = new EteSync.CryptoManager(keyBase64, USER);
const info1 = new EteSync.CollectionInfo({ uid: uid1, content: 'test', displayName: 'Dislpay 1' });
const journal = new EteSync.Journal();
journal.setInfo(cryptoManager, info1);
await expect(journalManager.create(journal)).resolves.toBeDefined();
const entryManager = new EteSync.EntryManager(credentials, testApiBase, journal.uid);
{
const entries = await entryManager.list(null);
expect(entries.length).toBe(0);
}
const syncEntry = new EteSync.SyncEntry({ action: 'ADD', content: 'bla' });
let prevUid = null;
const entry = new EteSync.Entry();
entry.setSyncEntry(cryptoManager, syncEntry, prevUid);
await expect(entryManager.create([entry], prevUid)).resolves.toBeDefined();
prevUid = entry.uid;
{
// Verify we get back what we sent
const entries = await entryManager.list(null);
expect(entries[0].serialize()).toEqual(entry.serialize());
syncEntry.uid = entries[0].uid;
expect(entries[0].getSyncEntry(cryptoManager, null)).toEqual(syncEntry);
}
let entry2 = new EteSync.Entry();
entry2.setSyncEntry(cryptoManager, syncEntry, prevUid);
{
// Try to push good entries with a bad prevUid
const entries = [entry2];
await expect(entryManager.create(entries, null)).rejects.toBeInstanceOf(EteSync.HTTPError);
// Second try with good prevUid
await expect(entryManager.create(entries, prevUid)).resolves.toBeDefined();
prevUid = entry2.uid;
}
{
// Check last works:
let entries = await entryManager.list(null);
expect(entries.length).toBe(2);
entries = await entryManager.list(entry.uid);
expect(entries.length).toBe(1);
entries = await entryManager.list(entry2.uid);
expect(entries.length).toBe(0);
}
{
// Corrupt the journal and verify we get it:
entry2 = new EteSync.Entry();
entry2.setSyncEntry(cryptoManager, syncEntry, 'somebaduid');
await expect(entryManager.create([entry2], prevUid)).resolves.toBeDefined();
const entries = await entryManager.list(null);
expect(() => {
let prev = null;
for (const ent of entries) {
expect(ent.getSyncEntry(cryptoManager, prev)).toBeDefined();
prev = ent.uid;
}
}).toThrowError();
}
});
it('User info sync', async () => {
const cryptoManager = new EteSync.CryptoManager(keyBase64, 'userInfo');
const userInfoManager = new EteSync.UserInfoManager(credentials, testApiBase);
// Get when there's nothing
await expect(userInfoManager.fetch(USER)).rejects.toBeInstanceOf(EteSync.HTTPError);
// Create
const userInfo = new EteSync.UserInfo(USER);
userInfo.setKeyPair(cryptoManager, new EteSync.AsymmetricKeyPair([0, 1, 2, 3], [4, 5, 6, 6]));
await expect(userInfoManager.create(userInfo)).resolves.not.toBeNull();
// Get
let userInfo2 = await userInfoManager.fetch(USER);
expect(userInfo2).not.toBeNull();
expect(userInfo.getKeyPair(cryptoManager)).toEqual(userInfo2!.getKeyPair(cryptoManager));
// Update
userInfo.setKeyPair(cryptoManager, new EteSync.AsymmetricKeyPair([1, 94, 45], [4, 34, 45, 45]));
await userInfoManager.update(userInfo);
userInfo2 = await userInfoManager.fetch(USER);
expect(userInfo2).not.toBeNull();
expect(userInfo.getKeyPair(cryptoManager)).toEqual(userInfo2!.getKeyPair(cryptoManager));
// Delete
await userInfoManager.delete(userInfo);
await expect(userInfoManager.fetch(USER)).rejects.toBeInstanceOf(EteSync.HTTPError);
});

@ -1,623 +0,0 @@
import sjcl from 'sjcl';
import URI from 'urijs';
import * as Constants from './Constants';
import { byte, base64, stringToByteArray } from './Helpers';
import { CryptoManager, AsymmetricCryptoManager, AsymmetricKeyPair, HMAC_SIZE_BYTES } from './Crypto';
export { CryptoManager, AsymmetricCryptoManager, AsymmetricKeyPair, deriveKey, genUid } from './Crypto';
type URI = typeof URI;
class ExtendableError extends Error {
constructor(message: any) {
super(message);
Object.setPrototypeOf(this, ExtendableError.prototype);
this.name = this.constructor.name;
this.stack = (new Error(message)).stack;
}
}
export class HTTPError extends ExtendableError {
constructor(message: any) {
super(message);
Object.setPrototypeOf(this, HTTPError.prototype);
this.name = this.constructor.name;
}
}
export class IntegrityError extends ExtendableError {
constructor(message: any) {
super(message);
Object.setPrototypeOf(this, IntegrityError.prototype);
this.name = this.constructor.name;
}
}
export class EncryptionPasswordError extends ExtendableError {
constructor(message: any) {
super(message);
Object.setPrototypeOf(this, EncryptionPasswordError.prototype);
this.name = this.constructor.name;
}
}
// FIXME: Make secure + types
function CastJson(json: any, to: any) {
return Object.assign(to, json);
}
function hmacToHex(hmac: byte[]): string {
return sjcl.codec.hex.fromBits(sjcl.codec.bytes.toBits(hmac));
}
export class Credentials {
public email: string;
public authToken: string;
constructor(email: string, authToken: string) {
this.email = email;
this.authToken = authToken;
}
}
export class CollectionInfo {
public uid: string;
public type: string;
public displayName: string;
public description: string;
public color: number;
constructor(json?: any) {
CastJson(json, this);
}
}
interface BaseItemJson {
content: base64;
}
class BaseItem<T extends BaseItemJson> {
protected _json: T;
protected _encrypted: byte[];
protected _content?: object;
constructor() {
this._json = {} as T;
}
public deserialize(json: T) {
this._json = Object.assign({}, json);
if (json.content) {
this._encrypted = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(json.content));
}
this._content = undefined;
}
public serialize(): T {
return Object.assign(
{},
this._json,
{ content: sjcl.codec.base64.fromBits(sjcl.codec.bytes.toBits(this._encrypted)) }
);
}
protected verifyBase(hmac: byte[], calculated: byte[]) {
if (!this.hmacEqual(hmac, calculated)) {
throw new IntegrityError('Bad HMAC. ' + hmacToHex(hmac) + ' != ' + hmacToHex(calculated));
}
}
private hmacEqual(hmac: byte[], calculated: byte[]) {
return (hmac.length === calculated.length) &&
(hmac.every((v, i) => v === calculated[i]));
}
}
interface BaseJson extends BaseItemJson {
uid: string;
}
class BaseJournal<T extends BaseJson> extends BaseItem<T> {
get uid(): string {
return this._json.uid;
}
}
export interface JournalJson extends BaseJson {
version: number;
owner: string;
readOnly?: boolean;
key?: base64;
}
export class Journal extends BaseJournal<JournalJson> {
constructor(initial?: Partial<JournalJson>, version: number = Constants.CURRENT_VERSION) {
super();
this._json = {
...this._json,
version,
...initial,
};
}
get key(): byte[] | undefined {
if (this._json.key) {
return sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(this._json.key));
}
return undefined;
}
get owner(): string | undefined {
return this._json.owner;
}
get readOnly(): boolean | undefined {
return this._json.readOnly;
}
get version(): number {
return this._json.version;
}
public getCryptoManager(derived: string, keyPair: AsymmetricKeyPair) {
if (this.key) {
const asymmetricCryptoManager = new AsymmetricCryptoManager(keyPair);
const derivedJournalKey = asymmetricCryptoManager.decryptBytes(this.key);
return CryptoManager.fromDerivedKey(derivedJournalKey, this.version);
} else {
return new CryptoManager(derived, this.uid, this.version);
}
}
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);
}
public getInfo(cryptoManager: CryptoManager): CollectionInfo {
this.verify(cryptoManager);
if (this._content === undefined) {
this._content = JSON.parse(cryptoManager.decrypt(this.encryptedContent()));
}
const ret = new CollectionInfo(this._content);
ret.uid = this.uid;
return ret;
}
public calculateHmac(cryptoManager: CryptoManager, encrypted: byte[]): byte[] {
const prefix = stringToByteArray(this.uid);
return cryptoManager.hmac(prefix.concat(encrypted));
}
public verify(cryptoManager: CryptoManager) {
const calculated = this.calculateHmac(cryptoManager, this.encryptedContent());
const hmac = this._encrypted.slice(0, HMAC_SIZE_BYTES);
super.verifyBase(hmac, calculated);
}
private encryptedContent(): byte[] {
return this._encrypted.slice(HMAC_SIZE_BYTES);
}
}
export enum SyncEntryAction {
Add = 'ADD',
Delete = 'DELETE',
Change = 'CHANGE',
}
export class SyncEntry {
public uid?: string;
public action: SyncEntryAction;
public content: string;
constructor(json?: any, uid?: string) {
CastJson(json, this);
this.uid = uid;
}
}
export type EntryJson = BaseJson;
export class Entry extends BaseJournal<EntryJson> {
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));
}
public getSyncEntry(cryptoManager: CryptoManager, prevUid: string | null): SyncEntry {
this.verify(cryptoManager, prevUid);
if (this._content === undefined) {
this._content = JSON.parse(cryptoManager.decrypt(this._encrypted));
}
return new SyncEntry(this._content, 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[] {
const prefix = (prevUid !== null) ? stringToByteArray(prevUid) : [];
return cryptoManager.hmac(prefix.concat(encrypted));
}
}
export interface UserInfoJson extends BaseItemJson {
version?: number;
owner?: string;
pubkey: base64;
}
export class UserInfo extends BaseItem<UserInfoJson> {
public _owner: string;
constructor(owner: string, version: number = Constants.CURRENT_VERSION) {
super();
this._json.version = version;
this._owner = owner;
}
get version(): number {
return this._json.version!;
}
get owner(): string {
return this._owner;
}
get publicKey() {
return this._json.pubkey;
}
public serialize(): UserInfoJson {
const ret = super.serialize();
ret.owner = this._owner;
return ret;
}
public getCryptoManager(derived: string) {
return new CryptoManager(derived, 'userInfo', this.version);
}
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);
}
public getKeyPair(cryptoManager: CryptoManager): AsymmetricKeyPair {
this.verify(cryptoManager);
if (this._content === undefined) {
this._content = cryptoManager.decryptBytes(this.encryptedContent());
}
const pubkey = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(this._json.pubkey));
return new AsymmetricKeyPair(pubkey, this._content as byte[]);
}
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));
}
public verify(cryptoManager: CryptoManager) {
const calculated = this.calculateHmac(cryptoManager, this.encryptedContent());
const hmac = this._encrypted.slice(0, HMAC_SIZE_BYTES);
super.verifyBase(hmac, calculated);
}
private encryptedContent(): byte[] {
return this._encrypted.slice(HMAC_SIZE_BYTES);
}
}
// FIXME: baseUrl and apiBase should be the right type all around.
class BaseNetwork {
public static urlExtend(_baseUrl: URI, segments: string[]): URI {
let baseUrl = _baseUrl as any;
baseUrl = baseUrl.clone();
for (const segment of segments) {
baseUrl.segment(segment);
}
return baseUrl.normalize();
}
public apiBase: any; // FIXME
constructor(apiBase: string) {
this.apiBase = URI(apiBase).normalize();
}
public newCall<T = any>(segments: string[] = [], extra: RequestInit = {}, _apiBase: URI = this.apiBase): Promise<T> {
const apiBase = BaseNetwork.urlExtend(_apiBase, segments);
extra = {
...extra,
headers: {
Accept: 'application/json',
...extra.headers,
},
};
return new Promise((resolve, reject) => {
fetch(apiBase.toString(), extra).then((response) => {
response.text().then((text) => {
let json: any;
let body: any = text;
try {
json = JSON.parse(text);
body = json;
} catch (e) {
body = text;
}
if (response.ok) {
resolve(body);
} else {
if (json) {
reject(new HTTPError(json.detail || json.non_field_errors || JSON.stringify(json)));
} else {
reject(new HTTPError(body));
}
}
}).catch((error) => {
reject(error);
});
}).catch((error) => {
reject(error);
});
});
}
}
export class Authenticator extends BaseNetwork {
constructor(apiBase: string) {
super(apiBase);
this.apiBase = BaseNetwork.urlExtend(this.apiBase, ['api-token-auth', '']);
}
public getAuthToken(username: string, password: string): Promise<string> {
return new Promise((resolve, reject) => {
// FIXME: should be FormData but doesn't work for whatever reason
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,
};
this.newCall<{token: string}>([], extra).then((json) => {
resolve(json.token);
}).catch((error: Error) => {
reject(error);
});
});
}
}
export class BaseManager extends BaseNetwork {
protected credentials: Credentials;
constructor(credentials: Credentials, apiBase: string, segments: string[]) {
super(apiBase);
this.credentials = credentials;
this.apiBase = BaseNetwork.urlExtend(this.apiBase, ['api', 'v1'].concat(segments));
}
public newCall<T = any>(segments: string[] = [], extra: RequestInit = {}, apiBase: any = this.apiBase): Promise<T> {
extra = {
...extra,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Authorization': 'Token ' + this.credentials.authToken,
...extra.headers,
},
};
return super.newCall(segments, extra, apiBase);
}
}
export class JournalManager extends BaseManager {
constructor(credentials: Credentials, apiBase: string) {
super(credentials, apiBase, ['journals', '']);
}
public fetch(journalUid: string): Promise<Journal> {
return new Promise((resolve, reject) => {
this.newCall<JournalJson>([journalUid, '']).then((json) => {
const journal = new Journal({ uid: json.uid }, json.version);
journal.deserialize(json);
resolve(journal);
}).catch((error: Error) => {
reject(error);
});
});
}
public list(): Promise<Journal[]> {
return new Promise((resolve, reject) => {
this.newCall<JournalJson[]>().then((json) => {
resolve(json.map((val: JournalJson) => {
const journal = new Journal({ uid: val.uid }, val.version);
journal.deserialize(val);
return journal;
}));
}).catch((error: Error) => {
reject(error);
});
});
}
public create(journal: Journal): Promise<{}> {
const extra = {
method: 'post',
body: JSON.stringify(journal.serialize()),
};
return this.newCall<Journal>([], extra);
}
public update(journal: Journal): Promise<{}> {
const extra = {
method: 'put',
body: JSON.stringify(journal.serialize()),
};
return this.newCall<Journal>([journal.uid, ''], extra);
}
public delete(journal: Journal): Promise<{}> {
const extra = {
method: 'delete',
};
return this.newCall([journal.uid, ''], extra);
}
}
export class EntryManager extends BaseManager {
constructor(credentials: Credentials, apiBase: string, journalId: string) {
super(credentials, apiBase, ['journals', journalId, 'entries', '']);
}
public list(lastUid: string | null, limit = 0): Promise<Entry[]> {
let apiBase = this.apiBase.clone();
apiBase = apiBase.search({
last: (lastUid !== null) ? lastUid : undefined,
limit: (limit > 0) ? limit : undefined,
});
return new Promise((resolve, reject) => {
this.newCall<EntryJson[]>(undefined, undefined, apiBase).then((json) => {
resolve(json.map((val) => {
const entry = new Entry();
entry.deserialize(val);
return entry;
}));
}).catch((error: Error) => {
reject(error);
});
});
}
public create(entries: Entry[], lastUid: string | null): Promise<{}> {
let apiBase = this.apiBase.clone();
apiBase = apiBase.search({
last: (lastUid !== null) ? lastUid : undefined,
});
const extra = {
method: 'post',
body: JSON.stringify(entries.map((x) => x.serialize())),
};
return this.newCall(undefined, extra, apiBase);
}
}
export interface JournalMemberJson {
user: string;
key: base64;
readOnly?: boolean;
}
export class JournalMembersManager extends BaseManager {
constructor(credentials: Credentials, apiBase: string, journalId: string) {
super(credentials, apiBase, ['journals', journalId, 'members', '']);
}
public list(): Promise<JournalMemberJson[]> {
return new Promise((resolve, reject) => {
this.newCall<JournalMemberJson[]>().then((json) => {
resolve(json.map((val) => {
return val;
}));
}).catch((error: Error) => {
reject(error);
});
});
}
public create(journalMember: JournalMemberJson): Promise<{}> {
const extra = {
method: 'post',
body: JSON.stringify(journalMember),
};
return this.newCall([], extra);
}
public delete(journalMember: JournalMemberJson): Promise<{}> {
const extra = {
method: 'delete',
};
return this.newCall([journalMember.user, ''], extra);
}
}
export class UserInfoManager extends BaseManager {
constructor(credentials: Credentials, apiBase: string) {
super(credentials, apiBase, ['user', '']);
}
public fetch(owner: string): Promise<UserInfo> {
return new Promise((resolve, reject) => {
this.newCall<UserInfoJson>([owner, '']).then((json) => {
const userInfo = new UserInfo(owner, json.version);
userInfo.deserialize(json);
resolve(userInfo);
}).catch((error: Error) => {
reject(error);
});
});
}
public create(userInfo: UserInfo): Promise<{}> {
const extra = {
method: 'post',
body: JSON.stringify(userInfo.serialize()),
};
return this.newCall([], extra);
}
public update(userInfo: UserInfo): Promise<{}> {
const extra = {
method: 'put',
body: JSON.stringify(userInfo.serialize()),
};
return this.newCall([userInfo.owner, ''], extra);
}
public delete(userInfo: UserInfo): Promise<{}> {
const extra = {
method: 'delete',
};
return this.newCall([userInfo.owner, ''], extra);
}
}

@ -1,11 +0,0 @@
export type byte = number;
export type base64 = string;
export function stringToByteArray(str: string): byte[] {
const ret = [];
for (let i = 0 ; i < str.length ; i++) {
ret.push(str.charCodeAt(i));
}
return ret;
}

@ -1,6 +0,0 @@
export const USER = 'test@localhost';
export const PASSWORD = 'SomePassword';
export const keyBase64 =
'Gpn6j6WJ/9JJbVkWhmEfZjlqSps5rwEOzjUOO0rqufvb4vtT4UfRgx0uMivuGwjF7/8Y1z1glIASX7Oz/4l2jucgf+lAzg2oTZFodWkXRZCDmFa7c9' +
'a8/04xIs7koFmUH34Rl9XXW6V2/GDVigQhQU8uWnrGo795tupoNQMbtB8RgMX5GyuxR55FvcybHpYBbwrDIsKvXcBxWFEscdNU8zyeq3yjvDo/W/y2' +
'4dApW3mnNo7vswoL2rpkZj3dqw==';

@ -19,7 +19,7 @@ import ConfirmationDialog from '../widgets/ConfirmationDialog';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as ICAL from 'ical.js'; import * as ICAL from 'ical.js';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { ContactType } from '../pim-types'; import { ContactType } from '../pim-types';

@ -3,7 +3,7 @@ import * as React from 'react';
import { store, persistor } from '../store'; import { store, persistor } from '../store';
import { resetKey } from '../store/actions'; import { resetKey } from '../store/actions';
import { EncryptionPasswordError, IntegrityError } from '../api/EteSync'; import { EncryptionPasswordError, IntegrityError } from 'etesync';
import PrettyError from '../widgets/PrettyError'; import PrettyError from '../widgets/PrettyError';
interface PropsType { interface PropsType {

@ -26,7 +26,7 @@ import { withRouter } from 'react-router';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as ICAL from 'ical.js'; import * as ICAL from 'ical.js';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { EventType } from '../pim-types'; import { EventType } from '../pim-types';

@ -15,7 +15,7 @@ import * as ICAL from 'ical.js';
import { TaskType, EventType, ContactType } from '../pim-types'; import { TaskType, EventType, ContactType } from '../pim-types';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
class JournalEntries extends React.PureComponent { class JournalEntries extends React.PureComponent {
public static defaultProps = { public static defaultProps = {

@ -26,7 +26,7 @@ import { withRouter } from 'react-router';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as ICAL from 'ical.js'; import * as ICAL from 'ical.js';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { TaskType, TaskStatusType } from '../pim-types'; import { TaskType, TaskStatusType } from '../pim-types';

@ -1,4 +1,4 @@
import * as EteSync from './api/EteSync'; import * as EteSync from 'etesync';
import { CredentialsData, UserInfoData } from './store'; import { CredentialsData, UserInfoData } from './store';
import { addEntries } from './store/actions'; import { addEntries } from './store/actions';

@ -4,7 +4,7 @@ import * as ICAL from 'ical.js';
import { EventType, ContactType, TaskType } from './pim-types'; import { EventType, ContactType, TaskType } from './pim-types';
import * as EteSync from './api/EteSync'; import * as EteSync from 'etesync';
export function syncEntriesToItemMap( export function syncEntriesToItemMap(
collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>, base: {[key: string]: ContactType} = {}) { collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>, base: {[key: string]: ContactType} = {}) {

@ -1,7 +1,7 @@
import { Action, createAction, createActions } from 'redux-actions'; import { Action, createAction, createActions } from 'redux-actions';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { UserInfo } from '../api/EteSync'; import { UserInfo } from 'etesync';
import { CredentialsData, EntriesType, SettingsType } from './'; import { CredentialsData, EntriesType, SettingsType } from './';

@ -5,7 +5,7 @@ import session from 'redux-persist/lib/storage/session';
import { List, Map as ImmutableMap } from 'immutable'; import { List, Map as ImmutableMap } from 'immutable';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import { import {
JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, JournalsFetchRecord, UserInfoFetchRecord, JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, JournalsFetchRecord, UserInfoFetchRecord,
CredentialsTypeRemote, JournalsType, EntriesType, UserInfoType, SettingsType, CredentialsTypeRemote, JournalsType, EntriesType, UserInfoType, SettingsType,

@ -3,7 +3,7 @@ import { entries, EntriesTypeImmutable } from './reducers';
import { Map } from 'immutable'; import { Map } from 'immutable';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
it('Entries reducer', () => { it('Entries reducer', () => {
const jId = '24324324324'; const jId = '24324324324';

@ -2,7 +2,7 @@ import { Action, ActionFunctionAny, combineActions, handleAction, handleActions
import { List, Map as ImmutableMap, Record } from 'immutable'; import { List, Map as ImmutableMap, Record } from 'immutable';
import * as EteSync from '../api/EteSync'; import * as EteSync from 'etesync';
import * as actions from './actions'; import * as actions from './actions';

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import sjcl from 'sjcl'; import sjcl from 'sjcl';
import { byte, base64 } from '../api/Helpers'; import { byte, base64 } from 'etesync';
function byteArray4ToNumber(bytes: byte[], offset: number) { function byteArray4ToNumber(bytes: byte[], offset: number) {
// tslint:disable:no-bitwise // tslint:disable:no-bitwise

@ -4062,6 +4062,15 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
etesync@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/etesync/-/etesync-0.1.1.tgz#f856c9b9bc31f78200a2a34a0f1b9d1035f80d72"
integrity sha512-islIOoSopNgW0+MzLBU/lE0V0K64JCWl4mL5aNc/3MfbMQO6Db+ofmXNZo98EVB06vseglUs2ANc/wVrb9VL+w==
dependencies:
node-rsa "^1.0.6"
sjcl "git+https://github.com/etesync/sjcl"
urijs "^1.19.1"
eventemitter3@^4.0.0: eventemitter3@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
@ -6934,10 +6943,10 @@ node-releases@^1.1.29, node-releases@^1.1.38:
dependencies: dependencies:
semver "^6.3.0" semver "^6.3.0"
node-rsa@^1.0.3: node-rsa@^1.0.6:
version "1.0.3" version "1.0.6"
resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.0.3.tgz#e2d23be0096caeca014b139af480ca7bc614a589" resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.0.6.tgz#47d22eba8b41192cd6f06db15870c67126f00aa4"
integrity sha512-gQowjnOunjmojrpO+d8x1ubL9X2Zpj4MRmY2J2hPtVF8g1VgOX1yNWUeCCoyzkRHunJf1/3orLzit5PiRtDz1A== integrity sha512-v42495lozKpuQmrcIzld9ds/Tn7pwjuh0BHSHnhPrKkAVSyTAyrZodFLFafOfWiUKamLt4lgWdngP8W/LzCm2w==
dependencies: dependencies:
asn1 "^0.2.4" asn1 "^0.2.4"
@ -9487,9 +9496,9 @@ sisteransi@^1.0.3:
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb"
integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg==
"sjcl@git+https://github.com/etesync/sjcl": "sjcl@git+https://github.com/etesync/sjcl.git":
version "1.0.7" version "1.0.7"
resolved "git+https://github.com/etesync/sjcl#cf7673694e75d41902a68c69eb45ecc696393945" resolved "git+https://github.com/etesync/sjcl.git#f259515e3c5cf8f437cdfa99c1cf0a8ad7321556"
slash@^1.0.0: slash@^1.0.0:
version "1.0.0" version "1.0.0"
@ -10363,9 +10372,9 @@ uri-js@^4.2.2:
punycode "^2.1.0" punycode "^2.1.0"
urijs@^1.19.1: urijs@^1.19.1:
version "1.19.1" version "1.19.2"
resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a"
integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg== integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==
urix@^0.1.0: urix@^0.1.0:
version "0.1.0" version "0.1.0"

Loading…
Cancel
Save