├── public ├── robots.txt ├── favicon.png ├── img │ └── icons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── electron-1024x1024.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── msapplication-icon-144x144.png │ │ └── safari-pinned-tab.svg ├── manifest.json └── index.html ├── tests ├── unit │ ├── .eslintrc.js │ ├── common.spec.js │ └── crypto.spec.js └── fixtures │ └── polyfill.js ├── src ├── wasm │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── Cargo.lock ├── store │ ├── derivepass │ │ ├── wasm │ │ │ ├── binding.wasm.bin │ │ │ └── index.js │ │ ├── derive.worker.js │ │ └── index.js │ ├── getters.js │ ├── index.js │ ├── state.js │ ├── mutations.js │ └── actions.js ├── App.vue ├── pages │ ├── settings.vue │ ├── application-list.vue │ ├── master-password.vue │ ├── home.vue │ └── application.vue ├── plugins │ ├── sync │ │ └── index.js │ ├── index.js │ ├── service-worker │ │ ├── service-worker.js │ │ └── index.js │ └── auto-logout │ │ └── index.js ├── electron │ ├── preload.js │ └── main.js ├── routes.js ├── assets │ ├── logo.svg │ └── logo-fill.svg ├── utils │ ├── feature-test.js │ ├── sync │ │ ├── local-storage.js │ │ ├── base.js │ │ └── cloud-kit.js │ ├── common.js │ ├── crypto.js │ └── cloud-kit-api.js ├── locales │ ├── en.json │ ├── ca.json │ └── ru.json ├── components │ ├── computing.vue │ ├── fancy-logo.vue │ ├── qr.vue │ ├── qr-send.vue │ ├── cloud-kit.vue │ ├── nav-bar.vue │ ├── qr-receive.vue │ └── tutorial.vue ├── i18n.js ├── main.js ├── presets.js └── layouts │ └── default.vue ├── CODE_OF_CONDUCT.md ├── .gitignore ├── now.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE-MIT ├── vue.config.js ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | } -------------------------------------------------------------------------------- /src/wasm/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /bin 3 | /pkg 4 | wasm-pack.log 5 | /www/ 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/electron-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/electron-1024x1024.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /src/store/derivepass/wasm/binding.wasm.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/src/store/derivepass/wasm/binding.wasm.bin -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derivepass/derivepass-vue/HEAD/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isLoggedIn(state) { 3 | return state.cryptoKeys; 4 | }, 5 | 6 | newUser(state) { 7 | return state.applications.length === 0; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | * [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/master/CODE_OF_CONDUCT.md) 4 | * [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/master/Moderation-Policy.md) 5 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/wasm/README.md: -------------------------------------------------------------------------------- 1 | This can be compiled with `wasm-pack`. Just run: 2 | 3 | ```sh 4 | wasm-pack build 5 | ``` 6 | 7 | ...and copy: 8 | 9 | ```sh 10 | cp -rf src/wasm/pkg/derivepass_bg.wasm \ 11 | src/store/derivepass/wasm/binding.wasm.bin 12 | ``` 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import actions from './actions'; 2 | import getters from './getters'; 3 | import mutations from './mutations'; 4 | import state from './state'; 5 | 6 | export default { 7 | actions, 8 | getters, 9 | mutations, 10 | state, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/fixtures/polyfill.js: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | 3 | global.window = {}; 4 | global.window.crypto = {}; 5 | global.window.crypto.getRandomValues = (buf) => { 6 | const src = crypto.randomBytes(buf.length); 7 | for (let i = 0; i < buf.length; i++) { 8 | buf[i] = src[i]; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | import DerivePass from './derivepass'; 2 | 3 | export default () => { 4 | return { 5 | derivepass: new DerivePass(), 6 | cryptoKeys: null, 7 | master: '', 8 | applications: [], 9 | decryptedApps: [], 10 | settings: { 11 | cloudKitReady: false, 12 | } 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /dist-electron 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | 24 | # Private notes 25 | feedback.md 26 | -------------------------------------------------------------------------------- /src/pages/settings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /src/plugins/sync/index.js: -------------------------------------------------------------------------------- 1 | // Immediately start connecting to iCloud 2 | import CloudKit from '../../utils/sync/cloud-kit'; 3 | import Local from '../../utils/sync/local-storage'; 4 | 5 | // Enable cloud sync 6 | export default { 7 | install(Vue, options) { 8 | const cloudKit = new CloudKit(options.store); 9 | Vue.prototype.$cloudKit = cloudKit; 10 | 11 | // Enable local sync 12 | const local = new Local(options.store); 13 | Vue.prototype.$localStorage = local; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DerivePass", 3 | "short_name": "DerivePass", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./", 17 | "display": "standalone", 18 | "background_color": "#3d79de", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "derivepass", 4 | "alias": [ "dev.derivepass.com" ], 5 | "builds": [ 6 | {"src": "package.json", "use": "@now/static-build"} 7 | ], 8 | "routes": [ 9 | {"src": "^/js/(.*)", "dest": "/js/$1"}, 10 | {"src": "^/css/(.*)", "dest": "/css/$1"}, 11 | {"src": "^/img/(.*)", "dest": "/img/$1"}, 12 | {"src": "^/precache-manifest.*\\.js", "dest": "/precache-manifest.common.js"}, 13 | {"src": "^/(.*\\.(?:js|txt|json|png))", "dest": "/$1"}, 14 | {"src": ".*", "dest": "/index.html"} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/electron/preload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer: ipc } = require('electron'); 2 | 3 | window.electron = window.electron || {}; 4 | 5 | let seq = 0; 6 | const queue = new Map(); 7 | 8 | Object.assign(window.electron, { 9 | async iCloudAuth(url) { 10 | seq = (seq + 1) >>> 0; 11 | 12 | ipc.send('icloud:auth', { seq, url }); 13 | 14 | return await new Promise((resolve) => { 15 | queue.set(seq, (payload) => { 16 | queue.delete(seq); 17 | 18 | resolve(payload); 19 | }); 20 | }); 21 | }, 22 | }); 23 | 24 | ipc.on('icloud:response', (_, { seq, payload }) => { 25 | queue.get(seq)(payload); 26 | }); 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | // Pages 2 | import Home from './pages/home.vue'; 3 | 4 | export default [ 5 | { path: '/', redirect: '/about' }, 6 | { path: '/about', component: Home }, 7 | { 8 | path: '/master', 9 | component: () => import('./pages/master-password'), 10 | meta: { noAuth: true }, 11 | }, 12 | { 13 | path: '/applications', 14 | component: () => import('./pages/application-list'), 15 | meta: { requiresAuth: true }, 16 | }, 17 | { 18 | path: '/applications/:uuid', 19 | component: () => import('./pages/application'), 20 | meta: { requiresAuth: true }, 21 | }, 22 | { path: '/settings', component: () => import('./pages/settings') }, 23 | { path: '*', redirect: '/' }, 24 | ]; 25 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import VueRouter from 'vue-router'; 3 | import VueClipboard from 'vue-clipboard2'; 4 | 5 | import 'bootstrap/dist/css/bootstrap.css' 6 | import 'bootstrap-vue/dist/bootstrap-vue.css' 7 | 8 | import ServiceWorker from './service-worker'; 9 | import AutoLogout from './auto-logout'; 10 | import Sync from './sync'; 11 | 12 | export default { 13 | install(Vue) { 14 | Vue.use(Vuex); 15 | Vue.use(VueRouter); 16 | Vue.use(VueClipboard); 17 | 18 | // Internal Plugins 19 | Vue.use(ServiceWorker); 20 | Vue.use(AutoLogout); 21 | }, 22 | 23 | installStoreDependent(Vue, { store }) { 24 | // Internal Plugins 25 | Vue.use(Sync, { store }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/feature-test.js: -------------------------------------------------------------------------------- 1 | const FEATURES = [ 2 | { 3 | name: 'localStorage', 4 | test: () => window.localStorage, 5 | }, 6 | { 7 | name: 'randomValues', 8 | test: () => window.crypto && window.crypto.getRandomValues, 9 | }, 10 | { 11 | name: 'Web Workers', 12 | test: () => window.Worker, 13 | }, 14 | { 15 | name: 'Web Assembly', 16 | test: () => window.WebAssembly && window.WebAssembly.instantiate, 17 | }, 18 | { 19 | name: 'Promise', 20 | test: () => window.Promise, 21 | }, 22 | ]; 23 | 24 | export default function missingFeatures() { 25 | const missing = []; 26 | for (const feature of FEATURES) { 27 | if (!feature.test()) { 28 | missing.push(feature.name); 29 | } 30 | } 31 | 32 | if (missing.length === 0) { 33 | return false; 34 | } 35 | 36 | return missing; 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/service-worker/service-worker.js: -------------------------------------------------------------------------------- 1 | /* global workbox addEventListener */ 2 | workbox.core.setCacheNameDetails({ prefix: 'DerivePass' }); 3 | 4 | /** 5 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 6 | * requests for URLs in the manifest. 7 | * See https://goo.gl/S9QRab 8 | */ 9 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 10 | workbox.precaching.suppressWarnings(); 11 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 12 | 13 | addEventListener('message', (event) => { 14 | const port = event.ports[0]; 15 | const { type } = event.data; 16 | 17 | if (type === 'update') { 18 | const response = self.skipWaiting() 19 | .then(() => { 20 | port.postMessage({ type: 'ok', payload: null }); 21 | }) 22 | .catch((err) => { 23 | port.postMessage({ type: 'error', payload: err.message }); 24 | }); 25 | 26 | event.waitUntil(response); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derivepass" 3 | description = "derivepass bindings" 4 | version = "1.0.1" 5 | authors = ["Fedor Indutny "] 6 | categories = ["cryptography"] 7 | keywords = ["crypto","scrypt"] 8 | license = "MIT" 9 | edition = "2018" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [features] 15 | # default = ["console_error_panic_hook"] 16 | 17 | [dependencies] 18 | dumb-crypto = "^3.0.0" 19 | cfg-if = "0.1.2" 20 | wasm-bindgen = "0.2" 21 | wee_alloc = { version = "0.4.2", optional = true } 22 | 23 | # The `console_error_panic_hook` crate provides better debugging of panics by 24 | # logging them with `console.error`. This is great for development, but requires 25 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 26 | # code size when deploying. 27 | console_error_panic_hook = { version = "0.1.1", optional = true } 28 | 29 | [dev-dependencies] 30 | wasm-bindgen-test = "0.2" 31 | 32 | [profile.release] 33 | lto = true 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /tests/unit/common.spec.js: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import * as common from '../../src/utils/common'; 4 | 5 | describe('common', () => { 6 | it('should flatten valid range', () => { 7 | assert.deepStrictEqual( 8 | common.flattenRange('0-3\\-\\x\\\\A'), 9 | [ '-', '0', '1', '2', '3', 'A', '\\', 'x' ], 10 | ); 11 | }); 12 | 13 | it('should report range with invalid order', () => { 14 | assert.throws(() => { 15 | common.flattenRange('3-0'); 16 | }, /"3-0"/); 17 | }); 18 | 19 | it('should report range with unicode start-end', () => { 20 | assert.throws(() => { 21 | common.flattenRange('а-б'); 22 | }); 23 | }); 24 | 25 | it('should report range with unmatched start', () => { 26 | assert.throws(() => { 27 | common.flattenRange('0-'); 28 | }, /"0-"/); 29 | }); 30 | 31 | it('should report range with unmatched end', () => { 32 | assert.throws(() => { 33 | common.flattenRange('-9'); 34 | }, /"-9"/); 35 | }); 36 | 37 | it('should report range with no start and no end', () => { 38 | assert.throws(() => { 39 | common.flattenRange('-'); 40 | }, /"-"/); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright Fedor Indutny, 2018. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to permit 10 | persons to whom the Software is furnished to do so, subject to the 11 | following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 19 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 21 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 22 | USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "flatten": { 4 | "whitespace": "Can't contain whitespace", 5 | "invalid-start": "Invalid starting character in range \"{range}\"", 6 | "invalid-end": "Invalid ending character in range \"{range}\"", 7 | "invalid": "Invalid range \"{range}\"", 8 | "unterminated": "Unterminated range \"{range}\"" 9 | }, 10 | "cloud-kit": { 11 | "auth-failure": "Authentication failure", 12 | "premature-close": "Sign-in window was prematurely closed", 13 | "no-auth-url": "No authorization URL in response", 14 | "popup-blocked": "Pop-up blocked, please try again" 15 | } 16 | }, 17 | "button": { 18 | "start": "Start", 19 | "decrypt": "Decrypt", 20 | "next": "Next", 21 | "reset": "Reset", 22 | "dismiss": "Dismiss", 23 | "add-app": "Add application", 24 | "copy": { 25 | "ready": "Copy Password", 26 | "complete": "Copied" 27 | }, 28 | "compute": { 29 | "idle": "Compute Password", 30 | "running": "Computing Password" 31 | }, 32 | "edit": "Edit", 33 | "back": "Back" 34 | }, 35 | "label": { 36 | "domain": "Domain name", 37 | "login": "Username" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/plugins/auto-logout/index.js: -------------------------------------------------------------------------------- 1 | import * as createDebug from 'debug'; 2 | 3 | const debug = createDebug('derivepass:auto-logout'); 4 | 5 | class AutoLogout { 6 | constructor() { 7 | this.timer = null; 8 | this.timeout = null; 9 | this.doLogout = null; 10 | } 11 | 12 | login(doLogout, timeout) { 13 | debug('login with timeout=%d', timeout); 14 | this.doLogout = doLogout; 15 | this.timeout = timeout; 16 | 17 | this.reset(); 18 | } 19 | 20 | logout() { 21 | debug('logout'); 22 | if (this.doLogout) { 23 | const fn = this.doLogout; 24 | this.doLogout = null; 25 | fn(); 26 | } 27 | } 28 | 29 | reset() { 30 | if (!this.doLogout) { 31 | return; 32 | } 33 | 34 | if (this.timer) { 35 | clearTimeout(this.timer); 36 | } 37 | 38 | this.timer = setTimeout(() => { 39 | debug('logout timeout'); 40 | this.timer = null; 41 | 42 | this.logout(); 43 | }, this.timeout); 44 | } 45 | } 46 | 47 | export default { 48 | install(Vue) { 49 | const instance = new AutoLogout(); 50 | Vue.prototype.$autoLogout = instance; 51 | 52 | window.addEventListener('keypress', () => instance.reset()); 53 | window.addEventListener('click', () => instance.reset()); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "flatten": { 4 | "whitespace": "No pot haver-hi espais en blanc", 5 | "invalid-start": "Hi ha un caràcter inicial no vàlid al rang \"{range}\"", 6 | "invalid-end": "Hi ha un caràcter final no vàlid al rang \"{range}\"", 7 | "invalid": "Rang no vàlid \"{range}\"", 8 | "unterminated": "Rang no acabat \"{range}\"" 9 | }, 10 | "cloud-kit": { 11 | "auth-failure": "Error d'autenticació", 12 | "premature-close": "La finestra d'autenticació s'ha tancat prematurament", 13 | "no-auth-url": "No hi ha una URL d'autorització a la resposta", 14 | "popup-blocked": "El pop-up ha estat blocat, intenta-ho de nou si et plau" 15 | } 16 | }, 17 | "button": { 18 | "start": "Comença", 19 | "decrypt": "Desxifra", 20 | "next": "Següent", 21 | "reset": "Reinicia", 22 | "dismiss": "Ignora", 23 | "add-app": "Afegeix aplicació", 24 | "copy": { 25 | "ready": "Copia Contrasenya", 26 | "complete": "Copiada" 27 | }, 28 | "compute": { 29 | "idle": "Computa Contrasenya", 30 | "running": "Computant Contrasenya" 31 | }, 32 | "edit": "Edita", 33 | "back": "Enrere" 34 | }, 35 | "label": { 36 | "domain": "Nom de domini", 37 | "login": "Nom d'usuari" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "flatten": { 4 | "whitespace": "Не может содержать пробелы", 5 | "invalid-start": "Неверный начальный символ диапазона \"{range}\"", 6 | "invalid-end": "Неверный конечный символ диапазона \"{range}\"", 7 | "invalid": "Неверный диапазон \"{range}\"", 8 | "unterminated": "Незаконченный диапазон \"{range}\"" 9 | }, 10 | "cloud-kit": { 11 | "auth-failure": "Ошибка аутентификации", 12 | "premature-close": "Окно аутентификации было закрыто до завершения операции", 13 | "no-auth-url": "Отсутсвует URL авторизации в ответе сервера", 14 | "popup-blocked": "Окно заблокиравоно. Пожалуйста, попробуйте повторить действие позже" 15 | } 16 | }, 17 | "button": { 18 | "start": "Начать", 19 | "decrypt": "Расшифровать", 20 | "next": "Далее", 21 | "reset": "Сброс", 22 | "dismiss": "Скрыть", 23 | "add-app": "Новое приложение", 24 | "copy": { 25 | "ready": "Скопировать Пароль", 26 | "complete": "Скопирован" 27 | }, 28 | "compute": { 29 | "idle": "Сгенерировать Пароль", 30 | "running": "Генерируем Пароль" 31 | }, 32 | "edit": "Редактировать", 33 | "back": "Назад" 34 | }, 35 | "label": { 36 | "domain": "Доменное имя", 37 | "login": "Имя пользователя" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/computing.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import * as createDebug from 'debug'; 4 | 5 | import { LOCALE_KEY } from './utils/common'; 6 | 7 | const debug = createDebug('derivepass:i18n'); 8 | 9 | Vue.use(VueI18n) 10 | 11 | let locale = 'en'; 12 | 13 | // Guess locale 14 | try { 15 | let guess; 16 | if (!localStorage.getItem(LOCALE_KEY)) { 17 | guess = navigator.language.split('-')[0]; 18 | } 19 | if (guess) { 20 | locale = guess; 21 | } 22 | 23 | debug('guessed locale %j', locale); 24 | } catch (e) { 25 | // Ignore 26 | } 27 | 28 | try { 29 | locale = localStorage.getItem(LOCALE_KEY) || locale; 30 | 31 | debug('retrieved locale %j', locale); 32 | } catch (e) { 33 | // Ignore 34 | } 35 | 36 | function loadLocaleMessages () { 37 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i); 38 | const messages = {}; 39 | locales.keys().forEach(key => { 40 | const matched = key.match(/([A-Za-z0-9-_]+)\./i); 41 | if (matched && matched.length > 1) { 42 | const locale = matched[1]; 43 | messages[locale] = locales(key); 44 | } 45 | }) 46 | return messages; 47 | } 48 | 49 | export default new VueI18n({ 50 | locale, 51 | fallbackLocale: 'en', 52 | messages: loadLocaleMessages(), 53 | localeDir: 'locales', 54 | enableInSFC: true, 55 | }); 56 | -------------------------------------------------------------------------------- /src/utils/sync/local-storage.js: -------------------------------------------------------------------------------- 1 | import Sync from './base'; 2 | import { ENV } from '../common'; 3 | import * as createDebug from 'debug'; 4 | 5 | const debug = createDebug('derivepass:sync:local-storage'); 6 | 7 | const PREFIX = `derivepass/${ENV}`; 8 | 9 | export default class LocalStorage extends Sync { 10 | constructor(store) { 11 | super(store); 12 | 13 | this.db = window.localStorage; 14 | this.isBuffering = false; 15 | 16 | this.start(); 17 | } 18 | 19 | start() { 20 | if (!this.db) { 21 | return; 22 | } 23 | 24 | let count = 0; 25 | for (let i = 0; i < this.db.length; i++) { 26 | const key = this.db.key(i); 27 | if (!key.startsWith(PREFIX)) { 28 | continue; 29 | } 30 | let app; 31 | try { 32 | app = JSON.parse(this.db.getItem(key)); 33 | } catch (e) { 34 | this.db.removeItem(key); 35 | continue; 36 | } 37 | 38 | count++; 39 | this.receiveApp(app); 40 | } 41 | debug('emitting initial db.len=%d', count); 42 | 43 | this.subscribe(); 44 | } 45 | 46 | async sendApps(uuids) { 47 | if (!this.db) { 48 | return; 49 | } 50 | 51 | const apps = this.getApps(uuids); 52 | for (const app of apps) { 53 | this.db.setItem(`${PREFIX}/${app.uuid}`, JSON.stringify(app)); 54 | } 55 | debug('updated uuids.len=%d', uuids.length); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const CspHtmlWebpackPlugin = require('csp-html-webpack-plugin'); 2 | 3 | module.exports = { 4 | pwa: { 5 | name: 'DerivePass', 6 | themeColor: '#3D79DE', 7 | msTileColor: '#3d79de', 8 | workboxPluginMode: 'InjectManifest', 9 | workboxOptions: { 10 | importWorkboxFrom: 'local', 11 | swSrc: 'src/plugins/service-worker/service-worker.js', 12 | }, 13 | }, 14 | 15 | pluginOptions: { 16 | i18n: { 17 | locale: 'en', 18 | fallbackLocale: 'en', 19 | localeDir: 'locales', 20 | enableInSFC: true 21 | } 22 | }, 23 | 24 | configureWebpack: { 25 | plugins: [ 26 | new CspHtmlWebpackPlugin({ 27 | 'default-src': '\'self\'', 28 | 'script-src': '\'self\'', 29 | 'img-src': [ '\'self\'', 'data:' ], 30 | 'style-src': [ '\'self\'', '\'unsafe-inline\'' ], 31 | 'connect-src': [ '\'self\'', 'data:', 'https://api.apple-cloudkit.com' ], 32 | }, { 33 | enabled: () => { 34 | // Dev builds should be possible to run locally! 35 | return process.env.NODE_ENV !== 'development'; 36 | }, 37 | 38 | // TODO(indutny): reconsider those 39 | hashEnabled: { 40 | 'script-src': false, 41 | 'style-src': false, 42 | }, 43 | nonceEnabled: { 44 | 'script-src': false, 45 | 'style-src': false, 46 | }, 47 | }), 48 | ], 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /src/components/fancy-logo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /tests/unit/crypto.spec.js: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import '../fixtures/polyfill'; 4 | 5 | import * as crypto from '../../src/utils/crypto'; 6 | import { parseAppOptions } from '../../src/utils/common'; 7 | 8 | describe('crypto', () => { 9 | it('should convert to hex', () => { 10 | assert.strictEqual(crypto.toHex([ 1, 15, 83 ]), '010f53'); 11 | }); 12 | 13 | it('should convert from hex', () => { 14 | assert.deepStrictEqual(Array.from(crypto.fromHex('010f53')), [ 1, 15, 83 ]); 15 | }); 16 | 17 | it('should compute password entropy bits', () => { 18 | const bits = crypto.passwordEntropyBits({ 19 | union: new Array(14), 20 | maxLength: 24, 21 | }); 22 | 23 | assert.strictEqual(bits, 92); 24 | }); 25 | 26 | it('should compute legacy password', () => { 27 | // master: hello, domain: gmail.com/test 28 | const pass = crypto.computeLegacyPassword([ 29 | 0x6f, 0x8a, 0xf9, 0x70, 0xc3, 0x42, 0x75, 0xc2, 30 | 0xc9, 0x67, 0x96, 0xab, 0xa0, 0x2a, 0x39, 0x08, 31 | 0x63, 0x3b, 32 | ]); 33 | assert.strictEqual(pass, 'b4r5cMNCdcLJZ5aroCo5CGM7'); 34 | }); 35 | 36 | it('should compute password', () => { 37 | // master: test, domain: test.com/indutny 38 | const pass = crypto.computePassword([ 39 | 185, 200, 253, 102, 60, 26, 11, 22, 171, 244, 181, 40 | ], parseAppOptions({ 41 | allowed: '0-9', 42 | required: '_@', 43 | maxLength: 24, 44 | })); 45 | assert.strictEqual(pass, '903442501816978_9324552@'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/store/derivepass/derive.worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import Binding from './wasm'; 3 | 4 | const SCRYPT_R = 8; 5 | const SCRYPT_N = 32768; 6 | const SCRYPT_P = 4; 7 | 8 | const wasm = new Binding(); 9 | 10 | function derivepass(master, domain, outSize) { 11 | return wasm.derive(SCRYPT_R, SCRYPT_N, SCRYPT_P, master, domain, outSize); 12 | } 13 | 14 | wasm.init().then(() => { 15 | postMessage({ type: 'ready', payload: null }); 16 | }); 17 | 18 | onmessage = (e) => { 19 | const { type, payload } = e.data; 20 | 21 | let res; 22 | try { 23 | if (type === 'derivepass') { 24 | res = { 25 | type: 'derivepass', 26 | payload: derivepass(payload.master, payload.domain, payload.outSize), 27 | }; 28 | } else if (type === 'encrypt') { 29 | res = { 30 | type: 'encrypt', 31 | payload: wasm.encrypt(payload.keys.aesKey, payload.keys.macKey, 32 | payload.iv, payload.data), 33 | }; 34 | } else if (type === 'decrypt') { 35 | res = { 36 | type: 'decrypt', 37 | payload: wasm.decrypt(payload.keys.aesKey, payload.keys.macKey, 38 | payload.data), 39 | }; 40 | } else if (type === 'decrypt_legacy') { 41 | res = { 42 | type: 'decrypt_legacy', 43 | payload: wasm.decrypt_legacy(payload.aesKey, payload.data), 44 | }; 45 | } else { 46 | res = { 47 | type: 'error', 48 | payload: `Unknown message type "${type}"`, 49 | }; 50 | } 51 | } catch (e) { 52 | res = { type: 'error', payload: e.message ? (e.message + e.stack) : e }; 53 | } 54 | postMessage(res); 55 | }; 56 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import VueRouter from 'vue-router'; 4 | 5 | import plugins from './plugins'; 6 | import routes from './routes'; 7 | 8 | import App from './App.vue'; 9 | 10 | // Store 11 | import storeConfig from './store/index'; 12 | 13 | import testFeatures from './utils/feature-test'; 14 | import i18n from './i18n' 15 | 16 | const missingFeatures = testFeatures(); 17 | if (missingFeatures) { 18 | const title = document.createElement('b'); 19 | title.textContent = 20 | 'Following features are required for running this application:' 21 | document.body.appendChild(title); 22 | for (const feature of missingFeatures) { 23 | const elem = document.createElement('p'); 24 | elem.textContent = `* ${feature}`; 25 | document.body.appendChild(elem); 26 | } 27 | } 28 | 29 | Vue.config.productionTip = false; 30 | 31 | plugins.install(Vue); 32 | 33 | const store = new Vuex.Store(storeConfig); 34 | const router = new VueRouter({ 35 | mode: 'history', 36 | routes, 37 | }); 38 | 39 | router.beforeEach((to, from, next) => { 40 | if (to.matched.some((route) => route.meta.requiresAuth)) { 41 | if (store.getters.isLoggedIn) { 42 | next(); 43 | } else { 44 | next({ path: '/master' }); 45 | } 46 | } else if (to.matched.some((route) => route.meta.noAuth)) { 47 | if (store.getters.isLoggedIn) { 48 | next({ path: '/applications' }); 49 | } else { 50 | next(); 51 | } 52 | } else { 53 | next(); 54 | } 55 | }); 56 | 57 | plugins.installStoreDependent(Vue, { store }); 58 | 59 | new Vue({ 60 | store, 61 | router, 62 | i18n, 63 | render: (h) => h(App) 64 | }).$mount('#app'); 65 | -------------------------------------------------------------------------------- /src/presets.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_APP_OPTIONS } from './utils/common'; 2 | 3 | const RAW = { 4 | 'google.com': { 5 | alias: [ 'accounts.google.com', 'gmail.com', 'youtube.com' ], 6 | }, 7 | 8 | 'facebook.com': { 9 | alias: [ 'fb.com' ], 10 | }, 11 | 12 | 'yahoo.com': { 13 | alias: [ 'login.yahoo.com' ], 14 | }, 15 | 16 | 'live.com': { 17 | alias: [ 'signup.live.com' ], 18 | options: { 19 | required: '@', 20 | }, 21 | }, 22 | 23 | 'paypal.com': { 24 | options: { 25 | required: '@', 26 | maxLength: 20, 27 | }, 28 | }, 29 | 30 | 'easyjet.com': { 31 | options: { 32 | maxLength: 20, 33 | }, 34 | }, 35 | 36 | // NOTE: >= 3 repeating characters are disallowed 37 | 'nintendo.com': { 38 | alias: [ 'accounts.nintendo.com' ], 39 | options: { 40 | required: '@', 41 | maxLength: 20, 42 | }, 43 | }, 44 | 45 | 'nic.ru': { 46 | options: { 47 | allowed: 'a-zA-Z0-9', 48 | }, 49 | }, 50 | 51 | 'hrblock.com': { 52 | options: { 53 | required: '$', 54 | }, 55 | }, 56 | 57 | 'nyumlc.org': { 58 | options: { 59 | maxLength: 20, 60 | }, 61 | }, 62 | 63 | 'redislabs.com': { 64 | options: { 65 | required: '$', 66 | }, 67 | }, 68 | } 69 | 70 | const PRESETS = new Map(); 71 | 72 | Object.keys(RAW).forEach((domain) => { 73 | const entry = RAW[domain]; 74 | const domains = [ domain ].concat(entry.alias); 75 | 76 | for (const alias of domains) { 77 | PRESETS.set(alias, { 78 | domain, 79 | 80 | options: Object.assign({}, DEFAULT_APP_OPTIONS, entry.options || {}), 81 | }); 82 | } 83 | }); 84 | 85 | export default PRESETS; 86 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | DerivePass 26 | 27 | 28 | 31 | 32 |
33 |
34 | Loading... 35 |
36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as createDebug from 'debug'; 2 | 3 | const debug = createDebug('derivepass:store'); 4 | 5 | export default { 6 | receiveApp(state, app) { 7 | const existing = state.applications.find((existing) => { 8 | return existing.uuid === app.uuid; 9 | }); 10 | 11 | if (!existing) { 12 | debug('new app with uuid: %j', app.uuid); 13 | state.applications.push(app); 14 | return; 15 | } 16 | 17 | if (existing.changedAt >= app.changedAt) { 18 | return; 19 | } 20 | 21 | // NOTE: `removed` should not flip from `true` to `false` 22 | const removed = existing.removed || app.removed; 23 | let changedAt = app.changedAt; 24 | 25 | if (removed !== app.removed) { 26 | changedAt = Date.now(); 27 | } 28 | 29 | debug('updating existing app %j from %j to %j', app.uuid, 30 | existing.changedAt, 31 | app.changedAt); 32 | Object.assign(existing, app, { 33 | removed, 34 | changedAt, 35 | }); 36 | }, 37 | 38 | updateDecryptedApp(state, app) { 39 | const existing = state.decryptedApps.find((decrypted) => { 40 | return decrypted.uuid === app.uuid; 41 | }); 42 | 43 | if (!existing) { 44 | // Strange, but okay? 45 | if (app.removed) { 46 | return; 47 | } 48 | 49 | debug('new decrypted app.uuid=%j', app.uuid); 50 | state.decryptedApps.push(Object.assign({}, app)); 51 | 52 | // Display recently modified apps first 53 | state.decryptedApps.sort((a, b) => { 54 | return b.changedAt - a.changedAt; 55 | }); 56 | return; 57 | } 58 | 59 | if (app.removed) { 60 | debug('removed decrypted app.uuid=%j', app.uuid); 61 | state.decryptedApps.splice(state.decryptedApps.indexOf(existing), 1); 62 | return; 63 | } 64 | 65 | debug('updated decrypted app.uuid=%j', app.uuid); 66 | Object.assign(existing, app); 67 | }, 68 | 69 | setCryptoKeys(state, payload) { 70 | state.master = payload.master; 71 | state.cryptoKeys = payload.crypto; 72 | }, 73 | 74 | resetCryptoKeys(state) { 75 | state.cryptoKeys = null; 76 | state.master = ''; 77 | state.decryptedApps = []; 78 | }, 79 | 80 | setDecryptedApps(state, payload) { 81 | state.decryptedApps = payload; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | export const ENV = 2 | location.protocol === 'derivepass:' ? 'electron' : 3 | process.env.NODE_ENV === 'production' ? 'production' : 'development'; 4 | 5 | export const LOCALE_KEY = 'derivepass/config/locale'; 6 | 7 | export const DEFAULT_APP_OPTIONS = { 8 | allowed: 'a-zA-Z0-9_.', 9 | required: '', 10 | maxLength: 24, 11 | }; 12 | 13 | export class LocaleError extends Error { 14 | constructor(message, tag, extra = {}) { 15 | super(message); 16 | 17 | this.tag = tag; 18 | this.extra = extra; 19 | } 20 | } 21 | 22 | export function flattenRange(str) { 23 | if (/\s/.test(str)) { 24 | throw new Error(new LocaleError('Can\'t contain whitespace', 25 | 'error.flatten.whitespace')); 26 | } 27 | 28 | // a-zA-Z 29 | str = str.replace(/(\w)-(\w)/g, (range, from, to) => { 30 | const fromCode = from.charCodeAt(0); 31 | const toCode = to.charCodeAt(0); 32 | 33 | if (from.length !== 1 || fromCode > 0xff) { 34 | throw new LocaleError(`Invalid starting character in range "${range}"`, 35 | 'error.flatten.invalid-start', { range }); 36 | } 37 | if (to.length !== 1 || toCode > 0xff) { 38 | throw new LocaleError(`Invalid ending character in range "${range}"`, 39 | 'error.flatten.invalid-end', { range }); 40 | } 41 | 42 | if (fromCode > toCode) { 43 | throw new LocaleError(`Invalid range "${range}"`, 'error.flatten.invalid', 44 | { range }); 45 | } 46 | 47 | let res = ''; 48 | for (let code = fromCode; code <= toCode; code++) { 49 | res += String.fromCharCode(code); 50 | } 51 | return res; 52 | }); 53 | 54 | // Report invalid ranges 55 | str.replace(/[^\\]-|[^\\]-\W|^-\W|^-\w?/, (invalid) => { 56 | throw new LocaleError(`Unterminated range "${invalid}"`, 57 | 'error.flatten.unterminated', { range: invalid }); 58 | }); 59 | 60 | // Unescape `\x` => `x` 61 | str = str.replace(/\\(.)/g, '$1'); 62 | 63 | return Array.from(new Set(str.split(''))).sort(); 64 | } 65 | 66 | export function parseAppOptions(options) { 67 | const allowed = flattenRange(options.allowed); 68 | const required = flattenRange(options.required); 69 | const union = Array.from(new Set(allowed.concat(required))).sort(); 70 | 71 | return Object.assign({}, options, { 72 | allowed, 73 | required, 74 | union, 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | import * as createDebug from 'debug'; 2 | 3 | const debug = createDebug('derivepass:store:actions'); 4 | 5 | function scrubApp(app) { 6 | if (!app.domain && !app.login && !app.revision && !app.options && 7 | !app.master && !app.index) { 8 | // Already scrubbed 9 | return app; 10 | } 11 | 12 | // Scrub 13 | debug('scrubbing app with uuid: %j', app.uuid); 14 | return Object.assign({}, app, { 15 | changedAt: Date.now(), 16 | domain: '', 17 | login: '', 18 | revision: '', 19 | options: '', 20 | master: '', 21 | index: 0, 22 | }); 23 | } 24 | 25 | export default { 26 | receiveApp({ state, getters, commit }, app) { 27 | if (app.removed) { 28 | app = scrubApp(app); 29 | } else if (app.master) { 30 | // Scrub emojis 31 | app = Object.assign({}, app, { changedAt: Date.now(), master: '' }); 32 | } 33 | commit('receiveApp', app); 34 | 35 | if (app.removed) { 36 | // Shortcut 37 | commit('updateDecryptedApp', app); 38 | return; 39 | } 40 | 41 | if (!getters.isLoggedIn) { 42 | return; 43 | } 44 | 45 | state.derivepass.decryptApp(app, state.cryptoKeys).then((decrypted) => { 46 | if (!decrypted) { 47 | return; 48 | } 49 | 50 | commit('updateDecryptedApp', decrypted); 51 | }).catch((err) => { 52 | debug('decryption error=%j', err.message); 53 | }); 54 | }, 55 | 56 | async receiveDecryptedApp({ state, dispatch }, app) { 57 | const encrypted = await state.derivepass.encryptApp(app, state.cryptoKeys); 58 | dispatch('receiveApp', encrypted); 59 | }, 60 | 61 | async setCryptoKeys({ state, commit }, payload) { 62 | const matchingApps = state.applications.filter((app) => { 63 | return !app.removed; 64 | }); 65 | 66 | let decryptedApps = await Promise.all(matchingApps.map(async (app) => { 67 | return await state.derivepass.decryptApp(app, payload.crypto); 68 | })); 69 | 70 | // Filter out any apps encrypted with a different AES key 71 | decryptedApps = decryptedApps.filter((app) => app !== null); 72 | 73 | // Display recently modified apps first 74 | decryptedApps.sort((a, b) => { 75 | return b.changedAt - a.changedAt; 76 | }); 77 | 78 | commit('setCryptoKeys', payload); 79 | commit('setDecryptedApps', decryptedApps); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/qr.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "title": "QR Sync", 5 | "description": "Sync data directly between two devices by displaying QR codes on one, and scanning them on another.", 6 | "send": "Send", 7 | "stop-send": "Stop sending", 8 | "receive": "Receive", 9 | "stop-receive": "Stop receiving" 10 | }, 11 | "ru": { 12 | "title": "QR Синхронизация", 13 | "description": "Синхронизуйте данные напрямую между двумя устройствами, сканируя QR коды с одного на другое устройство.", 14 | "send": "Отправить", 15 | "stop-send": "Остановить отправку", 16 | "receive": "Получить", 17 | "stop-receive": "Прекратить получение" 18 | }, 19 | "ca": { 20 | "title": "Sincronització QR", 21 | "description": "Sincronitza dades entre dos dispositius mostrant codis QR a un d'ells, i escanejant-los amb l'altre.", 22 | "send": "Envia", 23 | "stop-send": "Atura enviament", 24 | "receive": "Rep", 25 | "stop-receive": "Atura recepció" 26 | } 27 | } 28 | 29 | 30 | 54 | 55 | 76 | -------------------------------------------------------------------------------- /src/utils/sync/base.js: -------------------------------------------------------------------------------- 1 | import * as createDebug from 'debug'; 2 | 3 | const debug = createDebug('derivepass:utils:sync:base'); 4 | 5 | const COALESCE_DELAY = 50; 6 | 7 | export default class Sync { 8 | constructor(store) { 9 | this.store = store; 10 | 11 | this.state = 'init'; 12 | 13 | this.received = new Map(); 14 | this.buffer = new Set(); 15 | this.bufferTimer = null; 16 | 17 | this.isBuffering = true; 18 | } 19 | 20 | subscribe() { 21 | if (this.state === 'init') { 22 | this.store.subscribe(({ type, payload }) => { 23 | if (type !== 'receiveApp') { 24 | return; 25 | } 26 | 27 | if (this.state === 'subscribed') { 28 | this.onAppChange(payload); 29 | } 30 | }); 31 | } 32 | 33 | this.state = 'subscribed'; 34 | 35 | // Feed all past app changes 36 | for (const app of this.store.state.applications) { 37 | this.onAppChange(app); 38 | } 39 | } 40 | 41 | unsubscribe() { 42 | this.state = 'unsubscribed'; 43 | } 44 | 45 | receiveApp(app) { 46 | this.received.set(app.uuid, Object.assign({}, app)); 47 | this.store.dispatch('receiveApp', app); 48 | } 49 | 50 | sendApps() { 51 | throw new Error('Not implemented'); 52 | } 53 | 54 | getApps(uuids) { 55 | // Could this be more efficient? 56 | return uuids.map((uuid) => { 57 | return this.store.state.applications.find((app) => { 58 | return app.uuid === uuid; 59 | }); 60 | }); 61 | } 62 | 63 | // Internal 64 | 65 | onAppChange(app) { 66 | if (this.received.has(app.uuid)) { 67 | const existing = this.received.get(app.uuid); 68 | // Avoid spurious changes 69 | if (existing.changedAt >= app.changedAt) { 70 | return; 71 | } 72 | } 73 | 74 | debug('received app with uuid %j changedAt %j', app.uuid, app.changedAt); 75 | 76 | this.received.set(app.uuid, app); 77 | 78 | // Store apps immediately when storage isn't remote 79 | if (!this.isBuffering) { 80 | this.sendApps([ app.uuid ]); 81 | return; 82 | } 83 | 84 | this.buffer.add(app.uuid); 85 | 86 | if (this.bufferTimer) { 87 | return; 88 | } 89 | this.bufferTimer = setTimeout(() => { 90 | const uuids = Array.from(this.buffer); 91 | this.buffer.clear(); 92 | this.bufferTimer = null; 93 | 94 | this.sendApps(uuids); 95 | }, COALESCE_DELAY); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/plugins/service-worker/index.js: -------------------------------------------------------------------------------- 1 | import { register } from 'register-service-worker' 2 | import * as createDebug from 'debug'; 3 | 4 | const debug = createDebug('derivepass:register-service-worker'); 5 | 6 | const UPDATE_EVERY = 3600 * 1000; // 1 hour 7 | 8 | class ServiceWorker { 9 | constructor() { 10 | this.updateQueue = []; 11 | this.registration = undefined; 12 | 13 | if (process.env.NODE_ENV !== 'production') { 14 | debug('running in debug mode, no service-worker used'); 15 | return; 16 | } 17 | 18 | register(`${process.env.BASE_URL}service-worker.js`, { 19 | ready() { 20 | debug('app is being served from cache by a service worker'); 21 | }, 22 | registered: (reg) => { 23 | debug('service worker has been registered'); 24 | 25 | this.registration = reg; 26 | setInterval(() => reg.update(), UPDATE_EVERY); 27 | }, 28 | cached() { 29 | debug('content has been cached for offline use'); 30 | }, 31 | updatefound() { 32 | debug('new content is downloading'); 33 | }, 34 | updated: () => { 35 | debug('new content is available; please refresh'); 36 | const queue = this.updateQueue; 37 | this.updateQueue = []; 38 | for (const resolve of queue) { 39 | resolve(); 40 | } 41 | }, 42 | offline() { 43 | debug('no internet connection found. App is running in offline mode'); 44 | }, 45 | error(error) { 46 | debug('error during service worker registration:', error); 47 | } 48 | }); 49 | } 50 | 51 | async whenUpdated() { 52 | await new Promise((resolve) => this.updateQueue.push(resolve)); 53 | } 54 | 55 | async update() { 56 | const reg = this.registration; 57 | if (!reg) { 58 | throw new Error('No updates available (no active Service Worker)'); 59 | } 60 | 61 | if (!reg.waiting) { 62 | throw new Error('No updates available (no waiting Service Worker)'); 63 | } 64 | 65 | const waiting = reg.waiting; 66 | 67 | await new Promise((resolve, reject) => { 68 | const channel = new MessageChannel(); 69 | 70 | channel.port1.onmessage = (e) => { 71 | const { type, payload } = e.data; 72 | if (type === 'ok') { 73 | resolve(); 74 | } else if (type === 'error') { 75 | reject(new Error(payload)); 76 | } 77 | }; 78 | 79 | waiting.postMessage({ type: 'update' }, [ channel.port2 ]); 80 | }); 81 | } 82 | } 83 | 84 | export default { 85 | install(Vue) { 86 | Vue.prototype.$serviceWorker = new ServiceWorker(); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DerivePass", 3 | "version": "1.2.1", 4 | "private": true, 5 | "author": "Fedor Indutny ", 6 | "description": "Compute secure passwords without storing them anywhere", 7 | "license": "MIT", 8 | "main": "src/electron/main.js", 9 | "repository": "git@github.com:derivepass/derivepass-vue", 10 | "build": { 11 | "appId": "com.indutny.derivepass-electron", 12 | "productName": "DerivePass", 13 | "mac": { 14 | "category": "public.app-category.productivity" 15 | }, 16 | "publish": [ 17 | "github" 18 | ], 19 | "files": [ 20 | "src/electron", 21 | "dist" 22 | ], 23 | "directories": { 24 | "output": "dist-electron" 25 | } 26 | }, 27 | "scripts": { 28 | "serve": "vue-cli-service serve", 29 | "build": "vue-cli-service build", 30 | "lint": "vue-cli-service lint", 31 | "now-build": "npm run build && cp -rf dist/precache-manifest*.js dist/precache-manifest.common.js", 32 | "test:unit": "vue-cli-service test:unit", 33 | "electron": "electron .", 34 | "electron:build": "npm run build && electron-builder", 35 | "electron:publish": "npm run electron:build -- -p always" 36 | }, 37 | "devDependencies": { 38 | "@kazupon/vue-i18n-loader": "^0.3.0", 39 | "@vue/cli-plugin-eslint": "^3.5.1", 40 | "@vue/cli-plugin-pwa": "^3.5.1", 41 | "@vue/cli-plugin-unit-mocha": "^3.5.1", 42 | "@vue/cli-service": "^3.5.3", 43 | "@vue/test-utils": "^1.0.0-beta.29", 44 | "babel-eslint": "^10.0.1", 45 | "bn.js": "^4.11.8", 46 | "bootstrap-vue": "^2.0.0-rc.18", 47 | "chai": "^4.1.2", 48 | "csp-html-webpack-plugin": "^3.0.1", 49 | "debug": "^4.1.1", 50 | "electron": "^4.1.4", 51 | "electron-builder": "^20.39.0", 52 | "eslint": "^5.16.0", 53 | "eslint-plugin-vue": "^5.2.2", 54 | "hash.js": "^1.1.7", 55 | "jsqr": "^1.2.0", 56 | "mocha": "^5.2.0", 57 | "qr-image": "^3.2.0", 58 | "register-service-worker": "^1.6.2", 59 | "uuid": "^3.3.2", 60 | "vue": "^2.6.10", 61 | "vue-cli-plugin-i18n": "^0.5.2", 62 | "vue-clipboard2": "^0.2.1", 63 | "vue-i18n": "^8.10.0", 64 | "vue-router": "^3.0.3", 65 | "vue-template-compiler": "^2.6.10", 66 | "vuex": "^3.1.0", 67 | "worker-loader": "^2.0.0" 68 | }, 69 | "eslintConfig": { 70 | "root": true, 71 | "env": { 72 | "node": true 73 | }, 74 | "extends": [ 75 | "plugin:vue/essential", 76 | "eslint:recommended" 77 | ], 78 | "rules": {}, 79 | "parserOptions": { 80 | "parser": "babel-eslint" 81 | } 82 | }, 83 | "postcss": { 84 | "plugins": { 85 | "autoprefixer": {} 86 | } 87 | }, 88 | "browserslist": [ 89 | "> 1%", 90 | "last 2 versions", 91 | "not ie <= 8" 92 | ], 93 | "dependencies": { 94 | "electron-log": "^3.0.5", 95 | "electron-updater": "^4.0.6" 96 | }, 97 | "engines": { "node": "12.x" } 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | // TODO(indutny): dumb-crypto and wasm 2 | import * as BN from 'bn.js'; 3 | 4 | export const AES_KEY_SIZE = 32; 5 | export const IV_SIZE = 16; 6 | export const MAC_KEY_SIZE = 64; 7 | export const MAC_SIZE = 32; 8 | 9 | export function fromHex(hex) { 10 | if (hex.length % 2 !== 0) { 11 | throw new Error(`Invalid hex: "${hex}"`); 12 | } 13 | 14 | const alpha = (ch) => { 15 | // 0-9 16 | if (ch >= 0x30 && ch <= 0x39) { 17 | return ch - 0x30; 18 | } else if (ch >= 0x41 && ch <= 0x46) { 19 | return ch - 0x41 + 10; 20 | } else if (ch >= 0x61 && ch <= 0x66) { 21 | return ch - 0x61 + 10; 22 | } else { 23 | throw new Error('Not a hex character, code: ' + ch); 24 | } 25 | }; 26 | 27 | const out = new Uint8Array(hex.length / 2); 28 | for (let i = 0; i < hex.length; i += 2) { 29 | const hi = alpha(hex.charCodeAt(i)); 30 | const lo = alpha(hex.charCodeAt(i + 1)); 31 | 32 | out[i >> 1] = (hi << 4) | lo; 33 | } 34 | return out; 35 | } 36 | 37 | export function toHex(buf) { 38 | let res = ''; 39 | for (let i = 0; i < buf.length; i++) { 40 | let d = buf[i].toString(16); 41 | if (d.length < 2) { 42 | d = '0' + d; 43 | } 44 | res += d; 45 | } 46 | return res; 47 | } 48 | 49 | // Password generation 50 | 51 | // NOTE: this is upper bound for an entropy, lower bound depends on the size 52 | // of `required` array. 53 | export function passwordEntropyBits(options) { 54 | return Math.ceil(Math.log2(options.union.length) * options.maxLength); 55 | } 56 | 57 | export const LEGACY_PASSWORD_SIZE = 18; 58 | export const PASSWORD_BASE64 = 59 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.'.split(''); 60 | 61 | export function computeLegacyPassword(raw) { 62 | if (raw.length !== LEGACY_PASSWORD_SIZE) { 63 | throw new Error('Invalid raw bytes'); 64 | } 65 | 66 | let out = ''; 67 | for (let i = 0; i < raw.length; i += 3) { 68 | const a = raw[i]; 69 | const b = raw[i + 1]; 70 | const c = raw[i + 2]; 71 | 72 | out += PASSWORD_BASE64[a >>> 2]; 73 | out += PASSWORD_BASE64[((a & 3) << 4) | (b >>> 4)]; 74 | out += PASSWORD_BASE64[((b & 0x0f) << 2) | (c >>> 6)]; 75 | out += PASSWORD_BASE64[c & 0x3f]; 76 | } 77 | 78 | return out; 79 | } 80 | 81 | export function computePassword(raw, options) { 82 | const num = new BN(Array.from(raw), 'le'); 83 | 84 | const required = new Set(options.required); 85 | 86 | let out = ''; 87 | while (out.length < options.maxLength) { 88 | let alphabet; 89 | 90 | // Emitted all required chars, move to allowed 91 | if (required.size === 0) { 92 | alphabet = options.allowed; 93 | 94 | // Remaining space has to be filled with required chars 95 | } else if (required.size === options.maxLength - out.length) { 96 | alphabet = Array.from(required); 97 | 98 | // Just emit any chars 99 | } else { 100 | alphabet = options.union; 101 | } 102 | 103 | const ch = alphabet[num.modn(alphabet.length)]; 104 | num.idivn(alphabet.length); 105 | 106 | required.delete(ch); 107 | out += ch; 108 | } 109 | 110 | return out; 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DerivePass 2 | 3 | *Compute secure passwords without storing them anywhere* 4 | 5 | ## About 6 | 7 | DerivePass - is Password Manager that never stores your passwords anywhere: not 8 | in the Cloud, and not even locally! Instead, the passwords are generated 9 | on-the-fly by using the Master Password and the combination of domain-name and 10 | login. This way, the passwords are unique for each website and at the same time 11 | compromising a single password does not compromise others. 12 | 13 | The project is a [VueJS](https://vuejs.org/) application, written in a 14 | JavaScript language. The online version is hosted on [Now][now], 15 | while the standalone version is an [Electron](https://electronjs.org/) app. 16 | 17 | ## Project setup 18 | 19 | You'll need to download the dependencies to run DerivePass locally: 20 | 21 | ``` 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | This command will start a local web-server, and will update the application 28 | through hot-reload mechanism. 29 | 30 | ``` 31 | npm run serve 32 | ``` 33 | 34 | ### Publishing 35 | 36 | Every build is automatically deployed to [Now][now], and the latest master 37 | commit lives at https://dev.derivepass.com/. 38 | 39 | Manual builds could be triggered by: 40 | 41 | ``` 42 | npm run build 43 | ``` 44 | 45 | Electron builds: 46 | 47 | ``` 48 | npm run electron:build 49 | ``` 50 | 51 | ...or just electron app (using the latest build output): 52 | 53 | ``` 54 | npm run electron 55 | ``` 56 | 57 | New releases of electron app could be published with: 58 | ``` 59 | npm run electron:publish 60 | ``` 61 | 62 | NOTE: requires `GH_TOKEN` env variable and appropriate developer certificates 63 | in the Keychain for macOS builds. 64 | 65 | ### Running tests 66 | ``` 67 | npm run test 68 | ``` 69 | 70 | ### Lints and fixes files 71 | ``` 72 | npm run lint 73 | ``` 74 | 75 | ### Credits 76 | 77 | * [Òscar Casajuana](https://github.com/elboletaire) - Catalan translation 78 | 79 | #### LICENSE 80 | 81 | This software is licensed under the MIT License. 82 | 83 | Copyright Fedor Indutny, 2019. 84 | 85 | Permission is hereby granted, free of charge, to any person obtaining a 86 | copy of this software and associated documentation files (the 87 | "Software"), to deal in the Software without restriction, including 88 | without limitation the rights to use, copy, modify, merge, publish, 89 | distribute, sublicense, and/or sell copies of the Software, and to permit 90 | persons to whom the Software is furnished to do so, subject to the 91 | following conditions: 92 | 93 | The above copyright notice and this permission notice shall be included 94 | in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 97 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 98 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 99 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 100 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 101 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 102 | USE OR OTHER DEALINGS IN THE SOFTWARE. 103 | 104 | [now]: https://zeit.co/now 105 | -------------------------------------------------------------------------------- /src/components/qr-send.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "qr": "QR code" 5 | }, 6 | "ru": { 7 | "qr": "QR код" 8 | }, 9 | "ca": { 10 | "qr": "Codi QR" 11 | } 12 | } 13 | 14 | 15 | 23 | 24 | 141 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "update": { 5 | "error": "Update error:", 6 | "running": "Updating...", 7 | "available": { 8 | "description": "Update available", 9 | "install": "install now", 10 | "or": "or in", 11 | "second": "sec | secs" 12 | } 13 | } 14 | }, 15 | "ru": { 16 | "update": { 17 | "error": "Ошибка обновления:", 18 | "running": "Обновляем...", 19 | "available": { 20 | "description": "Доступно обновление", 21 | "install": "установить сейчас", 22 | "or": "или через", 23 | "second": "сек | сек" 24 | } 25 | } 26 | }, 27 | "ca": { 28 | "update": { 29 | "error": "Error d'actualització:", 30 | "running": "Actualitzant...", 31 | "available": { 32 | "description": "Actualització disponible", 33 | "install": "instal·la ara", 34 | "or": "o en", 35 | "second": "segon | segons" 36 | } 37 | } 38 | } 39 | } 40 | 41 | 42 | 77 | 78 | 149 | -------------------------------------------------------------------------------- /src/electron/main.js: -------------------------------------------------------------------------------- 1 | const { 2 | app, BrowserWindow, protocol, session, shell, ipcMain: ipc, 3 | } = require('electron'); 4 | const log = require('electron-log'); 5 | const { autoUpdater } = require("electron-updater"); 6 | 7 | const { parse: parseURL } = require('url'); 8 | const { parse: parseQuery } = require('querystring'); 9 | const path = require('path'); 10 | const fs = require('fs'); 11 | 12 | const STATIC = path.join(__dirname, '..', '..', 'dist'); 13 | const INDEX_HTML = path.join(STATIC, 'index.html'); 14 | const PRELOAD = path.join(__dirname, 'preload.js'); 15 | 16 | // Request update every 4 hours for those who run it over prolonged periods 17 | // of time. 18 | const UPDATE_FREQUENCY = 4 * 3600 * 1000; 19 | 20 | let window = null; 21 | const iCloudQueue = []; 22 | 23 | autoUpdater.logger = log; 24 | autoUpdater.logger.transports.file.level = 'info'; 25 | log.info('App starting...'); 26 | 27 | function createWindow () { 28 | if (window) { 29 | return; 30 | } 31 | 32 | window = new BrowserWindow({ 33 | width: 800, 34 | height: 800, 35 | show: false, 36 | 37 | webPreferences: { 38 | preload: PRELOAD, 39 | 40 | // Security 41 | contextIsolation: false, 42 | nodeIntegration: false, 43 | sandbox: true, 44 | enableRemoteModule: false, 45 | 46 | // Disable unused features 47 | webgl: false, 48 | webaudio: false, 49 | }, 50 | }); 51 | window.once('ready-to-show', () => window.show()); 52 | 53 | window.loadURL('derivepass://electron/'); 54 | 55 | window.on('closed', () => { 56 | window = null; 57 | }); 58 | } 59 | 60 | protocol.registerStandardSchemes([ 'derivepass' ], { secure: true }); 61 | 62 | app.on('ready', () =>{ 63 | protocol.registerFileProtocol('derivepass', (request, callback) => { 64 | let url = request.url.replace(/^derivepass:\/\//, ''); 65 | if (!url.startsWith('electron/')) { 66 | throw new Error('Invalid url'); 67 | } 68 | 69 | url = url.replace(/^electron\//, ''); 70 | let file = path.join(STATIC, url); 71 | 72 | if (url === '' || !fs.existsSync(file)) { 73 | // Push History 74 | file = INDEX_HTML; 75 | } 76 | 77 | callback({ 78 | path: file, 79 | }); 80 | }); 81 | 82 | // Add `Origin` to CloudKit requests 83 | const webRequest = session.defaultSession.webRequest; 84 | webRequest.onBeforeSendHeaders({ 85 | urls: [ 86 | 'https://api.apple-cloudkit.com/*', 87 | ], 88 | }, (details, callback) => { 89 | const requestHeaders = Object.assign({}, details.requestHeaders, { 90 | Origin: 'https://derivepass.com', 91 | }); 92 | 93 | callback({ requestHeaders }); 94 | }); 95 | 96 | protocol.registerFileProtocol( 97 | 'cloudkit-icloud.com.indutny.derivepass', 98 | (req, callback) => { 99 | const uri = parseURL(req.url); 100 | const query = parseQuery(uri.query); 101 | 102 | callback({ path: 'about:blank' }); 103 | 104 | iCloudQueue.shift()(query); 105 | }); 106 | 107 | createWindow(); 108 | 109 | setInterval(() => { 110 | autoUpdater.checkForUpdatesAndNotify().catch(() => { 111 | // Ignore 112 | }); 113 | }, UPDATE_FREQUENCY); 114 | 115 | autoUpdater.checkForUpdatesAndNotify().catch(() => { 116 | // Ignore 117 | }); 118 | }); 119 | 120 | // Quit when all windows are closed. 121 | app.on('window-all-closed', () => { 122 | if (process.platform !== 'darwin') { 123 | app.quit(); 124 | } 125 | }); 126 | 127 | // Open windows in external browser 128 | app.on('web-contents-created', (_, contents) => { 129 | contents.on('new-window', (e, url) => { 130 | e.preventDefault(); 131 | shell.openExternal(url); 132 | }); 133 | }) 134 | 135 | app.on('activate', () => { 136 | createWindow(); 137 | }); 138 | 139 | ipc.on('icloud:auth', (_, { seq, url }) => { 140 | const popup = new BrowserWindow({ 141 | center: true, 142 | width: 500, 143 | height: 500, 144 | 145 | parent: window, 146 | webPreferences: { 147 | // Security 148 | contextIsolation: true, 149 | nodeIntegration: false, 150 | sandbox: true, 151 | enableRemoteModule: false, 152 | }, 153 | }); 154 | 155 | popup.loadURL(url); 156 | 157 | const onComplete = (payload) => { 158 | window.webContents.send('icloud:response', { seq, payload }); 159 | popup.close(); 160 | }; 161 | iCloudQueue.push(onComplete); 162 | 163 | popup.once('close', () => { 164 | const index = iCloudQueue.indexOf(onComplete); 165 | if (index !== -1) { 166 | iCloudQueue.splice(index, 1); 167 | onComplete({ canceled: true }); 168 | } 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /src/pages/application-list.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "filter": "Filter applications" 5 | }, 6 | "ru": { 7 | "filter": "Поиск приложений" 8 | }, 9 | "ca": { 10 | "filter": "Filtra aplicacions" 11 | } 12 | } 13 | 14 | 15 | 54 | 55 | 155 | 156 | 176 | -------------------------------------------------------------------------------- /src/components/cloud-kit.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "title": "iCloud Sync", 5 | "loading": { 6 | "init": "Connecting to iCloud...", 7 | "enable": "Enabling iCloud Synchronization...", 8 | "disable": "Disabling iCloud Synchronization...", 9 | "sign-in": "Signing into iCloud...", 10 | "sign-out": "Signing out of iCloud..." 11 | }, 12 | "failed": "Failed to connect to iCloud!", 13 | "details": "Details", 14 | "dismiss": "Dismiss", 15 | "enable": "Enable", 16 | "disable": "Disable", 17 | "sign-in": "Sign In", 18 | "sign-out": "Sign Out" 19 | }, 20 | "ru": { 21 | "title": "Синхронизация с iCloud", 22 | "loading": { 23 | "init": "Подключение к iCloud...", 24 | "enable": "Включение синхронизации с iCloud...", 25 | "disable": "Отключение синхронизации с iCloud...", 26 | "sign-in": "Авторизация iCloud...", 27 | "sign-out": "Выход из iCloud..." 28 | }, 29 | "failed": "Не удалось соединиться с iCloud!", 30 | "details": "Подробности", 31 | "dismiss": "Скрыть", 32 | "enable": "Включить", 33 | "disable": "Отключить", 34 | "sign-in": "Авторизовать", 35 | "sign-out": "Деавторизовать" 36 | }, 37 | "ca": { 38 | "title": "Sincronitza amb iCloud", 39 | "loading": { 40 | "init": "Connectant a iCloud...", 41 | "enable": "Habilitant sincronització d'iCloud...", 42 | "disable": "Deshabilitant sincronització d'iCloud...", 43 | "sign-in": "Iniciant sessió a iCloud...", 44 | "sign-out": "Tancant sessió a iCloud..." 45 | }, 46 | "failed": "Error en connectar a iCloud!", 47 | "details": "Detalls", 48 | "dismiss": "Ignora", 49 | "enable": "Habilita", 50 | "disable": "Deshabilita", 51 | "sign-in": "Inicia sessió", 52 | "sign-out": "Tanca sessió" 53 | } 54 | } 55 | 56 | 57 | 105 | 106 | 176 | -------------------------------------------------------------------------------- /src/utils/cloud-kit-api.js: -------------------------------------------------------------------------------- 1 | import * as qs from 'querystring'; 2 | 3 | import { ENV, LocaleError } from './common'; 4 | 5 | const API_TOKENS = { 6 | 'development': 7 | 'a549ed0b287668fdcef031438d4350e1e96ec12e758499bc1360a03564becaf8', 8 | 'production': 9 | 'cd95e9dcb918b2d45b94a10416eaed02df8727d7b6fdde4669a5fbcacefafe1b', 10 | 'electron': '3d81fb3790c935f8fe396a21d0acf93a2b0b886797abb95813855c3ffc062a59', 11 | }; 12 | 13 | const AUTH_TOKEN_KEY = 'derivepass/cloud-kit/auth-token'; 14 | 15 | export default class CloudKitAPI { 16 | constructor(env = ENV) { 17 | this.env = env; 18 | this.apiToken = API_TOKENS[env]; 19 | this.authToken = localStorage.getItem(AUTH_TOKEN_KEY); 20 | 21 | const apiEnv = env === 'electron' ? 'production' : env; 22 | this.base = 'https://api.apple-cloudkit.com/database/1/' + 23 | `iCloud.com.indutny.DerivePass/${apiEnv}`; 24 | 25 | this.authURL = null; 26 | } 27 | 28 | async request(path, { method = 'GET', query = {}, body } = {}) { 29 | query = Object.assign({}, query, { 30 | ckAPIToken: this.apiToken, 31 | }); 32 | 33 | if (this.authToken) { 34 | query = Object.assign({}, query, { 35 | ckWebAuthToken: this.authToken, 36 | }); 37 | } 38 | 39 | const uri = `${this.base}${path}?${qs.stringify(query)}`; 40 | const res = await fetch(uri, { 41 | method, 42 | headers: body ? { 43 | 'content-type': 'application/json; charset=UTF-8' 44 | } : {}, 45 | body: body ? JSON.stringify(body) : undefined, 46 | }); 47 | 48 | return { ok: res.ok, response: await res.json() }; 49 | } 50 | 51 | async getUser() { 52 | const user = await this.request('/public/users/current'); 53 | if (user.ok) { 54 | return user.response; 55 | } 56 | 57 | this.authURL = user.response.redirectURL; 58 | if (!this.authURL) { 59 | throw new LocaleError('No authorization URL in response', 60 | 'error.cloud-kit.no-auth-url'); 61 | } 62 | 63 | return null; 64 | } 65 | 66 | async signIn() { 67 | if (!this.authURL) { 68 | throw new LocaleError('No authorization URL in response', 69 | 'error.cloud-kit.no-auth-url'); 70 | } 71 | 72 | if (this.env === 'electron') { 73 | return await this.electronSignIn(); 74 | } 75 | 76 | const child = window.open(this.authURL, 77 | 'derivepass.iCloud.Auth', 78 | 'width=500,height=500'); 79 | 80 | if (!child) { 81 | throw new LocaleError('Pop-up blocked, please try again', 82 | 'error.cloud-kit.popup-blocked'); 83 | } 84 | 85 | await new Promise((resolve, reject) => { 86 | let fired = false; 87 | const once = () => { 88 | if (fired) { 89 | return false; 90 | } 91 | fired = true; 92 | window.removeEventListener('message', onMessage); 93 | return true; 94 | }; 95 | 96 | const onMessage = ({ source, data }) => { 97 | if (source !== child) { 98 | return; 99 | } 100 | 101 | if (!once()) { 102 | return; 103 | } 104 | 105 | if (data.errorMessage) { 106 | return reject(new Error(data.errorMessage)); 107 | } 108 | 109 | const authToken = data.ckWebAuthToken || data.ckSession; 110 | if (!authToken) { 111 | return reject(new LocaleError('Authentication failure', 112 | 'error.cloud-kit.auth-failure')); 113 | } 114 | 115 | this.setAuthToken(authToken); 116 | resolve(); 117 | }; 118 | window.addEventListener('message', onMessage); 119 | 120 | child.addEventListener('beforeunload', () => { 121 | if (!once()) { 122 | return; 123 | } 124 | 125 | reject(new LocaleError('Sign-in window was prematurely closed', 126 | 'error.cloud-kit.premature-close')); 127 | }); 128 | }); 129 | } 130 | 131 | async electronSignIn() { 132 | const response = await window.electron.iCloudAuth(this.authURL); 133 | if (response.canceled) { 134 | throw new LocaleError('Sign-in window was prematurely closed', 135 | 'error.cloud-kit.premature-close'); 136 | } 137 | 138 | if (response.errorMessage) { 139 | throw new Error(response.errorMessage); 140 | } 141 | 142 | const authToken = response.ckWebAuthToken || response.ckSession; 143 | if (!authToken) { 144 | throw new LocaleError('Authentication failure', 145 | 'error.cloud-kit.auth-failure'); 146 | } 147 | 148 | this.setAuthToken(authToken); 149 | } 150 | 151 | async signOut() { 152 | this.authToken = undefined; 153 | localStorage.removeItem(AUTH_TOKEN_KEY); 154 | 155 | // Update auth url 156 | await this.getUser(); 157 | } 158 | 159 | async fetchRecords({ db = 'private', recordType, continuationMarker }) { 160 | const res = await this.request(`/${db}/records/query`, { 161 | method: 'POST', 162 | body: { 163 | query: { 164 | recordType, 165 | }, 166 | continuationMarker, 167 | } 168 | }); 169 | 170 | if (!res.ok) { 171 | throw new Error(res.response.reason); 172 | } 173 | 174 | return res.response; 175 | } 176 | 177 | async saveRecords(records, { db = 'private', atomic = false } = {}) { 178 | const res = await this.request(`/${db}/records/modify`, { 179 | method: 'POST', 180 | body: { 181 | operations: records.map((record) => { 182 | return { 183 | operationType: record.recordChangeTag ? 'update' : 'create', 184 | record, 185 | }; 186 | }), 187 | atomic, 188 | }, 189 | }); 190 | 191 | if (!res.ok) { 192 | throw new Error(res.response.reason); 193 | } 194 | 195 | return res.response; 196 | } 197 | 198 | // Private 199 | setAuthToken(token) { 200 | this.authToken = token; 201 | localStorage.setItem(AUTH_TOKEN_KEY, token); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/components/nav-bar.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "applications": "Applications", 5 | "settings": "Settings", 6 | "about": "About", 7 | "logout": "Logout", 8 | "master": "Master Password", 9 | "home": "Home Page", 10 | "logo": "Logotype", 11 | "extra": "Extra", 12 | "suggest": "Suggest feature", 13 | "bug": "Report bug" 14 | }, 15 | "ru": { 16 | "applications": "Приложения", 17 | "settings": "Настройки", 18 | "about": "О сайте", 19 | "logout": "Выйти", 20 | "master": "Мастер Пароль", 21 | "home": "Главная Страница", 22 | "logo": "Логотип", 23 | "extra": "Дополнительно", 24 | "suggest": "Предложить идею", 25 | "bug": "Сообщить об ошибке" 26 | }, 27 | "ca": { 28 | "applications": "Aplicacions", 29 | "settings": "Configuració", 30 | "about": "Quant a", 31 | "logout": "Tanca sessió", 32 | "master": "Contrasenya Mestre", 33 | "home": "Pàgina Inicial", 34 | "logo": "Logo", 35 | "extra": "Extra", 36 | "suggest": "Suggerir funcionalitat", 37 | "bug": "Reportar incidència" 38 | } 39 | } 40 | 41 | 42 | 113 | 114 | 179 | 180 | 197 | -------------------------------------------------------------------------------- /src/utils/sync/cloud-kit.js: -------------------------------------------------------------------------------- 1 | import Sync from './base'; 2 | import * as createDebug from 'debug'; 3 | import CloudKitAPI from '../cloud-kit-api'; 4 | 5 | const debug = createDebug('derivepass:sync:cloud-kit'); 6 | 7 | const ENABLE_KEY = 'derivepass/config/enable-icloud'; 8 | 9 | // TODO(indutny): make this configurable 10 | const SYNC_EVERY = 60 * 60 * 1000; // 1 hour 11 | 12 | export default class CloudKit extends Sync { 13 | constructor(store) { 14 | super(store); 15 | 16 | this.initPromise = null; 17 | 18 | this.container = null; 19 | this.db = new CloudKitAPI(); 20 | this.user = null; 21 | this.syncTimer = null; 22 | 23 | // Map from app uuid to recordChangeTag 24 | this.changeTags = new Map(); 25 | 26 | // Automatically initialize when enabled 27 | if (this.isEnabled) { 28 | this.init(); 29 | } 30 | } 31 | 32 | get isEnabled() { 33 | return localStorage.getItem(ENABLE_KEY) === 'true'; 34 | } 35 | 36 | async enable() { 37 | localStorage.setItem(ENABLE_KEY, true); 38 | 39 | // Load scripts if needed 40 | await this.init(); 41 | } 42 | 43 | async disable() { 44 | localStorage.setItem(ENABLE_KEY, false); 45 | 46 | if (this.user) { 47 | await this.signOut(); 48 | } 49 | } 50 | 51 | async init() { 52 | // Do not load scripts until enabled 53 | if (!this.isEnabled) { 54 | return; 55 | } 56 | 57 | if (!this.initPromise) { 58 | this.initPromise = this.initOnce(); 59 | } 60 | 61 | return await this.initPromise; 62 | } 63 | 64 | async initOnce() { 65 | debug('setting up authentication'); 66 | this.setUser(await this.db.getUser()); 67 | 68 | debug('CloudKit fully operational'); 69 | } 70 | 71 | get isAuthenticated() { 72 | return !!this.user; 73 | } 74 | 75 | async signIn() { 76 | await this.db.signIn(); 77 | this.setUser(await this.db.getUser()); 78 | } 79 | 80 | async signOut() { 81 | await this.db.signOut(); 82 | this.setUser(null); 83 | } 84 | 85 | // Override 86 | 87 | async sendApps(uuids) { 88 | if (!this.user) { 89 | return; 90 | } 91 | debug('sending apps uuids.len=%d', uuids.length); 92 | 93 | // Always fetch fresh apps from storage 94 | const apps = this.getApps(uuids); 95 | 96 | const records = apps.map((app) => this.appToRecord(app)); 97 | 98 | const res = await this.db.saveRecords(records); 99 | 100 | // Update tag and modification dates 101 | for (const record of (res.records || [])) { 102 | this.receiveRecord(record); 103 | } 104 | 105 | if (!res.hasErrors) { 106 | debug('successfully sent apps'); 107 | return; 108 | } 109 | 110 | const hasConflicts = 111 | res.errors.some((err) => err.ckErrorCode === 'CONFLICT'); 112 | if (hasConflicts) { 113 | debug('has conflicts, forcing full synchronization'); 114 | this.sync(); 115 | } 116 | 117 | const uuidsLeft = res.errors.map((err) => err.recordName); 118 | debug('apps with errors uuidsLeft.len=%d', uuidsLeft.length); 119 | await this.sendApps(uuidsLeft); 120 | } 121 | 122 | // Internal 123 | 124 | async setUser(user) { 125 | if (this.user === user) { 126 | return; 127 | } 128 | 129 | this.user = user; 130 | if (this.user) { 131 | debug('logged in, syncing'); 132 | await this.sync(); 133 | 134 | debug('subscribing'); 135 | this.subscribe(); 136 | } else { 137 | this.pauseSync(); 138 | 139 | debug('unsubscribing'); 140 | this.unsubscribe(); 141 | } 142 | } 143 | 144 | async sync() { 145 | // Cancel any pending sync cal 146 | this.pauseSync(); 147 | 148 | debug('sync: attempting sync'); 149 | try { 150 | let marker = null; 151 | let received = 0; 152 | 153 | do { 154 | debug('sync: query with marker %j', marker); 155 | const res = await this.db.fetchRecords({ 156 | recordType: 'EncryptedApplication', 157 | continuationMarker: marker, 158 | }); 159 | 160 | if (res.hasErrors) { 161 | debug('sync: got errors', res.errors); 162 | break; 163 | } 164 | 165 | for (const record of res.records) { 166 | this.receiveRecord(record); 167 | received++; 168 | } 169 | 170 | marker = res.continuationMarker; 171 | } while (marker); 172 | 173 | debug('sync: received %d apps', received); 174 | } catch(e) { 175 | debug('sync: error', e); 176 | } 177 | 178 | // Concurrent `sync()` calls 179 | if (this.syncTimer) { 180 | return; 181 | } 182 | this.syncTimer = setTimeout(() => this.sync(), SYNC_EVERY); 183 | } 184 | 185 | pauseSync() { 186 | if (this.syncTimer) { 187 | debug('sync: pause'); 188 | clearTimeout(this.syncTimer); 189 | } 190 | this.syncTimer = null; 191 | } 192 | 193 | receiveRecord(record) { 194 | const fields = record.fields; 195 | 196 | const app = { 197 | uuid: record.recordName, 198 | 199 | domain: fields.domain.value, 200 | login: fields.login.value, 201 | revision: fields.revision.value, 202 | options: fields.options && fields.options.value, 203 | 204 | master: fields.master.value, 205 | index: fields.index.value, 206 | removed: fields.removed.value ? true : false, 207 | changedAt: record.modified.timestamp, 208 | }; 209 | 210 | if (record.recordChangeTag) { 211 | this.changeTags.set(app.uuid, record.recordChangeTag); 212 | } 213 | 214 | this.receiveApp(app); 215 | } 216 | 217 | appToRecord(app) { 218 | const recordChangeTag = this.changeTags.get(app.uuid); 219 | 220 | const wrap = (value) => ({ value }); 221 | 222 | return { 223 | recordType: 'EncryptedApplication', 224 | recordName: app.uuid, 225 | recordChangeTag, 226 | fields: { 227 | domain: wrap(app.domain), 228 | login: wrap(app.login), 229 | revision: wrap(app.revision), 230 | options: wrap(app.options), 231 | master: wrap(app.master), 232 | index: wrap(app.index), 233 | removed: wrap(app.removed ? 1 : 0), 234 | } 235 | }; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/store/derivepass/wasm/index.js: -------------------------------------------------------------------------------- 1 | import wasmURI from 'url-loader?mimetype=application/wasm!./binding.wasm.bin'; 2 | 3 | let wasm; 4 | 5 | // NOTE: This file is... almost generated by `wasm-pack` 6 | 7 | let cachegetUint8Memory = null; 8 | function getUint8Memory() { 9 | if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) { 10 | cachegetUint8Memory = new Uint8Array(wasm.memory.buffer); 11 | } 12 | return cachegetUint8Memory; 13 | } 14 | 15 | let WASM_VECTOR_LEN = 0; 16 | 17 | function passArray8ToWasm(arg) { 18 | const ptr = wasm.__wbindgen_malloc(arg.length * 1); 19 | getUint8Memory().set(arg, ptr / 1); 20 | WASM_VECTOR_LEN = arg.length; 21 | return ptr; 22 | } 23 | 24 | function getArrayU8FromWasm(ptr, len) { 25 | return getUint8Memory().subarray(ptr / 1, ptr / 1 + len); 26 | } 27 | 28 | let cachedGlobalArgumentPtr = null; 29 | function globalArgumentPtr() { 30 | if (cachedGlobalArgumentPtr === null) { 31 | cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr(); 32 | } 33 | return cachedGlobalArgumentPtr; 34 | } 35 | 36 | let cachegetUint32Memory = null; 37 | function getUint32Memory() { 38 | if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) { 39 | cachegetUint32Memory = new Uint32Array(wasm.memory.buffer); 40 | } 41 | return cachegetUint32Memory; 42 | } 43 | 44 | export default class Binding { 45 | constructor() { 46 | } 47 | 48 | async init() { 49 | if (wasm) { 50 | return; 51 | } 52 | 53 | const res = await fetch(wasmURI); 54 | if (!res.ok) { 55 | throw new Error('Failed to fetch wasm blob'); 56 | } 57 | const ab = await res.arrayBuffer(); 58 | const resource = await WebAssembly.instantiate(ab); 59 | wasm = resource.instance.exports; 60 | } 61 | 62 | /** 63 | * @param {number} arg0 64 | * @param {number} arg1 65 | * @param {number} arg2 66 | * @param {Uint8Array} arg3 67 | * @param {Uint8Array} arg4 68 | * @param {number} arg5 69 | * @returns {Uint8Array} 70 | */ 71 | derive(arg0, arg1, arg2, arg3, arg4, arg5) { 72 | const ptr3 = passArray8ToWasm(arg3); 73 | const len3 = WASM_VECTOR_LEN; 74 | const ptr4 = passArray8ToWasm(arg4); 75 | const len4 = WASM_VECTOR_LEN; 76 | const retptr = globalArgumentPtr(); 77 | try { 78 | wasm.derive(retptr, arg0, arg1, arg2, ptr3, len3, ptr4, len4, arg5); 79 | const mem = getUint32Memory(); 80 | const rustptr = mem[retptr / 4]; 81 | const rustlen = mem[retptr / 4 + 1]; 82 | 83 | const realRet = getArrayU8FromWasm(rustptr, rustlen).slice(); 84 | wasm.__wbindgen_free(rustptr, rustlen * 1); 85 | return realRet; 86 | 87 | 88 | } finally { 89 | wasm.__wbindgen_free(ptr3, len3 * 1); 90 | wasm.__wbindgen_free(ptr4, len4 * 1); 91 | 92 | } 93 | 94 | } 95 | 96 | /** 97 | * @param {Uint8Array} arg0 98 | * @param {Uint8Array} arg1 99 | * @param {Uint8Array} arg2 100 | * @param {Uint8Array} arg3 101 | * @returns {Uint8Array} 102 | */ 103 | encrypt(arg0, arg1, arg2, arg3) { 104 | const ptr0 = passArray8ToWasm(arg0); 105 | const len0 = WASM_VECTOR_LEN; 106 | const ptr1 = passArray8ToWasm(arg1); 107 | const len1 = WASM_VECTOR_LEN; 108 | const ptr2 = passArray8ToWasm(arg2); 109 | const len2 = WASM_VECTOR_LEN; 110 | const ptr3 = passArray8ToWasm(arg3); 111 | const len3 = WASM_VECTOR_LEN; 112 | const retptr = globalArgumentPtr(); 113 | try { 114 | wasm.encrypt(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3); 115 | const mem = getUint32Memory(); 116 | const rustptr = mem[retptr / 4]; 117 | const rustlen = mem[retptr / 4 + 1]; 118 | 119 | const realRet = getArrayU8FromWasm(rustptr, rustlen).slice(); 120 | wasm.__wbindgen_free(rustptr, rustlen * 1); 121 | return realRet; 122 | 123 | 124 | } finally { 125 | wasm.__wbindgen_free(ptr0, len0 * 1); 126 | wasm.__wbindgen_free(ptr1, len1 * 1); 127 | wasm.__wbindgen_free(ptr2, len2 * 1); 128 | wasm.__wbindgen_free(ptr3, len3 * 1); 129 | 130 | } 131 | 132 | } 133 | 134 | /** 135 | * @param {Uint8Array} arg0 136 | * @param {Uint8Array} arg1 137 | * @param {Uint8Array} arg2 138 | * @returns {Uint8Array} 139 | */ 140 | decrypt(arg0, arg1, arg2) { 141 | const ptr0 = passArray8ToWasm(arg0); 142 | const len0 = WASM_VECTOR_LEN; 143 | const ptr1 = passArray8ToWasm(arg1); 144 | const len1 = WASM_VECTOR_LEN; 145 | const ptr2 = passArray8ToWasm(arg2); 146 | const len2 = WASM_VECTOR_LEN; 147 | const retptr = globalArgumentPtr(); 148 | try { 149 | wasm.decrypt(retptr, ptr0, len0, ptr1, len1, ptr2, len2); 150 | const mem = getUint32Memory(); 151 | const rustptr = mem[retptr / 4]; 152 | const rustlen = mem[retptr / 4 + 1]; 153 | if (rustptr === 0) return; 154 | 155 | const realRet = getArrayU8FromWasm(rustptr, rustlen).slice(); 156 | wasm.__wbindgen_free(rustptr, rustlen * 1); 157 | return realRet; 158 | 159 | 160 | } finally { 161 | wasm.__wbindgen_free(ptr0, len0 * 1); 162 | wasm.__wbindgen_free(ptr1, len1 * 1); 163 | wasm.__wbindgen_free(ptr2, len2 * 1); 164 | 165 | } 166 | 167 | } 168 | 169 | /** 170 | * @param {Uint8Array} arg0 171 | * @param {Uint8Array} arg1 172 | * @returns {Uint8Array} 173 | */ 174 | decrypt_legacy(arg0, arg1) { 175 | const ptr0 = passArray8ToWasm(arg0); 176 | const len0 = WASM_VECTOR_LEN; 177 | const ptr1 = passArray8ToWasm(arg1); 178 | const len1 = WASM_VECTOR_LEN; 179 | const retptr = globalArgumentPtr(); 180 | try { 181 | wasm.decrypt_legacy(retptr, ptr0, len0, ptr1, len1); 182 | const mem = getUint32Memory(); 183 | const rustptr = mem[retptr / 4]; 184 | const rustlen = mem[retptr / 4 + 1]; 185 | if (rustptr === 0) return; 186 | 187 | const realRet = getArrayU8FromWasm(rustptr, rustlen).slice(); 188 | wasm.__wbindgen_free(rustptr, rustlen * 1); 189 | return realRet; 190 | 191 | 192 | } finally { 193 | wasm.__wbindgen_free(ptr0, len0 * 1); 194 | wasm.__wbindgen_free(ptr1, len1 * 1); 195 | 196 | } 197 | 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/store/derivepass/index.js: -------------------------------------------------------------------------------- 1 | // TODO(indutny): webpack.config.js 2 | // Blocked by: https://github.com/vuejs/vue-cli/issues/3192 3 | import Worker from 'worker-loader?{"name":"js/worker.[hash:8].js"}!./derive.worker.js'; 4 | import * as createDebug from 'debug'; 5 | 6 | import { 7 | AES_KEY_SIZE, MAC_KEY_SIZE, IV_SIZE, 8 | toHex, fromHex, 9 | LEGACY_PASSWORD_SIZE, 10 | passwordEntropyBits, 11 | computeLegacyPassword, 12 | computePassword, 13 | } from '../../utils/crypto'; 14 | 15 | const debug = createDebug('derivepass:plugins:derivepass'); 16 | const encoder = new TextEncoder('utf-8'); 17 | const decoder = new TextDecoder('utf-8'); 18 | 19 | const SCRYPT_AES_DOMAIN = 'derivepass/aes'; 20 | const MAX_WORKERS = 8; 21 | 22 | class DeriveWorker { 23 | constructor() { 24 | this.handle = null; 25 | this.queue = []; 26 | } 27 | 28 | async init() { 29 | // Already initialized 30 | if (this.handle) { 31 | return; 32 | } 33 | 34 | this.handle = await new Promise((resolve, reject) => { 35 | debug('creating worker'); 36 | const worker = new Worker(); 37 | 38 | debug('awaiting worker ready message'); 39 | worker.onmessage = (e) => { 40 | const { type, payload } = e.data; 41 | if (type === 'ready') { 42 | debug('worker ready'); 43 | resolve(worker); 44 | } else if (type === 'error') { 45 | debug('worker error', payload); 46 | reject(new Error(payload)); 47 | } else { 48 | throw new Error(`Unknown message type: "${type}"`); 49 | } 50 | }; 51 | 52 | worker.onerror = (e) => { 53 | reject(e); 54 | }; 55 | }); 56 | 57 | this.handle.onmessage = (e) => { 58 | const { type, payload } = e.data; 59 | if (this.queue.length === 0) { 60 | throw new Error(`Unexpected message with type: "${type}"`); 61 | } 62 | debug('worker message type=%j', type, payload); 63 | 64 | const first = this.queue.shift(); 65 | if (first.type !== type) { 66 | throw new Error(`Unexpected message with type: "${type}"`); 67 | } 68 | 69 | first.resolve(payload); 70 | }; 71 | } 72 | 73 | async send(type, payload) { 74 | return await new Promise((resolve) => { 75 | this.handle.postMessage({ type, payload }); 76 | 77 | this.queue.push({ type, resolve }); 78 | }); 79 | } 80 | } 81 | 82 | export default class DerivePass { 83 | constructor() { 84 | this.workers = { 85 | idle: [], 86 | active: new Set(), 87 | queue: [], 88 | count: 0, 89 | }; 90 | } 91 | 92 | async getWorker() { 93 | let worker; 94 | if (this.workers.idle.length !== 0) { 95 | debug('using idle worker'); 96 | worker = this.workers.idle.shift(); 97 | } else if (this.workers.count >= MAX_WORKERS) { 98 | debug('waiting for next idle worker'); 99 | worker = await new Promise((resolve) => { 100 | this.workers.queue.push(resolve); 101 | }); 102 | } else { 103 | debug('spawning new worker'); 104 | worker = new DeriveWorker(); 105 | this.workers.count++; 106 | await worker.init(); 107 | } 108 | this.workers.active.add(worker); 109 | return worker; 110 | } 111 | 112 | reclaimWorker(worker) { 113 | debug('reclaiming worker'); 114 | this.workers.active.delete(worker); 115 | if (this.workers.queue.length !== 0) { 116 | this.workers.queue.shift()(worker); 117 | } else { 118 | this.workers.idle.push(worker); 119 | } 120 | } 121 | 122 | async scrypt(master, domain, outSize) { 123 | const worker = await this.getWorker(); 124 | 125 | master = encoder.encode(master); 126 | domain = encoder.encode(domain); 127 | 128 | try { 129 | return await worker.send('derivepass', { master, domain, outSize }); 130 | } finally { 131 | this.reclaimWorker(worker); 132 | } 133 | } 134 | 135 | async encrypt(payload, keys) { 136 | const worker = await this.getWorker(); 137 | try { 138 | const iv = new Uint8Array(IV_SIZE); 139 | window.crypto.getRandomValues(iv); 140 | 141 | const data = encoder.encode(payload); 142 | const raw = await worker.send('encrypt', { keys, iv, data }); 143 | const hex = toHex(raw); 144 | 145 | return 'v1:' + hex; 146 | } finally { 147 | this.reclaimWorker(worker); 148 | } 149 | } 150 | 151 | async decrypt(payload, keys) { 152 | const worker = await this.getWorker(); 153 | try { 154 | let version = 0; 155 | if (/^v1:/.test(payload)) { 156 | version = 1; 157 | payload = payload.slice(3); 158 | } 159 | payload = fromHex(payload); 160 | const raw = await worker.send( 161 | version === 1 ? 'decrypt' : 'decrypt_legacy', 162 | { keys, data: payload }); 163 | if (!raw) { 164 | return undefined; 165 | } 166 | return decoder.decode(raw); 167 | } finally { 168 | this.reclaimWorker(worker); 169 | } 170 | } 171 | 172 | // NOTE: We're intentionally not using `Promise.all()` here and below for 173 | // better interactivity. Most users will have several apps and it will look 174 | // better if they'll load in chunks. 175 | async decryptApp(app, keys) { 176 | const raw = { 177 | domain: await this.decrypt(app.domain, keys), 178 | login: await this.decrypt(app.login, keys), 179 | revision: await this.decrypt(app.revision, keys), 180 | options: app.options && await this.decrypt(app.options, keys), 181 | }; 182 | if (raw.domain === undefined || 183 | raw.login === undefined || 184 | raw.revision === undefined || 185 | (app.options && raw.options === undefined)) { 186 | return null; 187 | } 188 | 189 | return Object.assign({}, app, { 190 | domain: raw.domain, 191 | login: raw.login, 192 | revision: parseInt(raw.revision, 10) || 1, 193 | options: raw.options && JSON.parse(raw.options), 194 | }); 195 | } 196 | 197 | async encryptApp(app, keys) { 198 | return Object.assign({}, app, { 199 | domain: await this.encrypt(app.domain, keys), 200 | login: await this.encrypt(app.login, keys), 201 | revision: await this.encrypt(app.revision.toString(), keys), 202 | options: await this.encrypt(JSON.stringify(app.options), keys), 203 | }); 204 | } 205 | 206 | async computeKeys(master) { 207 | const buf = await this.scrypt(master, SCRYPT_AES_DOMAIN, 208 | AES_KEY_SIZE + MAC_KEY_SIZE); 209 | 210 | return { 211 | aesKey: buf.slice(0, AES_KEY_SIZE), 212 | macKey: buf.slice(AES_KEY_SIZE), 213 | }; 214 | } 215 | 216 | async computeLegacyPassword(master, domain) { 217 | const raw = await this.scrypt(master, domain, LEGACY_PASSWORD_SIZE); 218 | 219 | return computeLegacyPassword(raw); 220 | } 221 | 222 | async computePassword(master, domain, options) { 223 | const bytes = Math.ceil(passwordEntropyBits(options) / 8); 224 | const raw = await this.scrypt(master, domain, bytes); 225 | return computePassword(raw, options); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/components/qr-receive.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "error": "Error", 5 | "complete": "Synchronization complete", 6 | "init-video": "Initializing video...", 7 | "not-supported": "Video recording is not supported by the browser" 8 | }, 9 | "ru": { 10 | "error": "Ошибка", 11 | "complete": "Синхронизация завершена", 12 | "init-video": "Инициализируем видео...", 13 | "not-supported": "Видео запись не поддерживается браузером" 14 | }, 15 | "ca": { 16 | "error": "Error", 17 | "complete": "Sincronització completada", 18 | "init-video": "Iniciant vídeo...", 19 | "not-supported": "El navegador no suporta enregistrament de vídeo" 20 | } 21 | } 22 | 23 | 24 | 51 | 52 | 273 | -------------------------------------------------------------------------------- /src/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate dumb_crypto; 2 | extern crate wasm_bindgen; 3 | 4 | use cfg_if::cfg_if; 5 | use dumb_crypto::{aes, aes_cbc, hmac, scrypt, sha256}; 6 | use wasm_bindgen::prelude::*; 7 | 8 | cfg_if! { 9 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 10 | // allocator. 11 | if #[cfg(feature = "wee_alloc")] { 12 | extern crate wee_alloc; 13 | #[global_allocator] 14 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 15 | } 16 | } 17 | 18 | cfg_if! { 19 | // When the `console_error_panic_hook` feature is enabled, we can call the 20 | // `set_panic_hook` function at least once during initialization, and then 21 | // we will get better error messages if our code ever panics. 22 | // 23 | // For more details see 24 | // https://github.com/rustwasm/console_error_panic_hook#readme 25 | if #[cfg(feature = "console_error_panic_hook")] { 26 | extern crate console_error_panic_hook; 27 | pub use self::console_error_panic_hook::set_once as set_panic_hook; 28 | } else { 29 | #[inline] 30 | pub fn set_panic_hook() {} 31 | } 32 | } 33 | 34 | #[wasm_bindgen] 35 | pub fn derive( 36 | r: usize, 37 | n: usize, 38 | p: usize, 39 | passphrase: &[u8], 40 | salt: &[u8], 41 | out_size: usize, 42 | ) -> Vec { 43 | let mut out: Vec = vec![0; out_size]; 44 | 45 | let s = scrypt::Scrypt::new(r, n, p); 46 | 47 | s.derive(passphrase, salt, &mut out) 48 | .expect("scrypt derivation to succeed"); 49 | 50 | out 51 | } 52 | 53 | #[wasm_bindgen] 54 | pub fn encrypt(aes_key: &[u8], mac_key: &[u8], iv: &[u8], payload: &[u8]) -> Vec { 55 | let mut iv_arr = [0; aes::BLOCK_SIZE]; 56 | iv_arr.copy_from_slice(iv); 57 | 58 | let mut cipher = aes_cbc::Cipher::new(iv_arr); 59 | cipher.init(aes_key).expect("cipher.init to succeed"); 60 | 61 | let mut out = iv.to_vec(); 62 | out.append(&mut cipher.write(payload).expect("cipher.write to succeed")); 63 | out.append(&mut cipher.flush().expect("cipher.flush to succed")); 64 | 65 | let mut hash = hmac::HMac::new(mac_key); 66 | hash.update(&out); 67 | out.append(&mut hash.digest().to_vec()); 68 | 69 | out 70 | } 71 | 72 | #[wasm_bindgen] 73 | pub fn decrypt(aes_key: &[u8], mac_key: &[u8], payload: &[u8]) -> Option> { 74 | if payload.len() < aes::BLOCK_SIZE + sha256::DIGEST_SIZE { 75 | panic!("Payload doesn't have enough bytes for MAC"); 76 | } 77 | 78 | let mut iv = [0; aes::BLOCK_SIZE]; 79 | iv.copy_from_slice(&payload[..aes::BLOCK_SIZE]); 80 | 81 | let rest = &payload[aes::BLOCK_SIZE..]; 82 | let mac_off = rest.len() - sha256::DIGEST_SIZE; 83 | 84 | let content = &rest[0..mac_off]; 85 | let mac = &rest[mac_off..]; 86 | 87 | let mut hash = hmac::HMac::new(mac_key); 88 | hash.update(&iv); 89 | hash.update(&content); 90 | let digest = hash.digest().to_vec(); 91 | 92 | let mut is_equal = 0; 93 | for (digest_elem, mac_elem) in digest.iter().zip(mac.iter()) { 94 | is_equal |= digest_elem ^ mac_elem; 95 | } 96 | if is_equal != 0 { 97 | return None 98 | } 99 | 100 | let mut decipher = aes_cbc::Decipher::new(iv); 101 | decipher.init(aes_key).expect("decipher.init to succeed"); 102 | 103 | let mut out = decipher.write(&content).expect("cipher.write to succeed"); 104 | out.append(&mut decipher.flush().expect("cipher.flush to succed")); 105 | 106 | Some(out) 107 | } 108 | 109 | #[wasm_bindgen] 110 | pub fn decrypt_legacy(aes_key: &[u8], payload: &[u8]) -> Option> { 111 | if payload.len() < aes::BLOCK_SIZE { 112 | panic!("Payload doesn't have enough bytes for AES IV"); 113 | } 114 | 115 | let mut iv = [0; aes::BLOCK_SIZE]; 116 | iv.copy_from_slice(&payload[..aes::BLOCK_SIZE]); 117 | 118 | let content = &payload[aes::BLOCK_SIZE..]; 119 | 120 | let mut decipher = aes_cbc::Decipher::new(iv); 121 | decipher.init(aes_key).expect("decipher.init to succeed"); 122 | 123 | let mut out = decipher.write(content).expect("cipher.write to succeed"); 124 | out.append(&mut decipher.flush().expect("cipher.flush to succed")); 125 | 126 | Some(out) 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | 133 | #[test] 134 | fn it_should_decrypt_legacy() { 135 | let aes_key = [ 136 | 0xe3, 0x3b, 0x22, 0x21, 0xd5, 0x1d, 0xe5, 0xb5, 0x92, 0x17, 0xd9, 0xea, 0x05, 0x83, 137 | 0x25, 0xa5, 0x1d, 0x3b, 0x32, 0x93, 0x06, 0xcd, 0x1c, 0x98, 0x61, 0xaa, 0x5e, 0x17, 138 | 0xee, 0xef, 0x16, 0x71, 139 | ]; 140 | let old = [ 141 | 0x7b, 0xc8, 0x5a, 0x06, 0xf6, 0xcb, 0xc3, 0x15, 0xe2, 0x76, 0x96, 0xc4, 0xe6, 0x48, 142 | 0xc4, 0x6e, 0x21, 0x7c, 0x12, 0x94, 0x62, 0x99, 0x52, 0x25, 0x83, 0x77, 0x39, 0x07, 143 | 0xc6, 0xbf, 0x32, 0xb4, 144 | ]; 145 | 146 | assert_eq!(decrypt_legacy(&aes_key, &old).unwrap(), vec![0x6f, 0x68, 0x61, 0x69]); 147 | } 148 | 149 | #[test] 150 | fn it_should_decrypt_modern() { 151 | let payload = [ 152 | 0x43, 0x3b, 0x62, 0x41, 0xc2, 0x9f, 0x03, 0x20, 0x31, 0x47, 0x7f, 0xa3, 0xab, 0xc7, 153 | 0x19, 0xe0, 0x91, 0x4d, 0x87, 0x70, 0x7a, 0x79, 0x63, 0x82, 0x79, 0xb5, 0x32, 0x55, 154 | 0xb1, 0xbc, 0xa8, 0xa3, 0x0e, 0x54, 0x2d, 0x77, 0xab, 0x59, 0x46, 0xc7, 0xcd, 0x4f, 155 | 0x59, 0xcd, 0xd5, 0x4f, 0x13, 0xe1, 0x18, 0x63, 0x95, 0x35, 0x25, 0x86, 0x96, 0x48, 156 | 0xf0, 0x3a, 0x71, 0x1e, 0x55, 0xd5, 0x73, 0xbf, 157 | ]; 158 | 159 | let aes_key = [ 160 | 0x16, 0xf1, 0x9d, 0x4f, 0x9f, 0x18, 0x6f, 0x89, 0x1d, 0x8b, 0xb1, 0x1f, 0x2b, 0x84, 161 | 0xfe, 0x8b, 0x39, 0xcf, 0xf8, 0x10, 0x82, 0x1c, 0xf3, 0x8a, 0x3d, 0x8a, 0xd2, 0x2c, 162 | 0x1e, 0x08, 0x5f, 0x4b, 163 | ]; 164 | 165 | let mac_key = [ 166 | 0xfc, 0xf9, 0xd0, 0xb8, 0x71, 0x88, 0x03, 0x8b, 0xa8, 0x0e, 0xa7, 0x04, 0x6e, 0x94, 167 | 0x89, 0x11, 0xdf, 0xdd, 0x58, 0x13, 0x6c, 0xea, 0x13, 0x22, 0xeb, 0xe4, 0x88, 0xbd, 168 | 0xdf, 0xae, 0xb2, 0x8c, 0x5f, 0x8e, 0x89, 0x5a, 0x36, 0x2a, 0xcc, 0x43, 0xaa, 0x13, 169 | 0x14, 0x47, 0xdd, 0x51, 0xa7, 0x41, 0x41, 0x7e, 0x2c, 0x43, 0x38, 0x8f, 0x01, 0x8e, 170 | 0xa3, 0xdf, 0xbe, 0xf3, 0xe7, 0xa8, 0xe1, 0x6e, 171 | ]; 172 | 173 | assert_eq!(decrypt(&aes_key, &mac_key, &payload).unwrap(), vec![ 174 | 0x6f, 0x6d, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 175 | ]); 176 | } 177 | 178 | #[test] 179 | fn it_should_encrypt_decrypt_modern() { 180 | let aes_key = [ 181 | 0xe3, 0x3b, 0x22, 0x21, 0xd5, 0x1d, 0xe5, 0xb5, 0x92, 0x17, 0xd9, 0xea, 0x05, 0x83, 182 | 0x25, 0xa5, 0x1d, 0x3b, 0x32, 0x93, 0x06, 0xcd, 0x1c, 0x98, 0x61, 0xaa, 0x5e, 0x17, 183 | 0xee, 0xef, 0x16, 0x71, 184 | ]; 185 | let mac_key = [ 186 | 0x8a, 0x41, 0x93, 0x94, 0x8b, 0xcd, 0x65, 0x34, 0x76, 0xba, 0x6e, 0xc4, 0x1b, 0x28, 187 | 0x02, 0xed, 0x41, 0xd4, 0x3e, 0x03, 0x2f, 0x87, 0x90, 0x9a, 0xf0, 0xc4, 0x3e, 0x0c, 188 | 0x2d, 0x25, 0xaa, 0x83, 0x1c, 0xb2, 0x1a, 0xe0, 0x82, 0x54, 0xf3, 0x09, 0x4c, 0x81, 189 | 0xe1, 0xe2, 0x57, 0xf5, 0x26, 0xf8, 0xed, 0xbb, 0xdb, 0x60, 0x99, 0xcf, 0xb0, 0xa0, 190 | 0xc5, 0x55, 0x6c, 0x0b, 0x22, 0x8a, 0x96, 0xf2, 191 | ]; 192 | let iv = [ 193 | 0xe3, 0x3b, 0x22, 0x21, 0xd5, 0x1d, 0xe5, 0xb5, 0x92, 0x17, 0xd9, 0xea, 0x05, 0x83, 194 | 0x25, 0xa5, 195 | ]; 196 | 197 | let payload = vec![0x6f, 0x68, 0x61, 0x69]; 198 | let encrypted = encrypt(&aes_key, &mac_key, &iv, &payload); 199 | 200 | assert_eq!(payload, decrypt(&aes_key, &mac_key, &encrypted).unwrap()); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/components/tutorial.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "master": { 5 | "empty": "Hello!|Let's walk you through the basics|Choose a strong Master Password|Memorize it or write it down...|...and enter it below", 6 | "password": "Excellent choice!|Let's hit \"@:(button.next)\" button{\"delay\":true}", 7 | "confirm": "Still remember your Master Password?|Let's type it one more time to confirm|If something is wrong, you can start from scratch by hitting \"@:(button.reset)\"{\"delay\":true}", 8 | "submit": "Perfect!|Click \"@:(button.start)\" to continue" 9 | }, 10 | "application-list": { 11 | "first": "You don't have any applications yet|Let's fix this by creating one|Click \"@:(button.add-app)\" to continue{\"delay\":true}" 12 | }, 13 | "application": { 14 | "empty": "What the heck is \"Application\"?|Suppose that you'd like to register on a website: fancypillows.com|Naturally, they ask you to provide a password|Instead of choosing one manually, you could use DerivePass{\"silent\":\"glasses\"}Type the name of website in \"@:(label.domain)\" field to continue{\"delay\":true}", 15 | "domain": "Fantastic!{\"silent\":\"thumb\"}Now let's fill \"@:(label.login)\" field{\"delay\":true}", 16 | "username": "Lovely{\"silent\":\"heart\"}Generate the password password by hitting \"@:(button.compute.idle)\"", 17 | "password": "Copy the password by clicking \"@:(button.copy.ready)\"|Saving the application will complete this tutorial{\"delay\":true}" 18 | } 19 | }, 20 | "ru": { 21 | "master": { 22 | "empty": "Здравствуйте!|Давайте вместе разберемся, как пользоваться DerivePass|Выберите надежный Мастер Пароль|Запомните его или запишите...|...и введите его в поле ниже", 23 | "password": "Прекрасный выбор!|Давайте нажмем \"@:(button.next)\"{\"delay\":true}", 24 | "confirm": "Все еще помните ваш Мастер Пароль?|Введите его еще один раз для подтверждения|Если что-то пошло не так, вы всегда можете начать сначала, нажав \"@:(button.reset)\"{\"delay\":true}", 25 | "submit": "Замечательно!|Нажмите \"@:(button.start)\", чтобы продолжить" 26 | }, 27 | "application-list": { 28 | "first": "У вас пока нет приложений|Давайте исправим это, создав одно!|Нажмите \"@:(button.add-app)\" для продолжения{\"delay\":true}" 29 | }, 30 | "application": { 31 | "empty": "Что такое \"Приложение\"?|Допустим, вы хотите зарегистрироваться на веб-сайте: fancypillows.com|Для этого вам, наверняка, будет необходимо выбрать пароль|Вместо того, чтобы выбирать его вручную, вы можете использовать DerivePass{\"silent\":\"glasses\"}Введите название веб-сайта в поле \"@:(label.domain)\", чтобы продолжить{\"delay\":true}", 32 | "domain": "Прекрасно!{\"silent\":\"thumb\"}Теперь давайте заполним поле \"@:(label.login)\"{\"delay\":true}", 33 | "username": "Изумительно!{\"silent\":\"heart\"}Вы можете сгенерировать пароль, нажав \"@:(button.compute.idle)\"", 34 | "password": "Скопируйте пароль, щелкнув по \"@:(button.copy.ready)\"|Сохранение данного приложения завершит наше обучение{\"delay\":true}" 35 | } 36 | }, 37 | "ca": { 38 | "master": { 39 | "empty": "Hola!|Anem a veure els bàsics|Escull una Contrasenya Mestre forta|Memoritza-la, o anota-te-la...|...i introdueix-la a continuació", 40 | "password": "Genial elecció!!|Va, apretem el botó \"@:(button.next)\"{\"delay\":true}", 41 | "confirm": "Encara recordes la teva Contrasenya Mestre?|Va, posa-la un cop més per confirmar-ho|Si res esta malament, pots començar de nou prement el botó \"@:(button.reset)\"{\"delay\":true}", 42 | "submit": "Perfecte!|Prem \"@:(button.start)\" per continuar" 43 | }, 44 | "application-list": { 45 | "first": "Encara no tens cap aplicació|Solucionem-ho afegeint-ne una|Prem \"@:(button.add-app)\" per continuar{\"delay\":true}" 46 | }, 47 | "application": { 48 | "empty": "Què nassos és \"Aplicació\"?|Suposa que vols registrar-te a un lloc web: coixins-guais.cat|Naturalment, et demanaran una contrasenya|Enlloc de triar-ne una manualment, pots emprar DerivePass{\"silent\":\"glasses\"}Escriu el nom del lloc web al camp \"@:(label.domain)\" per continuar{\"delay\":true}", 49 | "domain": "Fantàstic!{\"silent\":\"thumb\"}Ara omple el camp \"@:(label.login)\"{\"delay\":true}", 50 | "username": "Preciós{\"silent\":\"heart\"}Genera la contrasenya prement \"@:(button.compute.idle)\"", 51 | "password": "Copia la contrasenya prement \"@:(button.copy.ready)\"|Desa l'aplicació per completar aquest tutorial{\"delay\":true}" 52 | } 53 | } 54 | } 55 | 56 | 57 | 70 | 71 | 201 | 202 | 208 | -------------------------------------------------------------------------------- /src/pages/master-password.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "master": { 5 | "description": "Used for decrypting storage and computing passwords", 6 | "label": { 7 | "new": "Choose your Master Password", 8 | "existing": "Enter your Master Password" 9 | }, 10 | "feedback": { 11 | "empty": "Master Password can't be empty" 12 | } 13 | }, 14 | "confirm": { 15 | "label": "Confirm your Master Password", 16 | "feedback": { 17 | "almost": "Just a few more characters...", 18 | "no-match": "Password and confirmation should match" 19 | } 20 | }, 21 | "computing": "Computing decryption keys..." 22 | }, 23 | "ru": { 24 | "master": { 25 | "description": "Используется для расшифровки хранилища и генерации паролей", 26 | "label": { 27 | "new": "Выберите ваш Мастер Пароль", 28 | "existing": "Введите ваш Мастер Пароль" 29 | }, 30 | "feedback": { 31 | "empty": "Мастер Пароль не может быть пустым" 32 | } 33 | }, 34 | "confirm": { 35 | "label": "Подтвердите ваш Мастер Пароль", 36 | "feedback": { 37 | "almost": "Еще немного символов...", 38 | "no-match": "Пароль и подтверждение должны быть одинаковыми" 39 | } 40 | }, 41 | "computing": "Генерируем криптографические ключи..." 42 | }, 43 | "ca": { 44 | "master": { 45 | "description": "Emprat per desencriptar l'emmagatzematge i per computar les contrasenyes", 46 | "label": { 47 | "new": "Escull la teva Contrasenya Mestre", 48 | "existing": "Introdueix la teva Contrasenya Mestre" 49 | }, 50 | "feedback": { 51 | "empty": "La Contrasenya Mestre no pot ser buida" 52 | } 53 | }, 54 | "confirm": { 55 | "label": "Confirma la teva Contrasenya Mestre", 56 | "feedback": { 57 | "almost": "Va, uns pocs caràcters més...", 58 | "no-match": "La contrasenya i la confirmació han de coincidir" 59 | } 60 | }, 61 | "computing": "Computant claus de desxifratge..." 62 | } 63 | } 64 | 65 | 66 | 139 | 140 | 331 | -------------------------------------------------------------------------------- /src/pages/home.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "lead": "Compute secure passwords without storing them anywhere.", 5 | "start": "Start", 6 | "description": [ 7 | "Most websites require a password on Sign Up. Using the same password everywhere is insecure. Using individual secure passwords and remembering all of them - impossible!", 8 | "DerivePass is here to help with the task! Choose the Master Password once and use it to generate an unlimited number of secure website passwords.", 9 | "Unlike traditional password managers, DerivePass never uploads either the Master Password or website passwords to the cloud. The only information that is stored is encrypted website domain names and usernames." 10 | ], 11 | "security": { 12 | "title": "Security", 13 | "subtitle": "Technical Details", 14 | "description": [ 15 | "The {scrypt} algorithm is used for deriving application passwords and encryption/authentication keys. Due to the strong cryptographic properties of {scrypt}, the compromise of any single application password does not compromise any other application passwords or the Master password.", 16 | "An application's domain name (website), username, and revision are used for generating unique and secure passwords. In particular, domain/username(#revision)? is used as a {salt} parameter of {scrypt}. Thus every revision increment will result in a completely different generated password. This is convenient for changing the password whenever it is required.", 17 | "Every bit of information that is stored locally and/or remotely (through optional iCloud synchronization) is encrypted with an {AES} key generated from the Master Password via the {scrypt} algorithm, using derivepass/aes as a {salt}. The data is further passed to the {HMAC} algorithm to ensure the data integrity." 18 | ] 19 | }, 20 | "privacy": { 21 | "title": "Privacy", 22 | "description": [ 23 | "Your privacy is important to us. Your data is stored only locally on your computer, by default, unless you manually decide to use remote storage (e.g., iCloud). We never store unencrypted website domain names, usernames, or passwords. No tracking of any kind is used on this website." 24 | ] 25 | }, 26 | "source": "source code" 27 | }, 28 | "ru": { 29 | "lead": "Генерируйте надежные пароли, не сохраняя их копий.", 30 | "start": "Начать", 31 | "description": [ 32 | "Большинство веб-сайтов требуют пароль во время регистрации. Использование одинакового пароля для всех сайтов - небезопасно. Использование разных надежных паролей и запоминание их всех - невозможно!", 33 | "DerivePass готов помочь в решении этой задачи! Выберите Мастер Пароль один раз и используйте его для создания бесконечного числа надежных паролей для веб-сайтов.", 34 | "В отличии от традиционных менеджеров паролей, DerivePass не загружает в облако ни Мастер Пароль, ни пароли от веб-сайтов. Единственная сохраняемая информация - это зашифрованные адреса веб-сайтов и имена пользователей." 35 | ], 36 | "security": { 37 | "title": "Безопасность", 38 | "subtitle": "Технические детали", 39 | "description": [ 40 | "Алгоритм {scrypt} используется для вычисления паролей приложений и ключей шифрования/аутентификации. Благодаря криптографическим качествам {scrypt}, потеря одного пароля для приложения не выдаст злоумышленникам ваш Мастер Пароль и не позволит им вычислить прочие пароли от ваших приложений.", 41 | "Доменное имя приложения (название веб-сайта), имя пользователя и \"ревизия\" используются для генерации уникального и надежного пароля. В частности, domain/username(#revision)? используется в качестве \"соли\" для {scrypt}. Таким образом, увеличение \"ревизии\" приведет к изменению сгенерированного пароля. Это удобно в ситуациях, когда необходима смена пароля.", 42 | "Вся информация, которая хранится на вашем компьютере и/или удаленно (через необязательную синхронизацию с iCloud), зашифрована {AES} ключом, сгенерированным из Мастер Пароля с помощью алгоритма {scrypt}, используя derivepass/aes в качестве \"соли\". Данные, в последствии, получают {HMAC} подпись, с целью подтверждения их подлинности." 43 | ] 44 | }, 45 | "privacy": { 46 | "title": "Конфиденциальность", 47 | "description": [ 48 | "Ваша конфиденциальность - наш приоритет. Вся информация хранится на вашем компьютере, если вы не используете, отключенное по умолчанию, удаленное хранилище (например, iCloud). Мы не храним незашифрованные адреса веб-сайтов, имена пользователей или пароли. Трекинг не используется на этом веб-сайте." 49 | ] 50 | }, 51 | "source": "исходный код" 52 | }, 53 | "ca": { 54 | "lead": "Computa contrasenyes mestres sense desar-les enlloc.", 55 | "start": "Comença", 56 | "description": [ 57 | "La majoria de websites requereixen una contrasenya per regitrar-se. Emprar la mateixa contrasenya a tot arreu és insegur. Emprar contrasenyes individuals segures i recordar-les totes - impossible!", 58 | "DerivePass és aquí per ajudar en aquesta tasca! Escull la Contrasenya Mestre un sol cop i utilitza-la per generar un nombre il·limitat de contrasenyes segures.", 59 | "A diferència dels gestors tradicionals de contrasenyes, DerivePass mai puja ni la Contrasenya Mestre ni cap de les altres contrasenyes al núvol. La única informació que es desa encriptada són noms de dominis i d'usuaris." 60 | ], 61 | "security": { 62 | "title": "Seguretat", 63 | "subtitle": "Detalls Tècnics", 64 | "description": [ 65 | "L'algoritme {scrypt} és emprat per derivar contrasenyes d'aplicacions i claus d'encriptació/desencriptació. Degut a les fortes propietats criptogràfiques de {scrypt}, el compromís d'una sola aplicació no comprometrà la Contrasenya Mestre, així com tampoc comprometrà cap altra contrasenya d'altres aplicacions.", 66 | "Els noms de domini (websites), noms d'usuari i revisió són emprats per generar una contrasenya única i segura. En particular, domini/usuari(#revisió)? s'empra com a salt de {scrypt}. Així doncs, cada increment de revisió resultarà en una contrasenya diferent. Això és convenient per a canviar la contrasenya sempre que faci falta.", 67 | "Cada bit d'informació que es desa localment i/o remotament (a través de la sincronització opcional d'iCloud) és xifrat amb una clau {AES} generada a partir de la Contrasenya Mestre amb l'algoritme {scrypt}, emprant derivepass/aes com a salt. Les dades són lavors passades a l'algoritme {HMAC} per assegurar la integritat de les dades." 68 | ] 69 | }, 70 | "privacy": { 71 | "title": "Privacitat", 72 | "description": [ 73 | "La teva privacitat ens importa. Les teves dades es desen localment al teu ordinador, a menys que vulguis fer servir emmagatzematge remot (deshabilitat per defecte). Nosaltres mai emmagatzemem noms de domini, usuaris o contrasenyes sense encriptar. Aquest website no fa seguiments de cap tipus en aquest web." 74 | ] 75 | }, 76 | "source": "codi font" 77 | } 78 | } 79 | 80 | 81 | 144 | 145 | 187 | 188 | 190 | -------------------------------------------------------------------------------- /src/wasm/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "cfg-if" 5 | version = "0.1.6" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "console_error_panic_hook" 10 | version = "0.1.5" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | dependencies = [ 13 | "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "derivepass" 19 | version = "1.0.1" 20 | dependencies = [ 21 | "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 22 | "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 23 | "dumb-crypto 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 24 | "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 25 | "wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "wee_alloc 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 27 | ] 28 | 29 | [[package]] 30 | name = "dumb-crypto" 31 | version = "3.1.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "futures" 36 | version = "0.1.25" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | 39 | [[package]] 40 | name = "js-sys" 41 | version = "0.3.10" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | dependencies = [ 44 | "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 45 | ] 46 | 47 | [[package]] 48 | name = "lazy_static" 49 | version = "1.2.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | 52 | [[package]] 53 | name = "libc" 54 | version = "0.2.48" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | 57 | [[package]] 58 | name = "log" 59 | version = "0.4.6" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | dependencies = [ 62 | "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 63 | ] 64 | 65 | [[package]] 66 | name = "memory_units" 67 | version = "0.4.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | 70 | [[package]] 71 | name = "proc-macro2" 72 | version = "0.4.26" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | dependencies = [ 75 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 76 | ] 77 | 78 | [[package]] 79 | name = "quote" 80 | version = "0.6.11" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | dependencies = [ 83 | "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", 84 | ] 85 | 86 | [[package]] 87 | name = "scoped-tls" 88 | version = "0.1.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | 91 | [[package]] 92 | name = "syn" 93 | version = "0.15.26" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | dependencies = [ 96 | "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", 97 | "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 99 | ] 100 | 101 | [[package]] 102 | name = "unicode-xid" 103 | version = "0.1.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | 106 | [[package]] 107 | name = "unreachable" 108 | version = "1.0.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | dependencies = [ 111 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 112 | ] 113 | 114 | [[package]] 115 | name = "void" 116 | version = "1.0.2" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | 119 | [[package]] 120 | name = "wasm-bindgen" 121 | version = "0.2.33" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | dependencies = [ 124 | "wasm-bindgen-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 125 | ] 126 | 127 | [[package]] 128 | name = "wasm-bindgen-backend" 129 | version = "0.2.33" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | dependencies = [ 132 | "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 133 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 134 | "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", 136 | "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", 137 | "wasm-bindgen-shared 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 138 | ] 139 | 140 | [[package]] 141 | name = "wasm-bindgen-futures" 142 | version = "0.3.10" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | dependencies = [ 145 | "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", 146 | "js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 148 | ] 149 | 150 | [[package]] 151 | name = "wasm-bindgen-macro" 152 | version = "0.2.33" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | dependencies = [ 155 | "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", 156 | "wasm-bindgen-macro-support 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 157 | ] 158 | 159 | [[package]] 160 | name = "wasm-bindgen-macro-support" 161 | version = "0.2.33" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | dependencies = [ 164 | "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", 166 | "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "wasm-bindgen-backend 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 168 | "wasm-bindgen-shared 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 169 | ] 170 | 171 | [[package]] 172 | name = "wasm-bindgen-shared" 173 | version = "0.2.33" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | 176 | [[package]] 177 | name = "wasm-bindgen-test" 178 | version = "0.2.33" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | dependencies = [ 181 | "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", 183 | "js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", 184 | "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 185 | "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 186 | "wasm-bindgen-futures 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", 187 | "wasm-bindgen-test-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", 188 | ] 189 | 190 | [[package]] 191 | name = "wasm-bindgen-test-macro" 192 | version = "0.2.33" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | dependencies = [ 195 | "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", 196 | "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", 197 | ] 198 | 199 | [[package]] 200 | name = "wee_alloc" 201 | version = "0.4.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | dependencies = [ 204 | "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 205 | "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", 206 | "memory_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 207 | "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 208 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 209 | ] 210 | 211 | [[package]] 212 | name = "winapi" 213 | version = "0.3.6" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | dependencies = [ 216 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 217 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 218 | ] 219 | 220 | [[package]] 221 | name = "winapi-i686-pc-windows-gnu" 222 | version = "0.4.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | 225 | [[package]] 226 | name = "winapi-x86_64-pc-windows-gnu" 227 | version = "0.4.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | 230 | [metadata] 231 | "checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" 232 | "checksum console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6c5dd2c094474ec60a6acaf31780af270275e3153bafff2db5995b715295762e" 233 | "checksum dumb-crypto 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d834a69557fb94c1f2a64f1678455de8befff9c96240c3b1a6962bcf3c0a967" 234 | "checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b" 235 | "checksum js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "f0edfcbe54ba2071053f2e67f5dfbcea344b69aedfb371b76e27392427a5750f" 236 | "checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" 237 | "checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047" 238 | "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" 239 | "checksum memory_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" 240 | "checksum proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)" = "38fddd23d98b2144d197c0eca5705632d4fe2667d14a6be5df8934f8d74f1978" 241 | "checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" 242 | "checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" 243 | "checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9" 244 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 245 | "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 246 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 247 | "checksum wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "2d8c1eb210a0e91e24feb8ccd6f7484a5d442bfdf2ff179204b3a1d16e1029cc" 248 | "checksum wasm-bindgen-backend 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "57ab9a5e88bf5dea5be82bb5f8b68c0f8e550675796ac88570ea4c4e89923413" 249 | "checksum wasm-bindgen-futures 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d35a917b7c857bf8bce3e47d51d7d74a074d467aa2b750857fc7d9f878483124" 250 | "checksum wasm-bindgen-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "52909345426e198a0d34f63526f9f4d86ca50c24b4a22a019d0bc86570ffe1e3" 251 | "checksum wasm-bindgen-macro-support 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "2a130a5906bd540390cda0a28e7f8a2d450222461beb92a5eb5c6a33b8b8bc2a" 252 | "checksum wasm-bindgen-shared 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "9c406cd5b42c36db6f76f17d28bcd26187fd90cda686231f3d04de07a716fc3a" 253 | "checksum wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "edda7c4a76a8b2c1852e2bf707c3c537ae1a910fe5324bda77b10d249b6afd51" 254 | "checksum wasm-bindgen-test-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "1a97eada8f48c14b098237f45332b3b9d88bbc15ee9f93bf2478853fae85ebf2" 255 | "checksum wee_alloc 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "27875be1daf838fa18f3e94fd19fd12638e34615b42f56da2610c8f46be80cc6" 256 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 257 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 258 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 259 | -------------------------------------------------------------------------------- /src/pages/application.vue: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "en": { 4 | "title": { 5 | "new": "New Application" 6 | }, 7 | "computing": { 8 | "password": "Computing secure password...", 9 | "app": "Encrypting application..." 10 | }, 11 | 12 | "domain": { 13 | "description": "Examples: google.com, fb.com, etc", 14 | "placeholder": "google.com", 15 | "feedback": { 16 | "empty": "Domain can't be empty", 17 | "reserved": "Sorry, this domain name is reserved for internal use", 18 | "www": "Domain should not start with `www.`, `http://`, or any other `schema://`", 19 | "upper-case": "Domain should be lower-case", 20 | "whitespace": "Domain should not start or end with whitespace" 21 | } 22 | }, 23 | 24 | "preset": { 25 | "title": "Recommended configuration is available for this domain.", 26 | "danger": "Warning: Changing configuration for an existing app will change computed password. Consider computing and copying current password before making a change.", 27 | "use": "Use" 28 | }, 29 | 30 | "login": { 31 | "description": "Examples: my_user_name, derivepass82", 32 | "placeholder": "my@email.com", 33 | "feedback": { 34 | "empty": "Username can't be empty", 35 | "whitespace": "Username should not start or end with whitespace" 36 | } 37 | }, 38 | 39 | "revision": { 40 | "label": "Revision", 41 | "description": "Increment this by one to change the password", 42 | "feedback": { 43 | "non-positive": "Revision must be greater than zero" 44 | } 45 | }, 46 | 47 | "extra": { 48 | "button": "Extra Options", 49 | "danger": "Most websites do not require editing options below", 50 | "allowed": { 51 | "label": "Allowed characters", 52 | "description": "Characters that can be present in the password", 53 | "feedback": { 54 | "empty": "Can't be empty" 55 | } 56 | }, 57 | "required": { 58 | "label": "Required characters", 59 | "description": "Characters that must be present in the password" 60 | }, 61 | "max-len": { 62 | "label": "Password length", 63 | "feedback": { 64 | "less-than-one": "Must be greater than 1", 65 | "less-than-required": "Minimum length is {len}", 66 | "entropy": "Password is not strong enough. Please increase length, or add more allowed characters" 67 | } 68 | }, 69 | "save": { 70 | "ready": "Save", 71 | "complete": "Saved" 72 | }, 73 | "reset": "Reset", 74 | "delete": { 75 | "button": "Delete", 76 | "title": "Confirm Deletion", 77 | "confirm": "Delete", 78 | "pre": "This action will irreversibly delete:", 79 | "post": "...from the application list." 80 | } 81 | }, 82 | 83 | "leave": "You have unsaved changes. Are you sure you want to leave?" 84 | }, 85 | "ru": { 86 | "title": { 87 | "new": "Новое Приложение" 88 | }, 89 | "computing": { 90 | "password": "Генерируем надежный пароль...", 91 | "app": "Зашифровываем приложение..." 92 | }, 93 | 94 | "domain": { 95 | "description": "Примеры: google.com, fb.com, etc", 96 | "placeholder": "google.com", 97 | "feedback": { 98 | "empty": "Доменное имя не может быть пустым", 99 | "reserved": "Извините, но это доменное имя зарезервировано для внутреннего использования", 100 | "www": "Доменное имя не должен начинаться на `www.`, `http://`, или какой-либо другой `протокол://`", 101 | "upper-case": "Доменное имя не может содержать заглавные буквы", 102 | "whitespace": "Доменное имя не должен начинаться или заканчиваться пробелом" 103 | } 104 | }, 105 | 106 | "preset": { 107 | "title": "Для этого веб-сайта доступна рекомендованная конфигурация.", 108 | "danger": "Опасность: Смена конфигурации для существующего приложения изменит генерируемый пароль. Сгенерируйте и скопируйте пароль перед внесением изменений.", 109 | "use": "Использовать" 110 | }, 111 | 112 | "login": { 113 | "description": "Примеры: my_user_name, derivepass82", 114 | "placeholder": "my@email.com", 115 | "feedback": { 116 | "empty": "Имя пользователя не может быть пустым", 117 | "whitespace": "Имя пользователя не должно начинаться или заканчиваться пробелом" 118 | } 119 | }, 120 | 121 | "revision": { 122 | "label": "Ревизия", 123 | "description": "Добавьте единицу, если необходима смена пароля", 124 | "feedback": { 125 | "non-positive": "Ревизия должна быть больше нуля" 126 | } 127 | }, 128 | 129 | "extra": { 130 | "button": "Дополнительные Настройки", 131 | "danger": "Большинство веб-сайтов не требуют изменения настроек ниже", 132 | "allowed": { 133 | "label": "Разрешенные символы", 134 | "description": "Символы, которые могут присутствовать в пароле", 135 | "feedback": { 136 | "empty": "Поле не может быть пустым" 137 | } 138 | }, 139 | "required": { 140 | "label": "Необходимые символы", 141 | "description": "Символы, которые должны присутствовать в пароле" 142 | }, 143 | "max-len": { 144 | "label": "Длина пароля", 145 | "feedback": { 146 | "less-than-one": "Длина должна быть больше 1", 147 | "less-than-required": "Минимальная длина: {len}", 148 | "entropy": "Пароль недостаточно сложный. Пожалуйста, увеличьте длину или добавьте больше разрешенных символов" 149 | } 150 | }, 151 | "save": { 152 | "ready": "Сохранить", 153 | "complete": "Сохранено" 154 | }, 155 | "reset": "Сброс", 156 | "delete": { 157 | "button": "Удалить", 158 | "title": "Подтвердить Удаление", 159 | "confirm": "Удалить", 160 | "pre": "Это действие необратимо удалит:", 161 | "post": "...из списка приложений." 162 | } 163 | }, 164 | 165 | "leave": "У вас есть несохраненные изменения. Вы уверены, что хотите покинуть страницу?" 166 | }, 167 | "ca": { 168 | "title": { 169 | "new": "Nova Aplicació" 170 | }, 171 | "computing": { 172 | "password": "Computant contrasenya segura...", 173 | "app": "Encriptant aplicació..." 174 | }, 175 | 176 | "domain": { 177 | "description": "Exemples: google.com, fb.com, etc", 178 | "placeholder": "google.com", 179 | "feedback": { 180 | "empty": "No pots deixar el domini buit", 181 | "reserved": "Dispensa, aquest domini està reservat per a ús intern", 182 | "www": "El domini no ha de començar amb `www.`, `http://`, o qualsevol altre `schema://`", 183 | "upper-case": "El domini ha de ser en minúscules", 184 | "whitespace": "El domini no pot començar ni acabar amb espai" 185 | } 186 | }, 187 | 188 | "preset": { 189 | "title": "Hi ha una configuració recomanada per a aquest domini.", 190 | "danger": "Atenció: Canviar la configuració d'una aplicació existent canviarà la contrasenya computada. Considera computar i copiar la contrasenya actual abans de fer cap canvi.", 191 | "use": "Fes-la servir" 192 | }, 193 | 194 | "login": { 195 | "description": "Exemples: el_meu_nom, derivepass82", 196 | "placeholder": "el_meu@email.com", 197 | "feedback": { 198 | "empty": "El nom d'usuari no pot quedar buit", 199 | "whitespace": "El nom d'usuari no pot començar ni acabar amb espai" 200 | } 201 | }, 202 | 203 | "revision": { 204 | "label": "Revisió", 205 | "description": "Incrementa-ho una unitat per canviar la contrasenya", 206 | "feedback": { 207 | "non-positive": "La revisió ha de ser major que zero" 208 | } 209 | }, 210 | 211 | "extra": { 212 | "button": "Opcions Extra", 213 | "danger": "La majoria de llocs web no requereixen que editis les opcions que tens a continuació", 214 | "allowed": { 215 | "label": "Caràcters permesos", 216 | "description": "Caràcters que poden aparèixer a la contrasenya", 217 | "feedback": { 218 | "empty": "No pot quedar buit" 219 | } 220 | }, 221 | "required": { 222 | "label": "Caràcters requerits", 223 | "description": "Caràcters que han d'aparèixer a la contrasenya" 224 | }, 225 | "max-len": { 226 | "label": "Llargària de la contrasenyae", 227 | "feedback": { 228 | "less-than-one": "Ha de ser més gran que 1", 229 | "less-than-required": "La llargària mínima és de {len} caràcters", 230 | "entropy": "La contrasenya no és prou segura. Si et plau, incrementa la llargària o afegeix més caràcters permesos" 231 | } 232 | }, 233 | "save": { 234 | "ready": "Desa", 235 | "complete": "Desat" 236 | }, 237 | "reset": "Reinicia", 238 | "delete": { 239 | "button": "Esborra", 240 | "title": "Confirmar Supressió", 241 | "confirm": "Esborra", 242 | "pre": "Aquesta acció esborrarà de manera irreversible:", 243 | "post": "...de la llita d'aplicacions." 244 | } 245 | }, 246 | 247 | "leave": "Tens canvis sense desar. Segur que vols marxar?" 248 | } 249 | } 250 | 251 | 252 | 440 | 441 | 842 | 843 | 855 | --------------------------------------------------------------------------------