etebase
Version:
Etebase TypeScript API for the web and node
524 lines • 23.2 kB
JavaScript
import URI from "urijs";
import * as Constants from "./Constants";
import { deriveKey, concatArrayBuffers, BoxCryptoManager, ready } from "./Crypto";
export { ready, getPrettyFingerprint, _setRnSodium, deriveKey, KeyDerivationDifficulty } from "./Crypto";
import { ConflictError, UnauthorizedError } from "./Exceptions";
export * from "./Exceptions";
import { fromBase64, toBase64, fromString, toString, randomBytes, symmetricKeyLength, msgpackEncode, msgpackDecode, bufferUnpad } from "./Helpers";
export { fromBase64, toBase64, randomBytes } from "./Helpers";
import { EncryptedCollection, EncryptedCollectionItem, getMainCryptoManager, StorageCryptoManager, } from "./EncryptedModels";
export * from "./EncryptedModels"; // FIXME: cherry-pick what we export
import { Authenticator, CollectionManagerOnline, CollectionItemManagerOnline, CollectionInvitationManagerOnline, CollectionMemberManagerOnline, } from "./OnlineManagers";
import { ProgrammingError } from "./Exceptions";
import { CURRENT_VERSION } from "./Constants";
export { CURRENT_VERSION } from "./Constants";
export class Account {
constructor(mainEncryptionKey, version) {
this.mainKey = mainEncryptionKey;
this.version = version;
this.authToken = null;
}
static async isEtebaseServer(serverUrl) {
const authenticator = new Authenticator(serverUrl);
return authenticator.isEtebase();
}
static async signup(user, password, serverUrl) {
await ready;
serverUrl = serverUrl !== null && serverUrl !== void 0 ? serverUrl : Constants.SERVER_URL;
const authenticator = new Authenticator(serverUrl);
const version = CURRENT_VERSION;
const salt = randomBytes(32);
const mainKey = await deriveKey(salt, password);
const mainCryptoManager = getMainCryptoManager(mainKey, version);
const loginCryptoManager = mainCryptoManager.getLoginCryptoManager();
const identityCryptoManager = BoxCryptoManager.keygen();
const accountKey = randomBytes(symmetricKeyLength);
const encryptedContent = mainCryptoManager.encrypt(concatArrayBuffers(accountKey, identityCryptoManager.privkey));
const loginResponse = await authenticator.signup(user, salt, loginCryptoManager.pubkey, identityCryptoManager.pubkey, encryptedContent);
const ret = new this(mainKey, version);
ret.user = loginResponse.user;
ret.authToken = loginResponse.token;
ret.serverUrl = serverUrl;
return ret;
}
static async login(username, password, serverUrl) {
var _a;
await ready;
serverUrl = serverUrl !== null && serverUrl !== void 0 ? serverUrl : Constants.SERVER_URL;
const authenticator = new Authenticator(serverUrl);
let loginChallenge;
try {
loginChallenge = await authenticator.getLoginChallenge(username);
}
catch (e) {
if ((e instanceof UnauthorizedError) && (((_a = e.content) === null || _a === void 0 ? void 0 : _a.code) === "user_not_init")) {
const user = {
username,
email: "init@localhost",
};
return this.signup(user, password, serverUrl);
}
throw e;
}
const mainKey = await deriveKey(loginChallenge.salt, password);
const mainCryptoManager = getMainCryptoManager(mainKey, loginChallenge.version);
const loginCryptoManager = mainCryptoManager.getLoginCryptoManager();
const response = msgpackEncode({
username,
challenge: loginChallenge.challenge,
host: URI(serverUrl).host(),
action: "login",
});
const loginResponse = await authenticator.login(response, loginCryptoManager.signDetached(response));
const ret = new this(mainKey, loginChallenge.version);
ret.user = loginResponse.user;
ret.authToken = loginResponse.token;
ret.serverUrl = serverUrl;
return ret;
}
async fetchToken() {
const serverUrl = this.serverUrl;
const authenticator = new Authenticator(serverUrl);
const username = this.user.username;
const loginChallenge = await authenticator.getLoginChallenge(username);
const mainKey = this.mainKey;
const mainCryptoManager = getMainCryptoManager(mainKey, loginChallenge.version);
const loginCryptoManager = mainCryptoManager.getLoginCryptoManager();
const response = msgpackEncode({
username,
challenge: loginChallenge.challenge,
host: URI(serverUrl).host(),
action: "login",
});
const loginResponse = await authenticator.login(response, loginCryptoManager.signDetached(response));
this.authToken = loginResponse.token;
}
async logout() {
const authenticator = new Authenticator(this.serverUrl);
authenticator.logout(this.authToken);
this.version = -1;
this.mainKey = new Uint8Array();
this.authToken = null;
}
async changePassword(password) {
const serverUrl = this.serverUrl;
const authenticator = new Authenticator(serverUrl);
const username = this.user.username;
const loginChallenge = await authenticator.getLoginChallenge(username);
const oldMainCryptoManager = getMainCryptoManager(this.mainKey, this.version);
const content = oldMainCryptoManager.decrypt(this.user.encryptedContent);
const oldLoginCryptoManager = oldMainCryptoManager.getLoginCryptoManager();
const mainKey = await deriveKey(loginChallenge.salt, password);
const mainCryptoManager = getMainCryptoManager(mainKey, this.version);
const loginCryptoManager = mainCryptoManager.getLoginCryptoManager();
const encryptedContent = mainCryptoManager.encrypt(content);
const response = msgpackEncode({
username,
challenge: loginChallenge.challenge,
host: URI(serverUrl).host(),
action: "changePassword",
loginPubkey: loginCryptoManager.pubkey,
encryptedContent: encryptedContent,
});
await authenticator.changePassword(this.authToken, response, oldLoginCryptoManager.signDetached(response));
this.mainKey = mainKey;
this.user.encryptedContent = encryptedContent;
}
async getDashboardUrl() {
const serverUrl = this.serverUrl;
const authenticator = new Authenticator(serverUrl);
return await authenticator.getDashboardUrl(this.authToken);
}
async save(encryptionKey_) {
const version = CURRENT_VERSION;
const encryptionKey = encryptionKey_ !== null && encryptionKey_ !== void 0 ? encryptionKey_ : new Uint8Array(32);
const cryptoManager = new StorageCryptoManager(encryptionKey, version);
const content = {
user: this.user,
authToken: this.authToken,
serverUrl: this.serverUrl,
version: this.version,
key: cryptoManager.encrypt(this.mainKey),
};
const ret = {
version,
encryptedData: cryptoManager.encrypt(msgpackEncode(content), new Uint8Array([version])),
};
return toBase64(msgpackEncode(ret));
}
static async restore(accountDataStored_, encryptionKey_) {
var _a;
await ready;
const encryptionKey = encryptionKey_ !== null && encryptionKey_ !== void 0 ? encryptionKey_ : new Uint8Array(32);
const accountDataStored = msgpackDecode(fromBase64(accountDataStored_));
const cryptoManager = new StorageCryptoManager(encryptionKey, accountDataStored.version);
const accountData = msgpackDecode(cryptoManager.decrypt(accountDataStored.encryptedData, new Uint8Array([accountDataStored.version])));
const ret = new this(cryptoManager.decrypt(accountData.key), accountData.version);
ret.user = accountData.user;
ret.authToken = (_a = accountData.authToken) !== null && _a !== void 0 ? _a : null;
ret.serverUrl = accountData.serverUrl;
return ret;
}
getCollectionManager() {
return new CollectionManager(this);
}
getInvitationManager() {
return new CollectionInvitationManager(this);
}
_getCryptoManager() {
// FIXME: cache this
const mainCryptoManager = getMainCryptoManager(this.mainKey, this.version);
const content = mainCryptoManager.decrypt(this.user.encryptedContent);
return mainCryptoManager.getAccountCryptoManager(content.subarray(0, symmetricKeyLength));
}
_getIdentityCryptoManager() {
// FIXME: cache this
const mainCryptoManager = getMainCryptoManager(this.mainKey, this.version);
const content = mainCryptoManager.decrypt(this.user.encryptedContent);
return mainCryptoManager.getIdentityCryptoManager(content.subarray(symmetricKeyLength));
}
}
const defaultCacheOptions = {
saveContent: true,
};
export class CollectionManager {
constructor(etebase) {
this.etebase = etebase;
this.onlineManager = new CollectionManagerOnline(this.etebase);
}
async create(colType, meta, content) {
const uintcontent = (content instanceof Uint8Array) ? content : fromString(content);
const mainCryptoManager = this.etebase._getCryptoManager();
const encryptedCollection = await EncryptedCollection.create(mainCryptoManager, colType, meta, uintcontent);
return new Collection(encryptedCollection.getCryptoManager(mainCryptoManager), encryptedCollection);
}
async fetch(colUid, options) {
const mainCryptoManager = this.etebase._getCryptoManager();
const encryptedCollection = await this.onlineManager.fetch(colUid, options);
return new Collection(encryptedCollection.getCryptoManager(mainCryptoManager), encryptedCollection);
}
async list(colType, options) {
const mainCryptoManager = this.etebase._getCryptoManager();
const colTypes = (Array.isArray(colType)) ? colType : [colType];
const collectionTypes = colTypes.map((x) => mainCryptoManager.colTypeToUid(x));
const ret = await this.onlineManager.list(collectionTypes, options);
return {
...ret,
data: ret.data.map((x) => new Collection(x.getCryptoManager(mainCryptoManager), x)),
};
}
async upload(collection, options) {
const col = collection.encryptedCollection;
// If we have a etag, it means we previously fetched it.
if (col.lastEtag) {
const itemOnlineManager = new CollectionItemManagerOnline(this.etebase, col.uid);
await itemOnlineManager.batch([col.item], undefined, options);
}
else {
await this.onlineManager.create(col, options);
}
col.__markSaved();
}
async transaction(collection, options) {
const col = collection.encryptedCollection;
// If we have a etag, it means we previously fetched it.
if (col.lastEtag) {
const itemOnlineManager = new CollectionItemManagerOnline(this.etebase, col.uid);
await itemOnlineManager.transaction([col.item], undefined, options);
}
else {
await this.onlineManager.create(col, options);
}
col.__markSaved();
}
cacheSave(collection, options = defaultCacheOptions) {
return collection.encryptedCollection.cacheSave(options.saveContent);
}
cacheLoad(cache) {
const encCol = EncryptedCollection.cacheLoad(cache);
const mainCryptoManager = this.etebase._getCryptoManager();
return new Collection(encCol.getCryptoManager(mainCryptoManager), encCol);
}
getItemManager(col_) {
const col = col_.encryptedCollection;
const collectionCryptoManager = col.getCryptoManager(this.etebase._getCryptoManager());
return new ItemManager(this.etebase, collectionCryptoManager, col.uid);
}
getMemberManager(col) {
return new CollectionMemberManager(this.etebase, this, col.encryptedCollection);
}
}
export class ItemManager {
constructor(etebase, collectionCryptoManager, colUid) {
this.collectionCryptoManager = collectionCryptoManager;
this.onlineManager = new CollectionItemManagerOnline(etebase, colUid);
this.collectionUid = colUid;
}
async create(meta, content) {
const uintcontent = (content instanceof Uint8Array) ? content : fromString(content);
const encryptedItem = await EncryptedCollectionItem.create(this.collectionCryptoManager, meta, uintcontent);
return new Item(this.collectionUid, encryptedItem.getCryptoManager(this.collectionCryptoManager), encryptedItem);
}
async fetch(itemUid, options) {
const encryptedItem = await this.onlineManager.fetch(itemUid, options);
return new Item(this.collectionUid, encryptedItem.getCryptoManager(this.collectionCryptoManager), encryptedItem);
}
async list(options) {
const ret = await this.onlineManager.list(options);
return {
...ret,
data: ret.data.map((x) => new Item(this.collectionUid, x.getCryptoManager(this.collectionCryptoManager), x)),
};
}
async itemRevisions(item, options) {
const ret = await this.onlineManager.itemRevisions(item.encryptedItem, options);
return {
...ret,
data: ret.data.map((x) => new Item(this.collectionUid, x.getCryptoManager(this.collectionCryptoManager), x)),
};
}
// Prepare the items for upload and verify they belong to the right collection
itemsPrepareForUpload(items) {
return items === null || items === void 0 ? void 0 : items.map((x) => {
if (x.collectionUid !== this.collectionUid) {
throw new ProgrammingError(`Uploading an item belonging to collection ${x.collectionUid} to another collection (${this.collectionUid}) is not allowed!`);
}
return x.encryptedItem;
});
}
async fetchUpdates(items, options) {
const ret = await this.onlineManager.fetchUpdates(this.itemsPrepareForUpload(items), options);
return {
...ret,
data: ret.data.map((x) => new Item(this.collectionUid, x.getCryptoManager(this.collectionCryptoManager), x)),
};
}
async fetchMulti(items, options) {
const ret = await this.onlineManager.fetchMulti(items, options);
return {
...ret,
data: ret.data.map((x) => new Item(this.collectionUid, x.getCryptoManager(this.collectionCryptoManager), x)),
};
}
async batch(items, deps, options) {
await this.onlineManager.batch(this.itemsPrepareForUpload(items), this.itemsPrepareForUpload(deps), options);
items.forEach((item) => {
item.encryptedItem.__markSaved();
});
}
async transaction(items, deps, options) {
await this.onlineManager.transaction(this.itemsPrepareForUpload(items), this.itemsPrepareForUpload(deps), options);
items.forEach((item) => {
item.encryptedItem.__markSaved();
});
}
async uploadContent(item) {
const [encryptedItem] = this.itemsPrepareForUpload([item]);
const pendingChunks = encryptedItem.__getPendingChunks();
for (const chunk of pendingChunks) {
// FIXME: Upload multiple in parallel
try {
await this.onlineManager.chunkUpload(encryptedItem, chunk);
}
catch (e) {
if (e instanceof ConflictError) {
// Skip if we arleady have the chunk
continue;
}
throw e;
}
}
}
async downloadContent(item) {
const [encryptedItem] = this.itemsPrepareForUpload([item]);
const missingChunks = encryptedItem.__getMissingChunks();
for (const chunk of missingChunks) {
if (!chunk[1]) {
// FIXME: Download in parallel
chunk[1] = await this.onlineManager.chunkDownload(encryptedItem, chunk[0]);
}
}
}
async subscribeChanges(cb, options) {
return this.onlineManager.subscribeChanges(async (ret) => {
cb({
...ret,
data: ret.data.map((x) => new Item(this.collectionUid, x.getCryptoManager(this.collectionCryptoManager), x)),
});
}, options);
}
cacheSave(item, options = defaultCacheOptions) {
return item.encryptedItem.cacheSave(options.saveContent);
}
cacheLoad(cache) {
const encItem = EncryptedCollectionItem.cacheLoad(cache);
return new Item(this.collectionUid, encItem.getCryptoManager(this.collectionCryptoManager), encItem);
}
}
export class CollectionInvitationManager {
constructor(etebase) {
this.etebase = etebase;
this.onlineManager = new CollectionInvitationManagerOnline(this.etebase);
}
async listIncoming(options) {
return await this.onlineManager.listIncoming(options);
}
async listOutgoing(options) {
return await this.onlineManager.listOutgoing(options);
}
async accept(invitation) {
const mainCryptoManager = this.etebase._getCryptoManager();
const identCryptoManager = this.etebase._getIdentityCryptoManager();
const content = msgpackDecode(bufferUnpad(identCryptoManager.decrypt(invitation.signedEncryptionKey, invitation.fromPubkey)));
const colTypeUid = mainCryptoManager.colTypeToUid(content.collectionType);
const encryptedEncryptionKey = mainCryptoManager.encrypt(content.encryptionKey, colTypeUid);
return this.onlineManager.accept(invitation, colTypeUid, encryptedEncryptionKey);
}
async reject(invitation) {
return this.onlineManager.reject(invitation);
}
async fetchUserProfile(username) {
return await this.onlineManager.fetchUserProfile(username);
}
async invite(col, username, pubkey, accessLevel) {
const mainCryptoManager = this.etebase._getCryptoManager();
const identCryptoManager = this.etebase._getIdentityCryptoManager();
const invitation = await col.encryptedCollection.createInvitation(mainCryptoManager, identCryptoManager, username, pubkey, accessLevel);
await this.onlineManager.invite(invitation);
}
async disinvite(invitation) {
return this.onlineManager.disinvite(invitation);
}
get pubkey() {
const identCryptoManager = this.etebase._getIdentityCryptoManager();
return identCryptoManager.pubkey;
}
}
export class CollectionMemberManager {
constructor(etebase, _collectionManager, encryptedCollection) {
this.etebase = etebase;
this.onlineManager = new CollectionMemberManagerOnline(this.etebase, encryptedCollection.uid);
}
async list(options) {
return this.onlineManager.list(options);
}
async remove(username) {
return this.onlineManager.remove(username);
}
async leave() {
return this.onlineManager.leave();
}
async modifyAccessLevel(username, accessLevel) {
return this.onlineManager.modifyAccessLevel(username, accessLevel);
}
}
export var OutputFormat;
(function (OutputFormat) {
OutputFormat[OutputFormat["Uint8Array"] = 0] = "Uint8Array";
OutputFormat[OutputFormat["String"] = 1] = "String";
})(OutputFormat || (OutputFormat = {}));
export class Collection {
constructor(cryptoManager, encryptedCollection) {
this.cryptoManager = cryptoManager;
this.encryptedCollection = encryptedCollection;
}
verify() {
return this.encryptedCollection.verify(this.cryptoManager);
}
setMeta(meta) {
this.encryptedCollection.setMeta(this.cryptoManager, meta);
}
getMeta() {
return this.encryptedCollection.getMeta(this.cryptoManager);
}
async setContent(content) {
const uintcontent = (content instanceof Uint8Array) ? content : fromString(content);
await this.encryptedCollection.setContent(this.cryptoManager, uintcontent);
}
async getContent(outputFormat = OutputFormat.Uint8Array) {
const ret = await this.encryptedCollection.getContent(this.cryptoManager);
switch (outputFormat) {
case OutputFormat.Uint8Array:
return ret;
case OutputFormat.String:
return toString(ret);
default:
throw new Error("Bad output format");
}
}
delete(preserveContent = false) {
this.encryptedCollection.delete(this.cryptoManager, preserveContent);
}
get uid() {
return this.encryptedCollection.uid;
}
get etag() {
return this.encryptedCollection.etag;
}
get isDeleted() {
return this.encryptedCollection.isDeleted;
}
get stoken() {
return this.encryptedCollection.stoken;
}
get accessLevel() {
return this.encryptedCollection.accessLevel;
}
getCollectionType() {
return this.encryptedCollection.getCollectionType(this.cryptoManager.accountCryptoManager);
}
get item() {
const encryptedItem = this.encryptedCollection.item;
return new Item(this.uid, encryptedItem.getCryptoManager(this.cryptoManager), encryptedItem);
}
}
export class Item {
constructor(collectionUid, cryptoManager, encryptedItem) {
this.cryptoManager = cryptoManager;
this.encryptedItem = encryptedItem;
this.collectionUid = collectionUid;
}
verify() {
return this.encryptedItem.verify(this.cryptoManager);
}
setMeta(meta) {
this.encryptedItem.setMeta(this.cryptoManager, meta);
}
getMeta() {
return this.encryptedItem.getMeta(this.cryptoManager);
}
async setContent(content) {
const uintcontent = (content instanceof Uint8Array) ? content : fromString(content);
await this.encryptedItem.setContent(this.cryptoManager, uintcontent);
}
async getContent(outputFormat = OutputFormat.Uint8Array) {
const ret = await this.encryptedItem.getContent(this.cryptoManager);
switch (outputFormat) {
case OutputFormat.Uint8Array:
return ret;
case OutputFormat.String:
return toString(ret);
default:
throw new Error("Bad output format");
}
}
delete(preserveContent = false) {
this.encryptedItem.delete(this.cryptoManager, preserveContent);
}
get uid() {
return this.encryptedItem.uid;
}
get etag() {
return this.encryptedItem.etag;
}
get isDeleted() {
return this.encryptedItem.isDeleted;
}
get isMissingContent() {
return this.encryptedItem.isMissingContent;
}
_clone() {
return new Item(this.collectionUid, this.cryptoManager, EncryptedCollectionItem.deserialize(this.encryptedItem.serialize()));
}
}
//# sourceMappingURL=Etebase.js.map