From c7d4a15f5fe716d4576d5ebe26688f3f3f1fed91 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 12 Nov 2019 09:55:35 +0200 Subject: [PATCH] Move the etesync-js API to its own repo. --- package.json | 4 +- src/Journals/ImportDialog.tsx | 2 +- src/Journals/JournalEdit.tsx | 2 +- src/Journals/JournalMemberAddDialog.tsx | 2 +- src/Journals/JournalMembers.tsx | 2 +- src/Journals/index.tsx | 2 +- src/Journals/journalView.tsx | 2 +- src/Pim/index.tsx | 2 +- src/SyncGate.tsx | 4 +- src/api/Constants.ts | 1 - src/api/Crypto.test.ts | 41 -- src/api/Crypto.ts | 165 ------- src/api/EteSync.test.ts | 168 ------- src/api/EteSync.ts | 623 ------------------------ src/api/Helpers.ts | 11 - src/api/TestConstants.ts | 6 - src/components/ContactEdit.tsx | 2 +- src/components/ErrorBoundary.tsx | 2 +- src/components/EventEdit.tsx | 2 +- src/components/JournalEntries.tsx | 2 +- src/components/TaskEdit.tsx | 2 +- src/etesync-helpers.ts | 2 +- src/journal-processors.ts | 2 +- src/store/actions.ts | 4 +- src/store/construct.ts | 2 +- src/store/index.test.ts | 2 +- src/store/reducers.ts | 2 +- src/widgets/PrettyFingerprint.tsx | 2 +- yarn.lock | 27 +- 29 files changed, 41 insertions(+), 1049 deletions(-) delete mode 100644 src/api/Constants.ts delete mode 100644 src/api/Crypto.test.ts delete mode 100644 src/api/Crypto.ts delete mode 100644 src/api/EteSync.test.ts delete mode 100644 src/api/EteSync.ts delete mode 100644 src/api/Helpers.ts delete mode 100644 src/api/TestConstants.ts diff --git a/package.json b/package.json index 8da1c29..397413e 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "dependencies": { "@material-ui/core": "^3.9.2", "@material-ui/icons": "^3.0.2", + "etesync": "^0.1.1", "ical.js": "^1.2.2", "immutable": "^4.0.0-rc.12", "localforage": "^1.7.3", "moment": "^2.24.0", - "node-rsa": "^1.0.3", "react": "^16.10.2", "react-big-calendar": "^0.20.3", "react-datetime": "^2.16.3", @@ -25,8 +25,6 @@ "redux-persist": "^5.10.0", "redux-thunk": "^2.3.0", "reselect": "^3.0.1", - "sjcl": "git+https://github.com/etesync/sjcl", - "urijs": "^1.19.1", "uuid": "^3.1.0" }, "scripts": { diff --git a/src/Journals/ImportDialog.tsx b/src/Journals/ImportDialog.tsx index 6ce2d32..6b9c939 100644 --- a/src/Journals/ImportDialog.tsx +++ b/src/Journals/ImportDialog.tsx @@ -16,7 +16,7 @@ import { SyncInfoJournal } from '../SyncGate'; import { store, CredentialsData, UserInfoData } from '../store'; import { addEntries } from '../store/actions'; import { createJournalEntry } from '../etesync-helpers'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import * as ICAL from 'ical.js'; import { ContactType, EventType, TaskType, PimType } from '../pim-types'; diff --git a/src/Journals/JournalEdit.tsx b/src/Journals/JournalEdit.tsx index a6a19a2..9f656b2 100644 --- a/src/Journals/JournalEdit.tsx +++ b/src/Journals/JournalEdit.tsx @@ -15,7 +15,7 @@ import AppBarOverride from '../widgets/AppBarOverride'; import Container from '../widgets/Container'; import ConfirmationDialog from '../widgets/ConfirmationDialog'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { SyncInfo } from '../SyncGate'; import { handleInputChange } from '../helpers'; diff --git a/src/Journals/JournalMemberAddDialog.tsx b/src/Journals/JournalMemberAddDialog.tsx index 2d3037d..1b9e5f6 100644 --- a/src/Journals/JournalMemberAddDialog.tsx +++ b/src/Journals/JournalMemberAddDialog.tsx @@ -6,7 +6,7 @@ import LoadingIndicator from '../widgets/LoadingIndicator'; import ConfirmationDialog from '../widgets/ConfirmationDialog'; import PrettyFingerprint from '../widgets/PrettyFingerprint'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { CredentialsData } from '../store'; import { handleInputChange } from '../helpers'; diff --git a/src/Journals/JournalMembers.tsx b/src/Journals/JournalMembers.tsx index cbad773..cc1b43d 100644 --- a/src/Journals/JournalMembers.tsx +++ b/src/Journals/JournalMembers.tsx @@ -13,7 +13,7 @@ import ConfirmationDialog from '../widgets/ConfirmationDialog'; import JournalMemberAddDialog from './JournalMemberAddDialog'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { CredentialsData, UserInfoData } from '../store'; import { SyncInfoJournal } from '../SyncGate'; diff --git a/src/Journals/index.tsx b/src/Journals/index.tsx index 2a34aae..6831f4f 100644 --- a/src/Journals/index.tsx +++ b/src/Journals/index.tsx @@ -13,7 +13,7 @@ import { store, JournalsData, UserInfoData, CredentialsData } from '../store'; import { addJournal, deleteJournal, updateJournal } from '../store/actions'; import { SyncInfo } from '../SyncGate'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; class Journals extends React.PureComponent { public props: { diff --git a/src/Journals/journalView.tsx b/src/Journals/journalView.tsx index 95ac41e..a858b4e 100644 --- a/src/Journals/journalView.tsx +++ b/src/Journals/journalView.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Route, Switch, withRouter } from 'react-router'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { routeResolver } from '../App'; diff --git a/src/Pim/index.tsx b/src/Pim/index.tsx index 1b549ac..4167e5c 100644 --- a/src/Pim/index.tsx +++ b/src/Pim/index.tsx @@ -9,7 +9,7 @@ import { RouteComponentProps, withRouter } from 'react-router'; import { Action } from 'redux-actions'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { createSelector } from 'reselect'; diff --git a/src/SyncGate.tsx b/src/SyncGate.tsx index 6457e2f..f474c91 100644 --- a/src/SyncGate.tsx +++ b/src/SyncGate.tsx @@ -19,8 +19,8 @@ import Journals from './Journals'; import Settings from './Settings'; import Pim from './Pim'; -import * as EteSync from './api/EteSync'; -import { CURRENT_VERSION } from './api/Constants'; +import * as EteSync from 'etesync'; +import { CURRENT_VERSION } from 'etesync'; import { store, SettingsType, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoType } from './store'; import { addJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions'; diff --git a/src/api/Constants.ts b/src/api/Constants.ts deleted file mode 100644 index 8a308aa..0000000 --- a/src/api/Constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const CURRENT_VERSION = 2; diff --git a/src/api/Crypto.test.ts b/src/api/Crypto.test.ts deleted file mode 100644 index 92c16ed..0000000 --- a/src/api/Crypto.test.ts +++ /dev/null @@ -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)); -}); diff --git a/src/api/Crypto.ts b/src/api/Crypto.ts deleted file mode 100644 index d5fba4e..0000000 --- a/src/api/Crypto.ts +++ /dev/null @@ -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')); - } -} diff --git a/src/api/EteSync.test.ts b/src/api/EteSync.test.ts deleted file mode 100644 index faf56b6..0000000 --- a/src/api/EteSync.test.ts +++ /dev/null @@ -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); -}); diff --git a/src/api/EteSync.ts b/src/api/EteSync.ts deleted file mode 100644 index dd80782..0000000 --- a/src/api/EteSync.ts +++ /dev/null @@ -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 { - 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 extends BaseItem { - 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 { - constructor(initial?: Partial, 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 { - 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 { - 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(segments: string[] = [], extra: RequestInit = {}, _apiBase: URI = this.apiBase): Promise { - 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 { - 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(segments: string[] = [], extra: RequestInit = {}, apiBase: any = this.apiBase): Promise { - 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 { - return new Promise((resolve, reject) => { - this.newCall([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 { - return new Promise((resolve, reject) => { - this.newCall().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([], extra); - } - - public update(journal: Journal): Promise<{}> { - const extra = { - method: 'put', - body: JSON.stringify(journal.serialize()), - }; - - return this.newCall([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 { - 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) => { - 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 { - return new Promise((resolve, reject) => { - this.newCall().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 { - return new Promise((resolve, reject) => { - this.newCall([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); - } -} diff --git a/src/api/Helpers.ts b/src/api/Helpers.ts deleted file mode 100644 index e966013..0000000 --- a/src/api/Helpers.ts +++ /dev/null @@ -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; -} diff --git a/src/api/TestConstants.ts b/src/api/TestConstants.ts deleted file mode 100644 index bf752ed..0000000 --- a/src/api/TestConstants.ts +++ /dev/null @@ -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=='; diff --git a/src/components/ContactEdit.tsx b/src/components/ContactEdit.tsx index 73e0a16..f6a52d8 100644 --- a/src/components/ContactEdit.tsx +++ b/src/components/ContactEdit.tsx @@ -19,7 +19,7 @@ import ConfirmationDialog from '../widgets/ConfirmationDialog'; import * as uuid from 'uuid'; import * as ICAL from 'ical.js'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { ContactType } from '../pim-types'; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 9ee09e8..c292693 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { store, persistor } from '../store'; import { resetKey } from '../store/actions'; -import { EncryptionPasswordError, IntegrityError } from '../api/EteSync'; +import { EncryptionPasswordError, IntegrityError } from 'etesync'; import PrettyError from '../widgets/PrettyError'; interface PropsType { diff --git a/src/components/EventEdit.tsx b/src/components/EventEdit.tsx index 90706ed..ab6d749 100644 --- a/src/components/EventEdit.tsx +++ b/src/components/EventEdit.tsx @@ -26,7 +26,7 @@ import { withRouter } from 'react-router'; import * as uuid from 'uuid'; import * as ICAL from 'ical.js'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { EventType } from '../pim-types'; diff --git a/src/components/JournalEntries.tsx b/src/components/JournalEntries.tsx index 6769861..73d04d2 100644 --- a/src/components/JournalEntries.tsx +++ b/src/components/JournalEntries.tsx @@ -15,7 +15,7 @@ import * as ICAL from 'ical.js'; import { TaskType, EventType, ContactType } from '../pim-types'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; class JournalEntries extends React.PureComponent { public static defaultProps = { diff --git a/src/components/TaskEdit.tsx b/src/components/TaskEdit.tsx index 84b8f4b..af8f631 100644 --- a/src/components/TaskEdit.tsx +++ b/src/components/TaskEdit.tsx @@ -26,7 +26,7 @@ import { withRouter } from 'react-router'; import * as uuid from 'uuid'; import * as ICAL from 'ical.js'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { TaskType, TaskStatusType } from '../pim-types'; diff --git a/src/etesync-helpers.ts b/src/etesync-helpers.ts index 8694bbf..d20efca 100644 --- a/src/etesync-helpers.ts +++ b/src/etesync-helpers.ts @@ -1,4 +1,4 @@ -import * as EteSync from './api/EteSync'; +import * as EteSync from 'etesync'; import { CredentialsData, UserInfoData } from './store'; import { addEntries } from './store/actions'; diff --git a/src/journal-processors.ts b/src/journal-processors.ts index 7c0a37b..ac32d75 100644 --- a/src/journal-processors.ts +++ b/src/journal-processors.ts @@ -4,7 +4,7 @@ import * as ICAL from 'ical.js'; import { EventType, ContactType, TaskType } from './pim-types'; -import * as EteSync from './api/EteSync'; +import * as EteSync from 'etesync'; export function syncEntriesToItemMap( collection: EteSync.CollectionInfo, entries: List, base: {[key: string]: ContactType} = {}) { diff --git a/src/store/actions.ts b/src/store/actions.ts index 1275a31..3677620 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,7 +1,7 @@ import { Action, createAction, createActions } from 'redux-actions'; -import * as EteSync from '../api/EteSync'; -import { UserInfo } from '../api/EteSync'; +import * as EteSync from 'etesync'; +import { UserInfo } from 'etesync'; import { CredentialsData, EntriesType, SettingsType } from './'; diff --git a/src/store/construct.ts b/src/store/construct.ts index ae61625..bb306af 100644 --- a/src/store/construct.ts +++ b/src/store/construct.ts @@ -5,7 +5,7 @@ import session from 'redux-persist/lib/storage/session'; import { List, Map as ImmutableMap } from 'immutable'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import { JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, JournalsFetchRecord, UserInfoFetchRecord, CredentialsTypeRemote, JournalsType, EntriesType, UserInfoType, SettingsType, diff --git a/src/store/index.test.ts b/src/store/index.test.ts index 0d60bc5..0cf793d 100644 --- a/src/store/index.test.ts +++ b/src/store/index.test.ts @@ -3,7 +3,7 @@ import { entries, EntriesTypeImmutable } from './reducers'; import { Map } from 'immutable'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; it('Entries reducer', () => { const jId = '24324324324'; diff --git a/src/store/reducers.ts b/src/store/reducers.ts index 52e2153..0936748 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -2,7 +2,7 @@ import { Action, ActionFunctionAny, combineActions, handleAction, handleActions import { List, Map as ImmutableMap, Record } from 'immutable'; -import * as EteSync from '../api/EteSync'; +import * as EteSync from 'etesync'; import * as actions from './actions'; diff --git a/src/widgets/PrettyFingerprint.tsx b/src/widgets/PrettyFingerprint.tsx index 8059b52..73c65d6 100644 --- a/src/widgets/PrettyFingerprint.tsx +++ b/src/widgets/PrettyFingerprint.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import sjcl from 'sjcl'; -import { byte, base64 } from '../api/Helpers'; +import { byte, base64 } from 'etesync'; function byteArray4ToNumber(bytes: byte[], offset: number) { // tslint:disable:no-bitwise diff --git a/yarn.lock b/yarn.lock index 23eafed..ec68aeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4062,6 +4062,15 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 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: version "4.0.0" 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: semver "^6.3.0" -node-rsa@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.0.3.tgz#e2d23be0096caeca014b139af480ca7bc614a589" - integrity sha512-gQowjnOunjmojrpO+d8x1ubL9X2Zpj4MRmY2J2hPtVF8g1VgOX1yNWUeCCoyzkRHunJf1/3orLzit5PiRtDz1A== +node-rsa@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/node-rsa/-/node-rsa-1.0.6.tgz#47d22eba8b41192cd6f06db15870c67126f00aa4" + integrity sha512-v42495lozKpuQmrcIzld9ds/Tn7pwjuh0BHSHnhPrKkAVSyTAyrZodFLFafOfWiUKamLt4lgWdngP8W/LzCm2w== dependencies: asn1 "^0.2.4" @@ -9487,9 +9496,9 @@ sisteransi@^1.0.3: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== -"sjcl@git+https://github.com/etesync/sjcl": +"sjcl@git+https://github.com/etesync/sjcl.git": 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: version "1.0.0" @@ -10363,9 +10372,9 @@ uri-js@^4.2.2: punycode "^2.1.0" urijs@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" - integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg== + version "1.19.2" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a" + integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w== urix@^0.1.0: version "0.1.0"