Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.mdns.eu/nextcloud/passwords-client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarius David Wieschollek <passwords.public@mdns.eu>2023-11-19 16:25:33 +0300
committerMarius David Wieschollek <passwords.public@mdns.eu>2023-11-19 16:25:33 +0300
commitd19cf0f24a197a4683a653c9b6d6ee5d415e036a (patch)
tree4cdfcdef39a9d5b0cb0639b0e2922da5bdaf9db6
parent1f18b35269cb8e68cd54c1aed57f5984d9535415 (diff)
Add EncryptionService and MODEL_TYPE
Signed-off-by: Marius David Wieschollek <passwords.public@mdns.eu>
-rw-r--r--package.json23
-rw-r--r--src/ClassLoader/BasicClassLoader.js2
-rw-r--r--src/ClassLoader/DefaultClassLoader.js2
-rw-r--r--src/Collection/AbstractCollection.js57
-rw-r--r--src/Encryption/CSEv1Encryption.js6
-rw-r--r--src/Event/EventEmitter.js127
-rw-r--r--src/Model/CustomField/AbstractField.js2
-rw-r--r--src/Model/Folder/Folder.js2
-rw-r--r--src/Model/Password/Password.js2
-rw-r--r--src/Model/Server/Server.js2
-rw-r--r--src/Model/Session/Session.js2
-rw-r--r--src/Model/Setting/Setting.js2
-rw-r--r--src/Model/Tag/Tag.js2
-rw-r--r--src/Services/EncryptionService.js91
14 files changed, 306 insertions, 16 deletions
diff --git a/package.json b/package.json
index 25e4258..317fdd9 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,21 @@
{
"name": "passwords-client",
- "version": "1.0.0.BUILD",
+ "version": "1.0.0",
"description": "JS client library for the Nextcloud Passwords app",
"main": "src/main.js",
"exports": {
- ".": "src/main.js",
- "./models": "src/models.js",
- "./errors": "src/errors.js",
- "./utility": "src/utility.js",
- "./boolean-state": "src/State/BooleanState.js",
- "./basic-class-loader": "src/ClassLoader/BasicClassLoader.js",
- "./default-class-loader": "src/ClassLoader/DefaultClassLoader.js",
- "./enhanced-class-loader": "src/ClassLoader/EnhancedClassLoader.js"
+ ".": "./src/main.js",
+ "./models": "./src/models.js",
+ "./errors": "./src/errors.js",
+ "./utility": "./src/utility.js",
+ "./passlink": "./src/PassLink/PassLink",
+ "./boolean-state": "./src/State/BooleanState.js",
+ "./event-emitter": "./src/Event/EventEmitter.js",
+ "./client": "./src/Client/PasswordsClient.js",
+ "./basic-client": "./src/Client/BasicPasswordsClient.js",
+ "./basic-class-loader": "./src/ClassLoader/BasicClassLoader.js",
+ "./default-class-loader": "./src/ClassLoader/DefaultClassLoader.js",
+ "./enhanced-class-loader": "./src/ClassLoader/EnhancedClassLoader.js"
},
"author": "Marius Wieschollek",
"license": "ISC",
@@ -26,7 +30,6 @@
"url": "https://git.mdns.eu/nextcloud/passwords-client.git"
},
"dependencies": {
- "eventemitter3": "^4.0.7",
"libsodium": "0.7.10",
"libsodium-wrappers": "0.7.10",
"url-parse": "^1.5.3",
diff --git a/src/ClassLoader/BasicClassLoader.js b/src/ClassLoader/BasicClassLoader.js
index d4ef1e8..43b9aa2 100644
--- a/src/ClassLoader/BasicClassLoader.js
+++ b/src/ClassLoader/BasicClassLoader.js
@@ -1,5 +1,3 @@
-import ObjectMerger from "../Utility/ObjectMerger";
-
export default class BasicClassLoader {
constructor(classes = {}) {
diff --git a/src/ClassLoader/DefaultClassLoader.js b/src/ClassLoader/DefaultClassLoader.js
index 00ff4b3..16ff9ec 100644
--- a/src/ClassLoader/DefaultClassLoader.js
+++ b/src/ClassLoader/DefaultClassLoader.js
@@ -36,7 +36,6 @@ import ExportV1Encryption from "../Encryption/ExportV1Encryption";
import CSEv1Keychain from "../Encryption/Keychain/CSEv1Keychain";
import Cache from "../Cache/Cache";
import BooleanState from "../State/BooleanState";
-import EventEmitter from "eventemitter3";
import ResponseContentTypeError from "../Exception/ResponseContentTypeError";
import ResponseDecodingError from "../Exception/ResponseDecodingError";
import UnknownPropertyError from "../Exception/UnknownPropertyError";
@@ -67,6 +66,7 @@ import HashService from "../Services/HashService";
import Logger from "../Logger/Logger";
import DefectField from "../Model/CustomField/DefectField";
import PreconditionFailedError from "../Exception/Http/PreconditionFailedError";
+import EventEmitter from "../Event/EventEmitter";
export default class DefaultClassLoader extends BasicClassLoader {
diff --git a/src/Collection/AbstractCollection.js b/src/Collection/AbstractCollection.js
index d7de25c..ac00ccb 100644
--- a/src/Collection/AbstractCollection.js
+++ b/src/Collection/AbstractCollection.js
@@ -82,6 +82,63 @@ export default class AbstractCollection {
}
/**
+ * Get the first item from the collection
+ *
+ * @returns {AbstractModel|null}
+ */
+ first() {
+ if(this._elements.length === 0) return null;
+ return this._elements[0];
+ }
+
+ /**
+ * Get the last item from the collection
+ *
+ * @returns {AbstractModel|null}
+ */
+ last() {
+ if(this._elements.length === 0) return null;
+ return this._elements[this._elements.length - 1];
+ }
+
+ /**
+ * Applies the callback to every item in the collection and returns the result
+ *
+ * @param {Function} callback
+ * @returns {Array<*>}
+ */
+ map(callback) {
+ let items = [],
+ collection = this.getReference();
+
+ for(let item of collection) {
+ items.push(callback(item));
+ }
+
+ return items;
+ }
+
+ /**
+ * Get a reference to the internal items array
+ *
+ * @returns {AbstractModel[]}
+ * @api
+ */
+ getReference() {
+ return this._elements;
+ }
+
+ /**
+ * Get a clone of the internal items array
+ *
+ * @returns {AbstractModel[]}
+ * @api
+ */
+ getClone() {
+ return this._elements.slice(0);
+ }
+
+ /**
*
* @param {(AbstractModel|AbstractModel[])} elements
*/
diff --git a/src/Encryption/CSEv1Encryption.js b/src/Encryption/CSEv1Encryption.js
index 10e29a9..9005947 100644
--- a/src/Encryption/CSEv1Encryption.js
+++ b/src/Encryption/CSEv1Encryption.js
@@ -50,7 +50,7 @@ export default class CSEv1Encryption {
key = this._keychain.getCurrentKey();
for(let field of fields) {
- let data = object[field];
+ let data = object[field];
if(data === null || data === undefined || data.length === 0) continue;
object[field] = this._encryptString(data, key);
@@ -78,7 +78,7 @@ export default class CSEv1Encryption {
key = this._keychain.getKey(object.cseKey);
for(let field of fields) {
- let data = object[field];
+ let data = object[field];
if(data === null || data.length === 0) continue;
object[field] = this._decryptString(data, key);
@@ -120,7 +120,7 @@ export default class CSEv1Encryption {
/**
* Encrypt the message with the given key
*
- * @param {Uint8Array} message
+ * @param {String} message
* @param {Uint8Array} key
* @returns {Uint8Array}
* @private
diff --git a/src/Event/EventEmitter.js b/src/Event/EventEmitter.js
new file mode 100644
index 0000000..3a6db25
--- /dev/null
+++ b/src/Event/EventEmitter.js
@@ -0,0 +1,127 @@
+export default class EventEmitter {
+
+ constructor() {
+ this._listeners = {};
+ this._once = {};
+ }
+
+ /**
+ * @param {String|String[]} event
+ * @param {Object} data
+ *
+ * @return {Promise<void>}
+ */
+ async emit(event, data) {
+ if(Array.isArray(event)) {
+ event.forEach((event) => {this.emit(event, data);});
+ return;
+ }
+
+ await this._notifyListeners(event, data);
+ await this._notifyOnce(event, data);
+ }
+
+ /**
+ * @param {String|String[]} event
+ * @param {Function} callback
+ *
+ * @return EventEmitter
+ */
+ on(event, callback) {
+ if(Array.isArray(event)) {
+ event.forEach((event) => {this.on(event, callback);});
+ return this;
+ }
+
+ if(!this._listeners.hasOwnProperty(event)) {
+ this._listeners[event] = [];
+ }
+
+ this._listeners[event].push(callback);
+
+ return this;
+ }
+
+ /**
+ * @param {String|String[]} event
+ * @param {Function} callback
+ *
+ * @return EventEmitter
+ */
+ off(event, callback) {
+ if(Array.isArray(event)) {
+ event.forEach((event) => {this.off(event, callback);});
+ return this;
+ }
+
+ if(!this._listeners.hasOwnProperty(event)) {
+ return this;
+ }
+
+ for(let i = 0; i < this._listeners[event].length; i++) {
+ if(this._listeners[event][i] === callback) {
+ this._listeners[event].splice(i, 1);
+ i--;
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * @param {String} event
+ * @param {Function} callback
+ *
+ * @return EventEmitter
+ */
+ once(event, callback) {
+ if(!this._once.hasOwnProperty(event)) {
+ this._once[event] = [];
+ }
+
+ this._once[event].push(callback);
+
+ return this;
+ }
+
+ /**
+ * @param {String} event
+ * @param {Object} data
+ * @return {Promise<void>}
+ * @private
+ */
+ async _notifyListeners(event, data) {
+ if(!this._listeners.hasOwnProperty(event)) {
+ return;
+ }
+
+ for(let callback of this._listeners[event]) {
+ try {
+ await callback(data);
+ } catch(e) {
+ console.error(e);
+ }
+ }
+ }
+
+ /**
+ * @param {String} event
+ * @param {Object} data
+ * @return {Promise<void>}
+ * @private
+ */
+ async _notifyOnce(event, data) {
+ if(!this._once.hasOwnProperty(event)) {
+ return;
+ }
+
+ let callback;
+ while(callback = this._once[event].pop()) {
+ try {
+ await callback(data);
+ } catch(e) {
+ console.error(e);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Model/CustomField/AbstractField.js b/src/Model/CustomField/AbstractField.js
index d1576d7..cd6c250 100644
--- a/src/Model/CustomField/AbstractField.js
+++ b/src/Model/CustomField/AbstractField.js
@@ -2,6 +2,8 @@ import AbstractModel from '../AbstractModel';
export default class AbstractField extends AbstractModel {
+ get MODEL_TYPE() {return 'custom-field';}
+
/**
* @param {String} value
*/
diff --git a/src/Model/Folder/Folder.js b/src/Model/Folder/Folder.js
index 46c4b76..531629e 100644
--- a/src/Model/Folder/Folder.js
+++ b/src/Model/Folder/Folder.js
@@ -3,6 +3,8 @@ import AbstractRevisionModel from '../AbstractRevisionModel';
export default class Folder extends AbstractRevisionModel {
+ get MODEL_TYPE() {return 'folder';}
+
/**
*
* @param {Object} [data={}]
diff --git a/src/Model/Password/Password.js b/src/Model/Password/Password.js
index da2692f..1a64a7d 100644
--- a/src/Model/Password/Password.js
+++ b/src/Model/Password/Password.js
@@ -3,6 +3,8 @@ import AbstractRevisionModel from '../AbstractRevisionModel';
export default class Password extends AbstractRevisionModel {
+ get MODEL_TYPE() {return 'password';}
+
/**
*
* @param {Object} [data={}]
diff --git a/src/Model/Server/Server.js b/src/Model/Server/Server.js
index 6c36ba3..792ac6b 100644
--- a/src/Model/Server/Server.js
+++ b/src/Model/Server/Server.js
@@ -5,6 +5,8 @@ import ObjectMerger from '../../Utility/ObjectMerger';
export default class Server extends AbstractModel {
+ get MODEL_TYPE() {return 'server';}
+
/**
*
* @param {Object} data
diff --git a/src/Model/Session/Session.js b/src/Model/Session/Session.js
index 1a2bcf4..b7653f6 100644
--- a/src/Model/Session/Session.js
+++ b/src/Model/Session/Session.js
@@ -1,5 +1,7 @@
export default class Session {
+ get MODEL_TYPE() {return 'session';}
+
constructor(user = null, token = null, id = null, authorized = false) {
this._user = user;
this._token = token;
diff --git a/src/Model/Setting/Setting.js b/src/Model/Setting/Setting.js
index e790f89..4d4d65d 100644
--- a/src/Model/Setting/Setting.js
+++ b/src/Model/Setting/Setting.js
@@ -2,6 +2,8 @@ import InvalidScopeError from '../../Exception/InvalidScopeError';
export default class Setting {
+ get MODEL_TYPE() {return 'setting';}
+
/**
* @return {String}
*/
diff --git a/src/Model/Tag/Tag.js b/src/Model/Tag/Tag.js
index 1cff8b7..b8f851e 100644
--- a/src/Model/Tag/Tag.js
+++ b/src/Model/Tag/Tag.js
@@ -3,6 +3,8 @@ import AbstractRevisionModel from '../AbstractRevisionModel';
export default class Tag extends AbstractRevisionModel {
+ get MODEL_TYPE() {return 'tag';}
+
/**
*
* @param {Object} [data={}]
diff --git a/src/Services/EncryptionService.js b/src/Services/EncryptionService.js
new file mode 100644
index 0000000..7f81498
--- /dev/null
+++ b/src/Services/EncryptionService.js
@@ -0,0 +1,91 @@
+import sodium from 'libsodium-wrappers';
+
+export default class EncryptionService {
+
+ constructor() {
+ }
+
+ /**
+ * Encrypt the message with the given key and return a hex encoded string
+ *
+ * @param {String} message
+ * @param {String} key
+ * @returns {String}
+ * @private
+ */
+ encrypt(message, passphrase) {
+ let {key, salt} = this._passwordToKey(passphrase);
+ let encrypted = this._encrypt(message, key);
+
+ return sodium.to_hex(new Uint8Array([...salt, ...encrypted]));
+ }
+
+ /**
+ * Decrypt the hex or base64 encoded message with the given key
+ *
+ * @param {String} encodedString
+ * @param {Uint8Array} key
+ * @returns {String}
+ * @private
+ */
+ _decryptString(encodedString, passphrase) {
+ let salt = encodedString.slice(0, sodium.crypto_pwhash_SALTBYTES),
+ text = encodedString.slice(sodium.crypto_pwhash_SALTBYTES),
+ key = this._passwordToKey(passphrase, salt);
+ return sodium.to_string(this._decrypt(text, key));
+ }
+
+ /**
+ * Encrypt the message with the given key
+ *
+ * @param {String} message
+ * @param {String} key
+ * @returns {Uint8Array}
+ * @private
+ */
+ _encrypt(message, key) {
+ let nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
+
+ return new Uint8Array([...nonce, ...sodium.crypto_secretbox_easy(message, nonce, key)]);
+ }
+
+ /**
+ * Decrypt the message with the given key
+ *
+ * @param {Uint8Array} encrypted
+ * @param {Uint8Array} key
+ * @returns {Uint8Array}
+ * @private
+ */
+ _decrypt(encrypted, key) {
+ if(encrypted.length < sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) throw new Error('Invalid encrypted text length');
+
+ let nonce = encrypted.slice(0, sodium.crypto_secretbox_NONCEBYTES),
+ ciphertext = encrypted.slice(sodium.crypto_secretbox_NONCEBYTES);
+
+ return sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
+ }
+
+ // noinspection JSMethodCanBeStatic
+ /**
+ *
+ * @param password
+ * @param salt
+ * @returns {Uint8Array}
+ * @private
+ */
+ _passwordToKey(password) {
+ let salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
+
+ let key = sodium.crypto_pwhash(
+ sodium.crypto_box_SEEDBYTES,
+ password,
+ salt,
+ sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
+ sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
+ sodium.crypto_pwhash_ALG_DEFAULT
+ );
+
+ return {key, salt};
+ }
+} \ No newline at end of file