diff options
author | Marius David Wieschollek <passwords.public@mdns.eu> | 2024-01-01 20:37:57 +0300 |
---|---|---|
committer | Marius David Wieschollek <passwords.public@mdns.eu> | 2024-01-01 20:37:57 +0300 |
commit | 6e716981adeeade0a71414dbc445870614902a91 (patch) | |
tree | 8d6778ebd459ae3188e757318993d31de560a92a /src | |
parent | 183b9668fb9a38e578839fac96ca920205b44506 (diff) |
Add legacy implementation compatibility layer
Signed-off-by: Marius David Wieschollek <passwords.public@mdns.eu>
Diffstat (limited to 'src')
-rw-r--r-- | src/Authorization/Challenge/PWDv1Challenge.js | 2 | ||||
-rw-r--r-- | src/ClassLoader/EnhancedClassLoader.js | 4 | ||||
-rw-r--r-- | src/Classes/Encryption.js | 436 | ||||
-rw-r--r-- | src/Classes/EnhancedApi.js | 289 | ||||
-rw-r--r-- | src/Classes/SimpleApi.js | 35 | ||||
-rw-r--r-- | src/Client/BasicPasswordsClient.js | 12 | ||||
-rw-r--r-- | src/Encryption/CSEv1Encryption.js | 7 | ||||
-rw-r--r-- | src/Encryption/Keychain/CSEv1Keychain.js | 22 | ||||
-rw-r--r-- | src/Model/CustomField/DataField.js | 2 | ||||
-rw-r--r-- | src/Model/CustomField/EmailField.js | 2 | ||||
-rw-r--r-- | src/Model/CustomField/FileField.js | 2 | ||||
-rw-r--r-- | src/Model/CustomField/SecretField.js | 2 | ||||
-rw-r--r-- | src/Model/CustomField/TextField.js | 2 | ||||
-rw-r--r-- | src/Model/CustomField/UrlField.js | 2 | ||||
-rw-r--r-- | src/Model/Folder/Folder.js | 2 | ||||
-rw-r--r-- | src/Model/Password/Password.js | 2 | ||||
-rw-r--r-- | src/Model/Server/Server.js | 2 | ||||
-rw-r--r-- | src/Model/Tag/Tag.js | 2 | ||||
-rw-r--r-- | src/http.js | 11 | ||||
-rw-r--r-- | src/legacy.js | 7 |
20 files changed, 257 insertions, 588 deletions
diff --git a/src/Authorization/Challenge/PWDv1Challenge.js b/src/Authorization/Challenge/PWDv1Challenge.js index 13a3559..1ebca08 100644 --- a/src/Authorization/Challenge/PWDv1Challenge.js +++ b/src/Authorization/Challenge/PWDv1Challenge.js @@ -80,7 +80,7 @@ export default class PWDv1Challenge { genericHashKey ); - let passwordHashSalt = sodium.sodium(sodium.crypto_pwhash_SALTBYTES), + let passwordHashSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES), passwordHash = sodium.crypto_pwhash( sodium.crypto_box_SEEDBYTES, genericHash, diff --git a/src/ClassLoader/EnhancedClassLoader.js b/src/ClassLoader/EnhancedClassLoader.js index cc4c667..c0dcf3f 100644 --- a/src/ClassLoader/EnhancedClassLoader.js +++ b/src/ClassLoader/EnhancedClassLoader.js @@ -2,6 +2,7 @@ import DefaultClassLoader from "./DefaultClassLoader"; import EnhancedPassword from "../Model/Password/EnhancedPassword"; import EnhancedFolder from "../Model/Folder/EnhancedFolder"; import EnhancedTag from "../Model/Tag/EnhancedTag"; +import EnhancedApi from "../Classes/EnhancedApi"; export default class EnhancedClassLoader extends DefaultClassLoader { /** @@ -15,6 +16,9 @@ export default class EnhancedClassLoader extends DefaultClassLoader { classes['model.folder'] = (d) => { return new EnhancedFolder(d, this.getInstance('client')); }; classes['model.tag'] = (d) => { return new EnhancedTag(d, this.getInstance('client')); }; + classes['legacy'] = () => { return new EnhancedApi(this.getInstance('client')); }; + + return classes; } }
\ No newline at end of file diff --git a/src/Classes/Encryption.js b/src/Classes/Encryption.js deleted file mode 100644 index 3e310c7..0000000 --- a/src/Classes/Encryption.js +++ /dev/null @@ -1,436 +0,0 @@ -import sodium from 'libsodium-wrappers'; - -export default class Encryption { - - constructor() { - this.fields = { - password: ['url', 'label', 'notes', 'password', 'username', 'customFields'], - folder : ['label'], - tag : ['label', 'color'] - }; - this._enabled = false; - this._keys = {}; - this._current = ''; - this._legacyEncoding = false; - this.ready(); - } - - /** - * - * @returns {Boolean} - */ - get enabled() { - return this._enabled; - } - - /** - * - * @returns {Boolean} - */ - get keys() { - return Object.keys(this._keys); - } - - // noinspection JSMethodCanBeStatic - /** - * - * @returns {Promise<boolean>} - */ - async ready() { - await sodium.ready; - } - - /** - * Returns true if the user has base64 encoded properties - * - * @return {Boolean} - */ - hasLegacyEncoding() { - return this._legacyEncoding; - } - - /** - * Encrypts an object - * - * @param object - * @param type - * @returns {{_encrypted}|*} - */ - encryptObject(object, type) { - if(!this._enabled) throw new Error('Encryption not available'); - if(!this.fields.hasOwnProperty(type)) throw new Error('Invalid object type'); - if(object.hasOwnProperty('_encrypted') && object._encrypted) return object; - - let fields = this.fields[type], - key = this._getKey(this._current); - - for(let i = 0; i < fields.length; i++) { - let field = fields[i], - data = object[field]; - - if(data === null || data.length === 0) continue; - object[field] = this.encryptString(data, key); - } - - object.cseType = 'CSEv1r1'; - object.cseKey = this._current; - object._encrypted = true; - - return object; - } - - /** - * Decrypts an object - * - * @param object - * @param type - * @returns {{_encrypted}|*} - */ - decryptObject(object, type) { - if(!this._enabled) throw new Error('Encryption not available'); - if(!this.fields.hasOwnProperty(type)) throw new Error('Invalid object type'); - if(object.cseType !== 'CSEv1r1') throw new Error('Unsupported encryption type'); - if(object.hasOwnProperty('_encrypted') && !object._encrypted) return object; - - let fields = this.fields[type], - key = this._getKey(object.cseKey); - - for(let i = 0; i < fields.length; i++) { - let field = fields[i], - data = object[field]; - - if(data === null || data.length === 0) continue; - object[field] = this.decryptString(data, key); - } - - object._encrypted = false; - - return object; - } - - /** - * Encrypts the message with the user defined password - * - * @param message - * @param password - * @returns {*} - */ - encryptWithPassword(message, password) { - let salt = this._generateRandom(sodium.crypto_pwhash_SALTBYTES), - key = this._passwordToKey(password, salt), - encrypted = this.encrypt(message, key); - - return sodium.to_hex(new Uint8Array([...salt, ...encrypted])); - } - - /** - * Decrypts the message with the user defined password - * - * @param message - * @param password - * @returns {*} - */ - decryptWithPassword(message, password) { - let encrypted = sodium.from_hex(message), - salt = encrypted.slice(0, sodium.crypto_pwhash_SALTBYTES), - text = encrypted.slice(sodium.crypto_pwhash_SALTBYTES), - key = this._passwordToKey(password, salt); - - return sodium.to_string(this.decrypt(text, key)); - } - - /** - * Encrypt the message with the given key and return a hex encoded string - * - * @param message - * @param key - * @returns {String} - */ - encryptString(message, key) { - return sodium.to_hex(this.encrypt(message, key)); - } - - /** - * Encrypt the message with the given key - * - * @param message - * @param key - * @returns {Uint8Array} - */ - encrypt(message, key) { - let nonce = this._generateRandom(sodium.crypto_secretbox_NONCEBYTES); - - return new Uint8Array([...nonce, ...sodium.crypto_secretbox_easy(message, nonce, key)]); - } - - /** - * Decrypt the hex or base64 encoded message with the given key - * - * @param encodedString - * @param key - * @returns {String} - */ - decryptString(encodedString, key) { - try { - let encryptedString = sodium.from_hex(encodedString); - return sodium.to_string(this.decrypt(encryptedString, key)); - } catch(e) { - let encryptedString = sodium.from_base64(encodedString); - let result = sodium.to_string(this.decrypt(encryptedString, key)); - this._legacyEncoding = true; - return result; - } - } - - // noinspection JSMethodCanBeStatic - /** - * Decrypt the message with the given key - * - * @param encrypted - * @param key - * @returns {Uint8Array} - */ - 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 - /** - * Generate a challenge solution with the user provided password - * and the server provided salts - * - * @param salts - * @param password - * @returns {String} - */ - solveChallenge(password, salts) { - if(password.length < 12) throw new Error('Password is too short'); - if(password.length > 128) throw new Error('Password is too long'); - - let passwordSalt = sodium.from_hex(salts[0]), - genericHashKey = sodium.from_hex(salts[1]), - passwordHashSalt = sodium.from_hex(salts[2]), - genericHash = sodium.crypto_generichash( - sodium.crypto_generichash_BYTES_MAX, - new Uint8Array([...sodium.from_string(password), ...passwordSalt]), - genericHashKey - ); - - let passwordHash = sodium.crypto_pwhash( - sodium.crypto_box_SEEDBYTES, - genericHash, - passwordHashSalt, - sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, - sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, - sodium.crypto_pwhash_ALG_DEFAULT - ); - - - return sodium.to_hex(passwordHash); - } - - /** - * Create the salts and the secret for the server - * using the user provided password - * - * @param password - * @returns {{salts: *[], secret: *}} - */ - createChallenge(password) { - if(password.length < 12) throw new Error('Password is too short'); - if(password.length > 128) throw new Error('Password is too long'); - - let passwordSalt = this._generateRandom(256), - genericHashKey = this._generateRandom(sodium.crypto_generichash_KEYBYTES_MAX), - genericHash = sodium.crypto_generichash( - sodium.crypto_generichash_BYTES_MAX, - new Uint8Array([...sodium.from_string(password), ...passwordSalt]), - genericHashKey - ); - - let passwordHashSalt = this._generateRandom(sodium.crypto_pwhash_SALTBYTES), - passwordHash = sodium.crypto_pwhash( - sodium.crypto_box_SEEDBYTES, - genericHash, - passwordHashSalt, - sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, - sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, - sodium.crypto_pwhash_ALG_DEFAULT - ); - - return { - salts : [ - sodium.to_hex(passwordSalt), - sodium.to_hex(genericHashKey), - sodium.to_hex(passwordHashSalt) - ], - secret: sodium.to_hex(passwordHash) - }; - } - - /** - * Decrypt and activate the keychain - * - * @param keychainText - * @param password - */ - setKeychain(keychainText, password) { - let encrypted; - try { - encrypted = sodium.from_hex(keychainText); - } catch(e) { - encrypted = sodium.from_base64(keychainText); - this._legacyEncoding = true; - } - - let salt = encrypted.slice(0, sodium.crypto_pwhash_SALTBYTES), - text = encrypted.slice(sodium.crypto_pwhash_SALTBYTES), - key = this._passwordToKey(password, salt), - keychain = JSON.parse(sodium.to_string(this.decrypt(text, key))); - - this._current = keychain.current; - for(let id in keychain.keys) { - if(keychain.keys.hasOwnProperty(id)) { - this._keys[id] = sodium.from_hex(keychain.keys[id]); - } - } - - this._enabled = true; - } - - /** - * Remove the current keychain - */ - unsetKeychain() { - this._enabled = false; - this._keys = {}; - this._current = ''; - } - - /** - * Add a new key to the keychain and return the full keychain - * - * @param password - * @param addKey - * @returns {*} - */ - getKeychain(password, addKey = false) { - if(this._enabled === false) { - this._keys = {}; - } - - if(addKey || this._current.length === 0) { - this._current = this.getUuid(); - this._keys[this._current] = this._generateRandom(sodium.crypto_secretbox_KEYBYTES); - this._enabled = true; - } - - let keychain = { - keys : {}, - current: this._current - }; - - for(let id in this._keys) { - if(this._keys.hasOwnProperty(id)) { - keychain.keys[id] = sodium.to_hex(this._keys[id]); - } - } - - let salt = this._generateRandom(sodium.crypto_pwhash_SALTBYTES), - key = this._passwordToKey(password, salt), - encrypted = this.encrypt(JSON.stringify(keychain), key); - - return sodium.to_hex(new Uint8Array([...salt, ...encrypted])); - } - - /** - * Create a uuidv4 - * - * @returns {String} - */ - getUuid() { - return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ); - } - - /** - * - * @param uuid - * @returns {Uint8Array} - * @private - */ - _getKey(uuid) { - if(this._keys.hasOwnProperty(uuid)) { - return this._keys[uuid]; - } - - throw new Error('Unknown CSE key id'); - } - - // noinspection JSMethodCanBeStatic - /** - * - * @param password - * @param salt - * @returns {Uint8Array} - * @private - */ - _passwordToKey(password, salt) { - return sodium.crypto_pwhash( - sodium.crypto_box_SEEDBYTES, - password, - salt, - sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, - sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, - sodium.crypto_pwhash_ALG_DEFAULT - ); - } - - // noinspection JSMethodCanBeStatic - /** - * - * @param length - * @returns {Uint8Array} - * @private - */ - _generateRandom(length) { - let array = new Uint8Array(length); - window.crypto.getRandomValues(array); - - return array; - } - - // noinspection JSMethodCanBeStatic - /** - * Generate a hash of the given text with the given algorithm - * - * @param value - * @param algorithm - * @returns {Promise<string>} - */ - async getHash(value, algorithm = 'SHA-1') { - if(['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'].indexOf(algorithm) !== -1) { - let msgBuffer = new TextEncoder('utf-8').encode(value), - hashBuffer = await - crypto.subtle.digest(algorithm, msgBuffer); - return sodium.to_hex(new Uint8Array(hashBuffer)); - } else if(algorithm.substr(0, 7) === 'BLAKE2b') { - let bytes = sodium.crypto_generichash_BYTES_MAX; - if(algorithm.indexOf('-') !== -1) { - bytes = algorithm.split('-')[1]; - if(sodium.crypto_generichash_BYTES_MAX < bytes) bytes = sodium.crypto_generichash_BYTES_MAX; - if(sodium.crypto_generichash_BYTES_MIN > bytes) bytes = sodium.crypto_generichash_BYTES_MIN; - } - - return sodium.to_hex(sodium.crypto_generichash(bytes, sodium.from_string(value))); - } else if(algorithm === 'Argon2') { - return sodium.crypto_pwhash_str(value, sodium.crypto_pwhash_OPSLIMIT_MIN, sodium.crypto_pwhash_MEMLIMIT_MIN); - } - } -}
\ No newline at end of file diff --git a/src/Classes/EnhancedApi.js b/src/Classes/EnhancedApi.js index 7a8ddbd..370aeec 100644 --- a/src/Classes/EnhancedApi.js +++ b/src/Classes/EnhancedApi.js @@ -1,6 +1,5 @@ import Url from 'url-parse'; import SimpleApi from './SimpleApi'; -import Encryption from './Encryption'; import EventEmitter from 'eventemitter3'; export default class EnhancedApi extends SimpleApi { @@ -11,7 +10,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Boolean} */ get isAuthorized() { - return this._isAuthorized === true; + return this._client.isAuthorized(); } /** @@ -20,32 +19,24 @@ export default class EnhancedApi extends SimpleApi { * @returns {Boolean} */ get hasEncryption() { - return this.config.encryption.enabled; + return this._client.getCseV1Encryption().enabled(); } - /** - * - * @param props - */ - constructor(props) { - super(props); - - this._isAuthorized = false; + constructor() { + super(); } /** * Initialize the api object * - * @param config + * @param {BasicPasswordsClient} client */ - initialize(config = {}) { - if(!config.baseUrl || config.baseUrl.substr(0, 5) !== 'https') throw new Error('Invalid Base URL given'); + initialize(client, config = {}) { + config.baseUrl = client.getServer().getBaseUrl(); if(!config.folderIcon) config.folderIcon = `${config.baseUrl}core/img/filetypes/folder.svg`; if(!config.apiUrl) config.apiUrl = `${config.baseUrl}index.php/apps/passwords/`; if(!config.hashLength) config.hashLength = 40; - - if(!config.encryption) config.encryption = new Encryption(); if(!config.cseMode || ['none', 'CSEv1r1'].indexOf(config.cseMode) === -1) config.cseMode = 'none'; if(!config.device) { @@ -63,7 +54,7 @@ export default class EnhancedApi extends SimpleApi { if(d.oldSessionToken) this._resetAuthorisation(); }); - super.initialize(config); + super.initialize(config, client); } /** @@ -76,10 +67,10 @@ export default class EnhancedApi extends SimpleApi { */ async getHash(value, algorithm = 'SHA-1', length = null) { if(length === null) length = this.config.hashLength; - let hash = await this.config.encryption.getHash(value, algorithm); + let hash = await this._client.getInstance('service.hash').getHash(value, algorithm); - if(length !== 40) { - return hash.substr(0, length); + if(length < hash.length) { + return hash.substring(0, length); } return hash; @@ -93,22 +84,23 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async openSession(login) { - let password = null; + /** @type {SessionAuthorization} **/ + let authRequest = this._client.getClass('authorization.session'); + await authRequest.load(); + this._client.setInstance('authorization.session', authRequest); + if(login.hasOwnProperty('password')) { - login.challenge = this.config.encryption.solveChallenge(login.password, login.salts); - password = login.password; - delete login.salts; - delete login.password; + authRequest.getChallenge().setPassword(login.password); } - let result = await this._sendRequest('session.open', login); - if(password !== null && result.hasOwnProperty('keys') && result.keys.hasOwnProperty('CSEv1r1')) { - this.config.encryption.setKeychain(result.keys.CSEv1r1, password); - } + if(login.hasOwnProperty('token')) { + let provider = Object.keys(login.token)[0]; - this._isAuthorized = true; + authRequest.setActiveToken(provider); + authRequest.getActiveToken().setToken(login.token[provider]); + } - return result; + await authRequest.authorize(); } /** @@ -117,9 +109,8 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async closeSession() { - let result = await super.closeSession(); this._resetAuthorisation(); - return result; + return true; } @@ -138,16 +129,32 @@ export default class EnhancedApi extends SimpleApi { async setAccountChallenge(password, oldPassword = null) { let oldSecret = null; if(oldPassword !== null) { - let oldChallenge = await super.getAccountChallenge(); - oldSecret = this.config.encryption.solveChallenge(oldPassword, oldChallenge.salts); + let oldChallengeData = await super.getAccountChallenge(); + let challenge = this._client.getClass('challenge.pwdv1', oldChallengeData); + challenge.setPassword(oldPassword); + oldSecret = challenge.solve(); } - let challenge = this.config.encryption.createChallenge(password); + let challenge = this._client.getClass('challenge.pwdv1', {}); + challenge.setPassword(password); + let data = challenge.create(); - let result = await super.setAccountChallenge(challenge.secret, challenge.salts, oldSecret); + let result = await super.setAccountChallenge(data.secret, data.salts, oldSecret); if(result.success) { - let keychain = this.config.encryption.getKeychain(password, true); - await super.setKeychain('CSEv1r1', keychain); + let keychain = this._client.getCseV1Encryption().getKeychain(), + initial = keychain === null; + if(initial) { + keychain = this._client.getClass('keychain.csev1', null, null); + } + + keychain.setPassword(password); + keychain.update(); + + let data = keychain.export(); + await super.setKeychain('CSEv1r1', data); + if(initial) { + this._client.getCseV1Encryption().setKeychain(keychain); + } } return result; @@ -177,13 +184,18 @@ export default class EnhancedApi extends SimpleApi { object.hash = await this.getHash(data.password); if(!object.label) this._generatePasswordTitle(object); - if(this.config.encryption.enabled && object.cseType !== 'none') { - this.config.encryption.encryptObject(object, 'password'); + if(!object._encrypted && this.hasEncryption && object.cseType !== 'none') { + let encryption = this._client.getCseV1Encryption(); + object = await encryption.encrypt(object, 'password'); } else { object.cseKey = ''; } - return await super.createPassword(object); + let result = await super.createPassword(object); + object.id = result.id; + object.revison = result.revison; + + return object; } /** @@ -208,14 +220,18 @@ export default class EnhancedApi extends SimpleApi { object.hash = await this.getHash(data.password); if(!object.label) this._generatePasswordTitle(object); - if(this.config.encryption.enabled && object.cseType !== 'none' && (!data.hasOwnProperty('shared') || !data.shared)) { - this.config.encryption.encryptObject(object, 'password'); + if(!object._encrypted && this.hasEncryption && object.cseType !== 'none') { + let encryption = this._client.getCseV1Encryption(); + object = await encryption.encrypt(object, 'password'); } else { - object.cseType = 'none'; object.cseKey = ''; } - return await super.updatePassword(object); + let result = await super.updatePassword(object); + object.id = result.id; + object.revison = result.revison; + + return object; } /** @@ -226,7 +242,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async showPassword(id, detailLevel = 'model') { - return this._processPassword( + return await this._processPassword( await super.showPassword(id, detailLevel) ); } @@ -238,7 +254,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async listPasswords(detailLevel = 'model') { - return this._processPasswordList( + return await this._processPasswordList( await super.listPasswords(detailLevel) ); } @@ -251,7 +267,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async findPasswords(criteria = {}, detailLevel = 'model') { - return this._processPasswordList( + return await this._processPasswordList( await super.findPasswords(criteria, detailLevel) ); } @@ -267,7 +283,7 @@ export default class EnhancedApi extends SimpleApi { * @param data * @returns {Promise} */ - createFolder(data = {}) { + async createFolder(data = {}) { let object = this._cloneObject(data); try { @@ -277,13 +293,18 @@ export default class EnhancedApi extends SimpleApi { return this._createRejectedPromise(e); } - if(this.config.encryption.enabled && object.cseType !== 'none') { - this.config.encryption.encryptObject(object, 'folder'); + if(this.hasEncryption && object.cseType !== 'none') { + let encryption = this._client.getCseV1Encryption(); + object = await encryption.encrypt(object, 'folder'); } else { object.cseKey = ''; } - return super.createFolder(object); + let result = await super.createFolder(object); + object.id = result.id; + object.revison = result.revison; + + return object; } /** @@ -293,7 +314,7 @@ export default class EnhancedApi extends SimpleApi { * @param data * @returns {Promise} */ - updateFolder(data = {}) { + async updateFolder(data = {}) { if(!data.id) return this.createFolder(data); let object = this._cloneObject(data); @@ -304,13 +325,18 @@ export default class EnhancedApi extends SimpleApi { return this._createRejectedPromise(e); } - if(this.config.encryption.enabled && object.cseType !== 'none') { - this.config.encryption.encryptObject(object, 'folder'); + if(this.hasEncryption && object.cseType !== 'none') { + let encryption = this._client.getCseV1Encryption(); + object = await encryption.encrypt(object, 'folder'); } else { object.cseKey = ''; } - return super.updateFolder(object); + let result = await super.updateFolder(object); + object.id = result.id; + object.revison = result.revison; + + return object; } /** @@ -321,7 +347,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async showFolder(id, detailLevel = 'model') { - return this._processFolder( + return await this._processFolder( await super.showFolder(id, detailLevel) ); } @@ -333,7 +359,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async listFolders(detailLevel = 'model') { - return this._processFolderList( + return await this._processFolderList( await super.listFolders(detailLevel) ); } @@ -346,7 +372,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async findFolders(criteria = {}, detailLevel = 'model') { - return this._processFolderList( + return await this._processFolderList( await super.findFolders(criteria, detailLevel) ); } @@ -362,7 +388,7 @@ export default class EnhancedApi extends SimpleApi { * @param data * @returns {Promise} */ - createTag(data = {}) { + async createTag(data = {}) { let object = this._cloneObject(data); try { @@ -372,13 +398,18 @@ export default class EnhancedApi extends SimpleApi { return this._createRejectedPromise(e); } - if(this.config.encryption.enabled && object.cseType !== 'none') { - this.config.encryption.encryptObject(object, 'tag'); + if(this.hasEncryption && object.cseType !== 'none') { + let encryption = this._client.getCseV1Encryption(); + object = await encryption.encrypt(object, 'tag'); } else { object.cseKey = ''; } - return super.createTag(object); + let result = await super.createTag(object); + object.id = result.id; + object.revison = result.revison; + + return object; } /** @@ -388,7 +419,7 @@ export default class EnhancedApi extends SimpleApi { * @param data * @returns {Promise} */ - updateTag(data = {}) { + async updateTag(data = {}) { if(!data.id) return this.createTag(data); let object = this._cloneObject(data); @@ -399,13 +430,18 @@ export default class EnhancedApi extends SimpleApi { return this._createRejectedPromise(e); } - if(this.config.encryption.enabled && object.cseType !== 'none') { - this.config.encryption.encryptObject(object, 'tag'); + if(this.hasEncryption && object.cseType !== 'none') { + let encryption = this._client.getCseV1Encryption(); + object = await encryption.encrypt(object, 'tag'); } else { object.cseKey = ''; } - return super.updateTag(object); + let result = await super.updateTag(object); + object.id = result.id; + object.revison = result.revison; + + return object; } /** @@ -416,7 +452,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async showTag(id, detailLevel = 'model') { - return this._processTag( + return await this._processTag( await super.showTag(id, detailLevel) ); } @@ -428,7 +464,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async listTags(detailLevel = 'model') { - return this._processTagList( + return await this._processTagList( await super.listTags(detailLevel) ); } @@ -441,7 +477,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async findTags(criteria = {}, detailLevel = 'model') { - return this._processTagList( + return await this._processTagList( await super.findTags(criteria, detailLevel) ); } @@ -496,7 +532,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async showShare(id, detailLevel = 'model') { - return this._processShare( + return await this._processShare( await super.showShare(id, detailLevel) ); } @@ -508,7 +544,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async listShares(detailLevel = 'model') { - return this._processShareList( + return await this._processShareList( await super.listShares(detailLevel) ); } @@ -521,7 +557,7 @@ export default class EnhancedApi extends SimpleApi { * @returns {Promise} */ async findShares(criteria = {}, detailLevel = 'model') { - return this._processShareList( + return await this._processShareList( await super.findShares(criteria, detailLevel) ); } @@ -846,14 +882,14 @@ export default class EnhancedApi extends SimpleApi { /** * * @param data - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processPasswordList(data) { + async _processPasswordList(data) { let passwords = {}; for(let i = 0; i < data.length; i++) { - let password = this._processPassword(data[i]); + let password = await this._processPassword(data[i]); passwords[password.id] = password; } @@ -863,16 +899,16 @@ export default class EnhancedApi extends SimpleApi { /** * * @param password - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processPassword(password) { - if(password.hasOwnProperty('cseType') && password.cseType !== 'none') { - this.config.encryption.decryptObject(password, 'password'); - } else { - password._encrypted = false; + async _processPassword(password) { + if(password.cseType === 'CSEv1r1' && password._encrypted !== false) { + let encryption = this._client.getCseV1Encryption(); + password = await encryption.decrypt(password, 'password'); } + password._encrypted = false; password.type = 'password'; if(password.url) { let host = this.parseUrl(password.url, 'host'), @@ -897,19 +933,19 @@ export default class EnhancedApi extends SimpleApi { if(password.tags) { - password.tags = this._processTagList(password.tags); + password.tags = await this._processTagList(password.tags); } if(password.revisions) { - password.revisions = this._processPasswordList(password.revisions); + password.revisions = await this._processPasswordList(password.revisions); } if(typeof password.folder === 'object') { - password.folder = this._processFolder(password.folder); + password.folder = await this._processFolder(password.folder); } if(password.share !== null && typeof password.share === 'object') { - password.share = this._processShare(password.share); + password.share = await this._processShare(password.share); } if(Array.isArray(password.shares)) { - password.shares = this._processShareList(password.shares); + password.shares = await this._processShareList(password.shares); } password.created = new Date(password.created * 1e3); @@ -922,14 +958,14 @@ export default class EnhancedApi extends SimpleApi { /** * * @param data - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processFolderList(data) { + async _processFolderList(data) { let folders = {}; for(let i = 0; i < data.length; i++) { - let folder = this._processFolder(data[i]); + let folder = await this._processFolder(data[i]); folders[folder.id] = folder; } @@ -939,29 +975,29 @@ export default class EnhancedApi extends SimpleApi { /** * * @param folder - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processFolder(folder) { - if(folder.hasOwnProperty('cseType') && folder.cseType !== 'none') { - this.config.encryption.decryptObject(folder, 'folder'); - } else { - folder._encrypted = false; + async _processFolder(folder) { + if(folder.cseType === 'CSEv1r1' && folder._encrypted !== false) { + let encryption = this._client.getCseV1Encryption(); + folder = await encryption.decrypt(folder, 'folder'); } + folder._encrypted = false; folder.type = 'folder'; - folder.icon = this._config.folderIcon; + folder.icon = this.config.folderIcon; if(folder.folders) { - folder.folders = this._processFolderList(folder.folders); + folder.folders = await this._processFolderList(folder.folders); } if(folder.passwords) { - folder.passwords = this._processPasswordList(folder.passwords); + folder.passwords = await this._processPasswordList(folder.passwords); } if(folder.revisions) { - folder.revisions = this._processFolderList(folder.revisions); + folder.revisions = await this._processFolderList(folder.revisions); } if(typeof folder.parent !== 'string') { - folder.parent = this._processFolder(folder.parent); + folder.parent = await this._processFolder(folder.parent); } folder.created = new Date(folder.created * 1e3); @@ -974,14 +1010,14 @@ export default class EnhancedApi extends SimpleApi { /** * * @param data - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processTagList(data) { + async _processTagList(data) { let tags = {}; for(let i = 0; i < data.length; i++) { - let tag = this._processTag(data[i]); + let tag = await this._processTag(data[i]); tags[tag.id] = tag; } @@ -991,22 +1027,22 @@ export default class EnhancedApi extends SimpleApi { /** * * @param tag - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processTag(tag) { - if(tag.hasOwnProperty('cseType') && tag.cseType !== 'none') { - this.config.encryption.decryptObject(tag, 'tag'); - } else { - tag._encrypted = false; + async _processTag(tag) { + if(tag.cseType === 'CSEv1r1' && tag._encrypted !== false) { + let encryption = this._client.getCseV1Encryption(); + tag = await encryption.decrypt(tag, 'tag'); } + tag._encrypted = false; tag.type = 'tag'; if(tag.passwords) { - tag.passwords = this._processPasswordList(tag.passwords); + tag.passwords = await this._processPasswordList(tag.passwords); } if(tag.revisions) { - tag.revisions = this._processTagList(tag.revisions); + tag.revisions = await this._processTagList(tag.revisions); } tag.created = new Date(tag.created * 1e3); tag.updated = new Date(tag.updated * 1e3); @@ -1018,14 +1054,14 @@ export default class EnhancedApi extends SimpleApi { /** * * @param data - * @returns {{}} + * @returns {Promise<{}>} * @private */ - _processShareList(data) { + async _processShareList(data) { let shares = {}; for(let i = 0; i < data.length; i++) { - let share = this._processShare(data[i]); + let share = await this._processShare(data[i]); shares[share.id] = share; } @@ -1035,14 +1071,14 @@ export default class EnhancedApi extends SimpleApi { /** * * @param share - * @returns {*} + * @returns {Promise<{}>} * @private */ - _processShare(share) { + async _processShare(share) { share.type = 'share'; if(typeof share.password !== 'string') { - share.password = this._processPassword(share.password); + share.password = await this._processPassword(share.password); } share.created = new Date(share.created * 1e3); @@ -1131,8 +1167,7 @@ export default class EnhancedApi extends SimpleApi { * @private */ _resetAuthorisation() { - this._isAuthorized = false; - this.config.encryption.unsetKeychain(); + this._client.closeSession(); } @@ -1150,9 +1185,9 @@ export default class EnhancedApi extends SimpleApi { cseDefault = 'none'; if(this.hasEncryption) { - cseDefault = this.config.cseMode; + cseDefault = 'CSEv1r1'; cseTypes.push('CSEv1r1'); - cseKeys = this.config.encryption.keys; + cseKeys = this._client.getCseV1Encryption().getKeychain().listKeys(); cseKeys.push(''); } @@ -1244,9 +1279,9 @@ export default class EnhancedApi extends SimpleApi { cseDefault = 'none'; if(this.hasEncryption) { - cseDefault = this.config.cseMode; + cseDefault = 'CSEv1r1'; cseTypes.push('CSEv1r1'); - cseKeys = this.config.encryption.keys; + cseKeys = this._client.getCseV1Encryption().getKeychain().listKeys(); cseKeys.push(''); } @@ -1310,9 +1345,9 @@ export default class EnhancedApi extends SimpleApi { cseDefault = 'none'; if(this.hasEncryption) { - cseDefault = this.config.cseMode; + cseDefault = 'CSEv1r1'; cseTypes.push('CSEv1r1'); - cseKeys = this.config.encryption.keys; + cseKeys = this._client.getCseV1Encryption().getKeychain().listKeys(); cseKeys.push(''); } diff --git a/src/Classes/SimpleApi.js b/src/Classes/SimpleApi.js index b92d18c..407e22c 100644 --- a/src/Classes/SimpleApi.js +++ b/src/Classes/SimpleApi.js @@ -17,6 +17,7 @@ export default class SimpleApi { * SimpleApi Constructor */ constructor() { + this._client = null; this._config = {}; this._headers = {}; this._paths = { @@ -73,6 +74,7 @@ export default class SimpleApi { 'service.favicon' : 'api/1.0/service/favicon/{domain}/{size}', 'service.preview' : 'api/1.0/service/preview/{domain}/{view}/{width}/{height}', 'service.password-change': 'api/1.0/service/password-change', + 'service.hashes' : 'api/1.0/service/hashes', 'cron.sharing' : 'cron/sharing', 'link.request' : 'link/connect/request', 'link.await' : 'link/connect/await', @@ -95,23 +97,18 @@ export default class SimpleApi { } /** - * @param config + * @param {Object} config + * @param {BasicPasswordsClient} client */ - initialize(config = {}) { + initialize(config, client) { this._enabled = false; + this._client = client; this._config = config; if(config.apiUrl.substr(0, 5) !== 'https') throw new Error('HTTPS required for api'); this._headers = {}; if(config.headers) this._headers = config.headers; - if(config.user !== null && config.password !== null) { - this._headers.Authorization = `Basic ${btoa(`${config.user}:${config.password}`)}`; - } else { - // @TODO Use custom error here - throw new Error('API username or password missing'); - } - this._enabled = true; } @@ -640,6 +637,14 @@ export default class SimpleApi { return this._sendRequest('service.password-change', {domain}); } + /** + * + * @returns {Promise} + */ + getHashes(range) { + return this._sendRequest('service.hashes', {range}); + } + /** * Account Management @@ -796,6 +801,8 @@ export default class SimpleApi { headers.append(header, this._headers[header]); } headers.append('Accept', dataType); + headers.append('Authorization', `Basic ${btoa(`${this._client.getServer().getUser()}:${this._client.getServer().getToken()}`)}`); + headers.append('x-api-session', this._client.getSession().getId()); let options = {method, headers, credentials: 'omit', redirect: 'error'}; if(data) { @@ -825,7 +832,12 @@ export default class SimpleApi { path = this._paths[path]; } - path = this._config.apiUrl + path; + path = this._client.getServer().getApiUrl() + path; + + if(path.indexOf('api/api') !== -1) { + path = path.replace('api/api', 'api'); + } + return path; } @@ -866,8 +878,7 @@ export default class SimpleApi { } let oldSessionToken = this._config.sessionToken; - this._config.sessionToken = sessionToken; - this._headers['X-API-SESSION'] = sessionToken; + this._client.getSession().setId(sessionToken); this._config.events.emit('api.session.token.changed', {sessionToken, oldSessionToken}); } } diff --git a/src/Client/BasicPasswordsClient.js b/src/Client/BasicPasswordsClient.js index a0411e3..803b529 100644 --- a/src/Client/BasicPasswordsClient.js +++ b/src/Client/BasicPasswordsClient.js @@ -120,6 +120,18 @@ export default class BasicPasswordsClient { /** * + * @return {Session} + */ + closeSession() { + this.getRequest() + .setPath('1.0/session/close') + .send(); + + return this.renewSession(); + } + + /** + * * @returns {SessionAuthorization} */ getSessionAuthorization() { diff --git a/src/Encryption/CSEv1Encryption.js b/src/Encryption/CSEv1Encryption.js index 9005947..82c7753 100644 --- a/src/Encryption/CSEv1Encryption.js +++ b/src/Encryption/CSEv1Encryption.js @@ -165,6 +165,13 @@ export default class CSEv1Encryption { } /** + * @return {CSEv1Keychain} + */ + getKeychain() { + return this._keychain; + } + + /** * Remove the current keychain */ unsetKeychain() { diff --git a/src/Encryption/Keychain/CSEv1Keychain.js b/src/Encryption/Keychain/CSEv1Keychain.js index 954bbd1..3944bb2 100644 --- a/src/Encryption/Keychain/CSEv1Keychain.js +++ b/src/Encryption/Keychain/CSEv1Keychain.js @@ -1,5 +1,4 @@ import sodium from 'libsodium-wrappers'; -import { v4 as uuid } from 'uuid'; export default class CSEv1Keychain { @@ -57,6 +56,15 @@ export default class CSEv1Keychain { } /** + * Get a key by id + * + * @returns {String[]} + */ + listKeys() { + return Object.keys(this._keys); + } + + /** * Get the current key * * @returns {String} @@ -131,7 +139,7 @@ export default class CSEv1Keychain { * Add a new key to the keychain and set it as current */ update() { - let uuid = uuid(); + let uuid = this._createUuid(); this._keys[uuid] = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES); this._current = uuid; this._enabled.set(true); @@ -186,4 +194,14 @@ export default class CSEv1Keychain { sodium.crypto_pwhash_ALG_DEFAULT ); } + /** + * Create a uuidv4 + * + * @returns {String} + */ + _createUuid() { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } }
\ No newline at end of file diff --git a/src/Model/CustomField/DataField.js b/src/Model/CustomField/DataField.js index 5e9ee01..bb57428 100644 --- a/src/Model/CustomField/DataField.js +++ b/src/Model/CustomField/DataField.js @@ -1,5 +1,5 @@ import AbstractField from './AbstractField'; -import Properties from '../../Configuration/DataField'; +import Properties from '../../Configuration/DataField.json'; export default class DataField extends AbstractField { diff --git a/src/Model/CustomField/EmailField.js b/src/Model/CustomField/EmailField.js index a2f9c35..43066bc 100644 --- a/src/Model/CustomField/EmailField.js +++ b/src/Model/CustomField/EmailField.js @@ -1,5 +1,5 @@ import AbstractField from './AbstractField'; -import Properties from '../../Configuration/EmailField'; +import Properties from '../../Configuration/EmailField.json'; export default class EmailField extends AbstractField { diff --git a/src/Model/CustomField/FileField.js b/src/Model/CustomField/FileField.js index 9333742..e5e15f0 100644 --- a/src/Model/CustomField/FileField.js +++ b/src/Model/CustomField/FileField.js @@ -1,5 +1,5 @@ import AbstractField from './AbstractField'; -import Properties from '../../Configuration/FileField'; +import Properties from '../../Configuration/FileField.json'; export default class FileField extends AbstractField { diff --git a/src/Model/CustomField/SecretField.js b/src/Model/CustomField/SecretField.js index 9c0e2a9..bc67ed0 100644 --- a/src/Model/CustomField/SecretField.js +++ b/src/Model/CustomField/SecretField.js @@ -1,5 +1,5 @@ import AbstractField from './AbstractField'; -import Properties from '../../Configuration/SecretField'; +import Properties from '../../Configuration/SecretField.json'; export default class SecretField extends AbstractField { diff --git a/src/Model/CustomField/TextField.js b/src/Model/CustomField/TextField.js index 8be3f8e..fb16cf8 100644 --- a/src/Model/CustomField/TextField.js +++ b/src/Model/CustomField/TextField.js @@ -1,5 +1,5 @@ import AbstractField from './AbstractField'; -import Properties from '../../Configuration/TextField'; +import Properties from '../../Configuration/TextField.json'; export default class TextField extends AbstractField { diff --git a/src/Model/CustomField/UrlField.js b/src/Model/CustomField/UrlField.js index d572e5c..b8f3d16 100644 --- a/src/Model/CustomField/UrlField.js +++ b/src/Model/CustomField/UrlField.js @@ -1,5 +1,5 @@ import AbstractField from './AbstractField'; -import Properties from '../../Configuration/UrlField'; +import Properties from '../../Configuration/UrlField.json'; export default class UrlField extends AbstractField { diff --git a/src/Model/Folder/Folder.js b/src/Model/Folder/Folder.js index 531629e..e9d3b5a 100644 --- a/src/Model/Folder/Folder.js +++ b/src/Model/Folder/Folder.js @@ -1,4 +1,4 @@ -import Properties from '../../Configuration/Folder'; +import Properties from '../../Configuration/Folder.json'; import AbstractRevisionModel from '../AbstractRevisionModel'; export default class Folder extends AbstractRevisionModel { diff --git a/src/Model/Password/Password.js b/src/Model/Password/Password.js index 1a64a7d..199f999 100644 --- a/src/Model/Password/Password.js +++ b/src/Model/Password/Password.js @@ -1,4 +1,4 @@ -import Properties from '../../Configuration/Password'; +import Properties from '../../Configuration/Password.json'; import AbstractRevisionModel from '../AbstractRevisionModel'; export default class Password extends AbstractRevisionModel { diff --git a/src/Model/Server/Server.js b/src/Model/Server/Server.js index 792ac6b..cfce53a 100644 --- a/src/Model/Server/Server.js +++ b/src/Model/Server/Server.js @@ -1,5 +1,5 @@ import ConfigruationError from '../../Exception/ConfigruationError'; -import Properties from '../../Configuration/Server'; +import Properties from '../../Configuration/Server.json'; import AbstractModel from './../AbstractModel'; import ObjectMerger from '../../Utility/ObjectMerger'; diff --git a/src/Model/Tag/Tag.js b/src/Model/Tag/Tag.js index b8f851e..a495bb3 100644 --- a/src/Model/Tag/Tag.js +++ b/src/Model/Tag/Tag.js @@ -1,4 +1,4 @@ -import Properties from '../../Configuration/Tag'; +import Properties from '../../Configuration/Tag.json'; import AbstractRevisionModel from '../AbstractRevisionModel'; export default class Tag extends AbstractRevisionModel { diff --git a/src/http.js b/src/http.js new file mode 100644 index 0000000..3d2b610 --- /dev/null +++ b/src/http.js @@ -0,0 +1,11 @@ +import ApiRequest from './Network/ApiRequest'; +import ApiResponse from './Network/ApiResponse'; +import HttpRequest from './Network/HttpRequest'; +import HttpResponse from './Network/HttpResponse'; + +export { + ApiRequest, + ApiResponse, + HttpRequest, + HttpResponse +}
\ No newline at end of file diff --git a/src/legacy.js b/src/legacy.js new file mode 100644 index 0000000..0eb98c2 --- /dev/null +++ b/src/legacy.js @@ -0,0 +1,7 @@ +import EnhancedApi from "./Classes/EnhancedApi"; +import SimpleApi from "./Classes/SimpleApi"; + +export { + EnhancedApi, + SimpleApi +}
\ No newline at end of file |