Initial code import of the etesync encryption and service module
This will be a small library in the end, but at the moment it's in this repo for convenience. It includes the etesync service, crypto and tests to cover them. The tests require a running debug etesync server. To create one, just create a server from: https://github.com/etesync/server-skeleton/ Set DEBUG to True, and create a test user with the credentials that are listed in the test files.master
parent
2d6628038d
commit
e0cc13cfd2
@ -0,0 +1 @@
|
|||||||
|
export const CURRENT_VERSION = 2;
|
@ -0,0 +1,29 @@
|
|||||||
|
import { CryptoManager, deriveKey } from './Crypto';
|
||||||
|
import { USER, PASSWORD, keyBase64 } from './TestConstants';
|
||||||
|
|
||||||
|
import { stringToByteArray } from './Helpers';
|
||||||
|
|
||||||
|
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')));
|
||||||
|
});
|
@ -0,0 +1,90 @@
|
|||||||
|
import * as sjcl from 'sjcl';
|
||||||
|
|
||||||
|
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 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hmac256(salt: sjcl.BitArray, key: sjcl.BitArray) {
|
||||||
|
let hmac = new sjcl.misc.hmac(salt);
|
||||||
|
return hmac.encrypt(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CryptoManager {
|
||||||
|
version: number;
|
||||||
|
key: sjcl.BitArray;
|
||||||
|
cipherKey: sjcl.BitArray;
|
||||||
|
hmacKey: sjcl.BitArray;
|
||||||
|
|
||||||
|
cipherWords = 4;
|
||||||
|
|
||||||
|
constructor(_keyBase64: base64, salt: string, version: number = Constants.CURRENT_VERSION) {
|
||||||
|
this.version = version;
|
||||||
|
let 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateDerivedKeys() {
|
||||||
|
this.cipherKey = hmac256(sjcl.codec.utf8String.toBits('aes'), this.key);
|
||||||
|
this.hmacKey = hmac256(sjcl.codec.utf8String.toBits('hmac'), this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(content: string): byte[] {
|
||||||
|
// FIXME!!! Use a fixed size iv for testing
|
||||||
|
// switch to: sjcl.random.randomWords(this.cipherWords);
|
||||||
|
const iv = [1, 1, 1, 1];
|
||||||
|
|
||||||
|
let prp = new sjcl.cipher.aes(this.cipherKey);
|
||||||
|
let cipherText = sjcl.mode.cbc.encrypt(prp, sjcl.codec.utf8String.toBits(content), iv);
|
||||||
|
return sjcl.codec.bytes.fromBits(iv.concat(cipherText));
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(content: byte[]): string {
|
||||||
|
let cipherText = sjcl.codec.bytes.toBits(content);
|
||||||
|
const iv = cipherText.splice(0, this.cipherWords);
|
||||||
|
|
||||||
|
let prp = new sjcl.cipher.aes(this.cipherKey);
|
||||||
|
let clearText = sjcl.mode.cbc.decrypt(prp, cipherText, iv);
|
||||||
|
return sjcl.codec.utf8String.fromBits(clearText);
|
||||||
|
}
|
||||||
|
|
||||||
|
hmac(content: byte[]): byte[] {
|
||||||
|
return sjcl.codec.bytes.fromBits(this.hmacBase(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
hmac64(content: byte[]): base64 {
|
||||||
|
return sjcl.codec.base64.fromBits(this.hmacBase(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
import * as EteSync from './EteSync';
|
||||||
|
|
||||||
|
import * as fetch from 'isomorphic-fetch';
|
||||||
|
import * as sjcl from 'sjcl';
|
||||||
|
|
||||||
|
const testApiBase = 'http://localhost:8000';
|
||||||
|
|
||||||
|
import { USER, PASSWORD, keyBase64 } from './TestConstants';
|
||||||
|
|
||||||
|
let credentials: EteSync.Credentials;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let 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 () => {
|
||||||
|
let 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'});
|
||||||
|
let 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
|
||||||
|
let 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 () => {
|
||||||
|
let 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'});
|
||||||
|
let journal = new EteSync.Journal();
|
||||||
|
journal.setInfo(cryptoManager, info1);
|
||||||
|
|
||||||
|
await expect(journalManager.create(journal)).resolves.toBeDefined();
|
||||||
|
|
||||||
|
let entryManager = new EteSync.EntryManager(credentials, testApiBase, journal.uid);
|
||||||
|
|
||||||
|
let entries = await entryManager.list(null);
|
||||||
|
expect(entries.length).toBe(0);
|
||||||
|
|
||||||
|
const syncEntry = new EteSync.SyncEntry({content: 'bla'});
|
||||||
|
let prevUid = null;
|
||||||
|
let entry = new EteSync.Entry();
|
||||||
|
entry.setSyncEntry(cryptoManager, syncEntry, prevUid);
|
||||||
|
|
||||||
|
entries = [entry];
|
||||||
|
await expect(entryManager.create(entries, prevUid)).resolves.toBeDefined();
|
||||||
|
prevUid = entry.uid;
|
||||||
|
|
||||||
|
let entry2 = new EteSync.Entry();
|
||||||
|
entry2.setSyncEntry(cryptoManager, syncEntry, prevUid);
|
||||||
|
|
||||||
|
// Try to push good entries with a bad prevUid
|
||||||
|
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:
|
||||||
|
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');
|
||||||
|
entries = [entry2];
|
||||||
|
await expect(entryManager.create(entries, prevUid)).resolves.toBeDefined();
|
||||||
|
|
||||||
|
entries = await entryManager.list(null);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
let prev = null;
|
||||||
|
for (let ent of entries) {
|
||||||
|
expect(ent.getSyncEntry(cryptoManager, prev)).toBeDefined();
|
||||||
|
prev = ent.uid;
|
||||||
|
}
|
||||||
|
}).toThrowError();
|
||||||
|
});
|
@ -0,0 +1,368 @@
|
|||||||
|
import * as sjcl from 'sjcl';
|
||||||
|
import * as URI from 'urijs';
|
||||||
|
|
||||||
|
import * as fetch from 'isomorphic-fetch';
|
||||||
|
|
||||||
|
import { byte, base64, stringToByteArray } from './Helpers';
|
||||||
|
import { CryptoManager, HMAC_SIZE_BYTES } from './Crypto';
|
||||||
|
export { CryptoManager } from './Crypto';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
email: string;
|
||||||
|
authToken: string;
|
||||||
|
|
||||||
|
constructor(email: string, authToken: string) {
|
||||||
|
this.email = email;
|
||||||
|
this.authToken = authToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CollectionInfo {
|
||||||
|
uid: string;
|
||||||
|
version: number;
|
||||||
|
type: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
color: number;
|
||||||
|
|
||||||
|
constructor(json?: any) {
|
||||||
|
CastJson(json, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseJournal<T> {
|
||||||
|
uid: string;
|
||||||
|
protected _encrypted: byte[];
|
||||||
|
protected _content?: string;
|
||||||
|
|
||||||
|
a(a: T) { /* Silence T unused */ }
|
||||||
|
|
||||||
|
deserialize(uid: string, content: base64) {
|
||||||
|
this.uid = uid;
|
||||||
|
this._encrypted = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(content));
|
||||||
|
this._content = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): {uid: string, content: base64} {
|
||||||
|
return {
|
||||||
|
uid: this.uid,
|
||||||
|
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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Journal extends BaseJournal<CollectionInfo> {
|
||||||
|
setInfo(cryptoManager: CryptoManager, info: CollectionInfo) {
|
||||||
|
this.uid = info.uid;
|
||||||
|
this._content = JSON.stringify(info);
|
||||||
|
const encrypted = cryptoManager.encrypt(this._content);
|
||||||
|
this._encrypted = this.calculateHmac(cryptoManager, encrypted).concat(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfo(cryptoManager: CryptoManager): CollectionInfo {
|
||||||
|
if (this._content === undefined) {
|
||||||
|
this._content = JSON.parse(cryptoManager.decrypt(this.encryptedContent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.verify(cryptoManager);
|
||||||
|
|
||||||
|
return new CollectionInfo(this._content);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateHmac(cryptoManager: CryptoManager, encrypted: byte[]): byte[] {
|
||||||
|
let 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);
|
||||||
|
|
||||||
|
super.verifyBase(hmac, calculated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private encryptedContent(): byte[] {
|
||||||
|
return this._encrypted.slice(HMAC_SIZE_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SyncEntryType {
|
||||||
|
Add = 'ADD',
|
||||||
|
Delete = 'DEL',
|
||||||
|
Change = 'CHANGE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncEntry {
|
||||||
|
type: SyncEntryType;
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
constructor(json?: any) {
|
||||||
|
CastJson(json, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Entry extends BaseJournal<SyncEntry> {
|
||||||
|
setSyncEntry(cryptoManager: CryptoManager, info: SyncEntry, prevUid: string | null) {
|
||||||
|
this._content = JSON.stringify(info);
|
||||||
|
this._encrypted = cryptoManager.encrypt(this._content);
|
||||||
|
this.uid = hmacToHex(this.calculateHmac(cryptoManager, this._encrypted, prevUid));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSyncEntry(cryptoManager: CryptoManager, prevUid: string | null): SyncEntry {
|
||||||
|
if (this._content === undefined) {
|
||||||
|
this._content = JSON.parse(cryptoManager.decrypt(this._encrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.verify(cryptoManager, prevUid);
|
||||||
|
|
||||||
|
return new SyncEntry(this._content);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
super.verifyBase(hmac, calculated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateHmac(cryptoManager: CryptoManager, encrypted: byte[], prevUid: string | null): byte[] {
|
||||||
|
let prefix = (prevUid !== null) ? stringToByteArray(prevUid) : [];
|
||||||
|
return cryptoManager.hmac(prefix.concat(encrypted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: baseUrl and apiBase should be the right type all around.
|
||||||
|
|
||||||
|
class BaseNetwork {
|
||||||
|
apiBase: any; // FIXME
|
||||||
|
|
||||||
|
static urlExtend(_baseUrl: URL, segments: Array<string>): URL {
|
||||||
|
let baseUrl = _baseUrl as any;
|
||||||
|
baseUrl = baseUrl.clone();
|
||||||
|
for (const segment of segments) {
|
||||||
|
baseUrl.segment(segment);
|
||||||
|
}
|
||||||
|
return baseUrl.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
extra = Object.assign({}, extra);
|
||||||
|
extra.headers = Object.assign(
|
||||||
|
{ '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 {
|
||||||
|
reject(new HTTPError(json.detail));
|
||||||
|
}
|
||||||
|
}).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', '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) +
|
||||||
|
'&password=' + encodeURIComponent(password);
|
||||||
|
const extra = {
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
},
|
||||||
|
body: form
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newCall([], extra).then((json: {token: string}) => {
|
||||||
|
resolve(json.token);
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseManager extends BaseNetwork {
|
||||||
|
protected credentials: Credentials;
|
||||||
|
|
||||||
|
constructor(credentials: Credentials, apiBase: string, segments: Array<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>> {
|
||||||
|
extra = Object.assign({}, extra);
|
||||||
|
extra.headers = Object.assign(
|
||||||
|
{
|
||||||
|
'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', '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(): Promise<Journal[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.newCall().then((json: Array<{}>) => {
|
||||||
|
resolve(json.map((val: any) => {
|
||||||
|
let journal = new Journal();
|
||||||
|
journal.deserialize(val.uid, val.content);
|
||||||
|
return journal;
|
||||||
|
}));
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
create(journal: Journal): Promise<{}> {
|
||||||
|
const extra = {
|
||||||
|
method: 'post',
|
||||||
|
body: JSON.stringify(journal.serialize()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.newCall([], extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(journal: Journal): Promise<{}> {
|
||||||
|
const extra = {
|
||||||
|
method: 'put',
|
||||||
|
body: JSON.stringify(journal.serialize()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.newCall([journal.uid, ''], extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
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', '']);
|
||||||
|
}
|
||||||
|
|
||||||
|
list(lastUid: string | null, limit: number = 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(undefined, undefined, apiBase).then((json: Array<{}>) => {
|
||||||
|
resolve(json.map((val: any) => {
|
||||||
|
let entryl = new Entry();
|
||||||
|
entryl.deserialize(val.uid, val.content);
|
||||||
|
return entryl;
|
||||||
|
}));
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
export type byte = number;
|
||||||
|
export type base64 = string;
|
||||||
|
|
||||||
|
export function stringToByteArray(str: string): byte[] {
|
||||||
|
let ret = [];
|
||||||
|
for (let i = 0 ; i < str.length ; i++) {
|
||||||
|
ret.push(str.charCodeAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export const USER = 'test@localhost';
|
||||||
|
export const PASSWORD = 'SomePassword';
|
||||||
|
export const keyBase64 =
|
||||||
|
'Gpn6j6WJ/9JJbVkWhmEfZjlqSps5rwEOzjUOO0rqufvb4vtT4UfRgx0uMivuGwjF7/8Y1z1glIASX7Oz/4l2jucgf+lAzg2oTZFodWkXRZCDmFa7c9' +
|
||||||
|
'a8/04xIs7koFmUH34Rl9XXW6V2/GDVigQhQU8uWnrGo795tupoNQMbtB8RgMX5GyuxR55FvcybHpYBbwrDIsKvXcBxWFEscdNU8zyeq3yjvDo/W/y2' +
|
||||||
|
'4dApW3mnNo7vswoL2rpkZj3dqw==';
|
Loading…
Reference in New Issue