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