Lock vault after 3 wrong attempts (Fixes #197)

Fix share button, fix shared_key not added to storedCredential after sharing (Fixes #249)
    Add password app importer. Fixes #248
    Fix version check via proxy. Fixes #237
    Fix activity app not filtering. Fixes #246
    Add EnPass txt importer. Fixes #159
    Fix for disabled share button
    Require vault key for export. Fixes #199
    Indicate that sharing only works with users that have 1 or more vaults.
    Fixes #242
    Reset tags on logout. Fixes #245
    Ability to enter OTP secret manually. Fixes #198
    Create teampass importer. Fixes #244
This commit is contained in:
brantje 2017-02-17 21:25:18 +01:00
parent 76852c0d77
commit 6767322a22
No known key found for this signature in database
GPG key ID: 5FF1D117F918687F
40 changed files with 619 additions and 141 deletions

View file

@ -8,7 +8,7 @@ To make it possible for us to help you please fill out below information careful
If you like to make a feature request, please remove the issue template.
If you want to fill in a issue, remove the feature request template.
-->
##Issue
## Bug report
### Steps to reproduce
1.
@ -41,10 +41,8 @@ Tell us what happens instead
**cloud server:** Nextcloud or ownCloud
**cloud version:** (see admin page or version.php)
#### Browser log
<details>
<summary>Browser log</summary>

135
README.md
View file

@ -1,4 +1,5 @@
#Passman
Passman is a full featured password manager.
[![Build Status](https://travis-ci.org/nextcloud/passman.svg?branch=master)](https://travis-ci.org/nextcloud/passman)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/749bb288c9fd4592a73056549d44a85e)](https://www.codacy.com/app/brantje/passman?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=nextcloud/passman&amp;utm_campaign=Badge_Grade)
@ -6,43 +7,19 @@
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nextcloud/passman/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/nextcloud/passman/?branch=master)
Passman is a full featured password manager.
Features:
- Vaults
- Vault key is never sent to the server
- Credentials are stored with 256 bit AES
- Ability to add custom fields to credentials
- Built-in OTP(One Time Password) generator
- Password analyzer
- Share passwords internally and via link in a secure manner.
- Import from various password managers:
- KeePass
- LastPass
- DashLane
- ZOHO
- Clipperz.is
## Contents
* [Screenshots](https://github.com/nextcloud/passman#Screenshots)
* [Features](https://github.com/nextcloud/passman#features)
* [External apps](https://github.com/nextcloud/passman#external-apps)
* [Security](https://github.com/nextcloud/passman#security)
* [Password generation](https://github.com/nextcloud/passman#password-generation)
* [Storing credentials](https://github.com/nextcloud/passman#storing-credentials)
* [API](https://github.com/nextcloud/passman#api)
* [Docker](https://github.com/nextcloud/passman#docker)
* [Maintainers](https://github.com/nextcloud/passman#main-developers)
* [Contributors](https://github.com/nextcloud/passman#contributors)
For a demo of this app visit [https://demo.passman.cc](https://demo.passman.cc)
## Tested on
- NextCloud 10 / 11
- ownCloud 9.1+
## Browser extensions
At this moment we don't have any extensions available, sorry!
However, if you're interested in helping us (or you want to guide us while building)
Check out: https://github.com/nextcloud/passman-chrome-extension
## Supported databases
- SQL Lite*
- MySQL / MariaDB*
*Tested on travis
Untested databases:
- pgsql
##Screenshots
@ -56,15 +33,85 @@ Untested databases:
For more screenshots: [Click here](http://imgur.com/a/giKVt)
## Encryption (server side)
All passwords are encrypted client side AND server side.
This means that if you move to another server you have to backup the following from config.php
- `passwordsalt`
- `secret`
## Features:
- Vaults
- Vault key is never sent to the server
- Credentials are stored with 256 bit AES (see [security](https://github.com/nextcloud/passman#security))
- Ability to add custom fields to credentials
- Built-in OTP(One Time Password) generator
- Password analyzer
- Share passwords internally and via link in a secure manner.
- Import from various password managers:
- KeePass
- LastPass
- DashLane
- ZOHO
- Clipperz.is
- EnPass
- [ocPasswords](https://github.com/fcturner/passwords)
For a demo of this app visit [https://demo.passman.cc](https://demo.passman.cc)
## Tested on
- NextCloud 10 / 11
- ownCloud 9.1+
## External apps
- [Firefox / chrome extension](https://github.com/nextcloud/passman-webextension)
- [Android app](https://github.com/nextcloud/passman-android)
## Supported databases
- SQL Lite*
- MySQL / MariaDB*
*Tested on travis
Untested databases:
- pgsql
## Security
### Password generation
Passman features a build in password generator.
Not it only generates passwords, but it also measures their strength using [zxcvbn](https://github.com/dropbox/zxcvbn).
![](http://i.imgur.com/2qVBUfM.png)
Generate passwords as you like
![](http://i.imgur.com/jcRicOV.png)
Passwords are generated using the random functions from `sjcl`.
### Storing credentials
All passwords are encrypted client side using [sjcl](https://github.com/bitwiseshiftleft/sjcl) which uses AES-256 bit.
Users supply a vault key which is feed into sjcl as encryption key.
After the credentials are encrypted they are send to the server, there they will be encrypted again.
This time using the following routine:
- A key is generated using `passwordsalt` and `secret` from config.php *so back those up*
- Then the key is [stretched](http://en.wikipedia.org/wiki/Key_stretching) using [Password-Based Key Derivation Function 2](http://en.wikipedia.org/wiki/PBKDF2) (PBKDF2).
- [Encrypt-then-MAC](http://en.wikipedia.org/wiki/Authenticated_encryption#Approaches_to_Authenticated_Encryption) (EtM) is used for ensuring the authenticity of the encrypted data.
- Uses openssl with the `aes-256-cbc` ciper.
- [Initialization vector](http://en.wikipedia.org/wiki/Initialization_vector) (IV) is hidden
- [Double Hash-based Message Authentication Code](http://en.wikipedia.org/wiki/Hash-based_message_authentication_code) (HMAC) is applied for verification of the source data.
### Sharing credentials.
Passman allows users to share passwords (this can be turned off by an administrator).
## API
For developers passman offers an [api](https://github.com/nextcloud/passman/wiki/API).
## Support Passman
If you like passman, support us via [patreon](https://www.patreon.com/user?u=4833592) or [paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6YS8F97PETVU2)
Passman is open source, but we would gladly accept a beer (or pizza!)
Please consider donating via
- [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6YS8F97PETVU2)
- [Patreon](https://www.patreon.com/user?u=4833592)
- Bitcoin: 1H2c5tkGX54n48yEtM4Wm4UrAGTW85jQpe
## Code reviews
If you have any improvements regarding our code.
@ -77,8 +124,8 @@ Please do the following
Or if you're feeling lazy, create an issue, and we'll think about it.
## Docker
To run passman with docker you can use `docker run -p 8080:80 -p 8443:443 brantje/passman`
To use your own SSL cert mount them as below
To run passman with docker you can use `docker run -p 8080:80 -p 8443:443 brantje/passman`
You have to supply your own ssl certs.
Example:
`docker run -p 8080:80 -p 8443:443 -v /directory/cert.pem:/data/ssl/cert.pem -v /directory/cert.key:/data/ssl/cert.key brantje/passman`

View file

@ -40,7 +40,7 @@ $manager->registerNotifier(function() {
$manager = \OC::$server->getActivityManager();
$manager->registerExtension(function() {
return new Activity(
\OC::$server->getL10NFactory()
\OC::$server->getURLGenerator()
);
});

View file

@ -18,7 +18,7 @@ For an demo of this app visit [https://demo.passman.cc](https://demo.passman.cc)
]]></description>
<licence>AGPL</licence>
<version>2.0.2</version>
<version>2.1.0</version>
<author homepage="https://github.com/brantje">Sander Brand</author>
<author homepage="https://github.com/animalillo">Marcos Zuriaga</author>
<namespace>Passman</namespace>

View file

@ -69,6 +69,7 @@ class TranslationController extends ApiController {
// js/app/controllers/import.js
'import.file.read' => $this->trans->t('File read successfully!'),
'import.steps' => $this->trans->t('Follow the following steps to import your file'),
'import.no.label' => $this->trans->t('Credential has no label, skipping'),
'import.adding' => $this->trans->t('Adding {{credential}}'),
@ -144,7 +145,7 @@ class TranslationController extends ApiController {
// templates/views/partials/edit_credential/otp.html
'upload.qr' => $this->trans->t('Upload your OTP qr code'),
'upload.qr' => $this->trans->t('Upload or enter your OTP secret'),
'current.qr' => $this->trans->t('Current OTP settings'),
'issuer' => $this->trans->t('Issuer'),
'secret' => $this->trans->t('Secret'),
@ -173,6 +174,7 @@ class TranslationController extends ApiController {
// templates/views/partials/forms/settings/export.html
'export.type' => $this->trans->t('Export type'),
'export' => $this->trans->t('Export'),
'export.confirm.text' => $this->trans->t('Enter vault password to confirm export.'),
// templates/views/partials/forms/settings/general_settings.html
'rename.vault' => $this->trans->t('Rename vault'),
@ -226,6 +228,7 @@ class TranslationController extends ApiController {
// templates/vieuws/partials/forms/share_credential/basics.html
'search.u.g' => $this->trans->t('Search users or groups...'),
'search.result.missing' => $this->trans->t('Missing users? Only users that have vaults are shown.'),
'cyphering' => $this->trans->t('Cyphering'),
'uploading' => $this->trans->t('Uploading'),
'user' => $this->trans->t('User'),
@ -325,6 +328,7 @@ class TranslationController extends ApiController {
'accept' => $this->trans->t('Accept'),
'decline' => $this->trans->t('Decline'),
'session.time.left' => $this->trans->t('You have {{session_time}} left before logout.'),
'vault.locked' => $this->trans->t('Your vault has been locked for {{time}} because of {{tries}} failed attempts!'),
// templates/views/vaults.html
'last.access' => $this->trans->t('Last accessed'),

View file

@ -326,6 +326,8 @@
display: inline-block; }
.vault_wrapper .login_form .button {
margin-top: 10px; }
.vault_wrapper .login_opts {
margin-bottom: 10px; }
@media screen and (max-width: 768px) {
.vault_wrapper {
@ -637,6 +639,8 @@
#app-content #app-content-wrapper .edit_credential .custom_fields table tr td, #app-content #app-content-wrapper .edit_credential .files table tr td {
height: 50px;
vertical-align: middle; }
#app-content #app-content-wrapper .edit_credential .otpText {
padding-right: 10px; }
#app-content #app-content-wrapper .app_sidebar {
padding: 10px;
overflow-y: auto; }
@ -882,6 +886,12 @@
.tab_container.settings {
margin-bottom: 50px; }
.import-steps {
padding-left: 16px;
margin-bottom: 10px; }
.import-steps li {
list-style-type: disc; }
#app-settings-content:not(.ng-hide) {
height: 90px;
display: inherit !important;
@ -943,4 +953,15 @@
input[type="checkbox"] {
min-height: inherit; }
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px; }
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1; }
/*# sourceMappingURL=app.css.map */

File diff suppressed because one or more lines are too long

View file

@ -72,7 +72,7 @@
url: 'views/partials/forms/edit_credential/basics.html',
color: 'blue'
};
$scope.otpType = 'qrcode';
$translate(['general', 'password', 'custom.fields','files','otp']).then(function (translations) {
$scope.tabs = [{
title: translations.general,

View file

@ -34,7 +34,7 @@
.controller('ExportCtrl', ['$scope', '$window', 'CredentialService', 'VaultService', '$translate', function ($scope, $window, CredentialService, VaultService, $translate) {
$scope.available_exporters = [];
$scope.active_vault = VaultService.getActiveVault();
$scope.confirm_key = '';
$scope.$watch(function () {
return $window.PassmanExporter;
@ -58,6 +58,13 @@
$scope.startExport = function () {
$scope.error = false;
if(VaultService.getActiveVault().vaultKey !== $scope.confirm_key){
var msg = $translate.instant('invalid.vault.key');
$scope.error = msg;
_log(msg);
return;
}
_log($translate.instant('export.starting'));
var _credentials = [];
VaultService.getVault(VaultService.getActiveVault()).then(function (vault) {
@ -67,7 +74,8 @@
for (var i = 0; i < vault.credentials.length; i++) {
var _credential = angular.copy(vault.credentials[i]);
if (_credential.hidden === 0) {
_credential = CredentialService.decryptCredential(_credential);
var key = CredentialService.getSharedKeyFromCredential(_credential);
_credential = CredentialService.decryptCredential(_credential, key);
_credentials.push(_credential);
}
}

View file

@ -36,6 +36,7 @@
function ($scope, VaultService, $location, $rootScope, TagService, SettingsService) {
$rootScope.logout = function () {
SettingsService.setSetting('defaultVaultPass', false);
TagService.resetTags();
$rootScope.$broadcast('logout');
$location.path('/');
};

View file

@ -254,6 +254,8 @@
CredentialService.updateCredential(c, true).then(function () {
NotificationService.showNotification($translate.instant('credential.unshared'), 4000);
$scope.sharing_complete = true;
$scope.storedCredential.shared_key = null;
$scope.share_settings.credentialSharedWithUserAndGroup = [];
});
});
};
@ -365,6 +367,7 @@
_credential.skip_revision = true;
_credential.shared_key = EncryptService.encryptString(key);
CredentialService.updateCredential(_credential, true).then(function () {
$scope.storedCredential.shared_key = _credential.shared_key;
NotificationService.showNotification($translate.instant('credential.shared'), 4000);
$scope.sharing_complete = true;
});

View file

@ -31,7 +31,8 @@
* Controller of the passmanApp
*/
angular.module('passmanApp')
.controller('VaultCtrl', ['$scope', 'VaultService', 'SettingsService', 'CredentialService', '$location', 'ShareService', 'EncryptService', '$translate', '$rootScope', function ($scope, VaultService, SettingsService, CredentialService, $location, ShareService, EncryptService, $translate, $rootScope) {
.controller('VaultCtrl', ['$scope', 'VaultService', 'SettingsService', 'CredentialService', '$location', 'ShareService', 'EncryptService', '$translate', '$rootScope', '$interval',
function ($scope, VaultService, SettingsService, CredentialService, $location, ShareService, EncryptService, $translate, $rootScope, $interval) {
VaultService.getVaults().then(function (vaults) {
$scope.vaults = vaults;
if (SettingsService.getSetting('defaultVault') != null) {
@ -50,6 +51,10 @@
if (SettingsService.getSetting('defaultVaultPass')) {
$location.path('/vault/' + vault.guid);
}
$scope.vault_tries[vault.guid] = {
tries: 0,
timeout: 0
};
break;
}
}
@ -74,12 +79,12 @@
var settingsLoaded = function () {
$scope.minimal_value_key_strength = SettingsService.getSetting('vault_key_strength');
$translate(key_strengths[SettingsService.getSetting('vault_key_strength')]).then(function(translation){
$translate(key_strengths[SettingsService.getSetting('vault_key_strength')]).then(function (translation) {
$scope.required_score = {'strength': translation};
});
};
if(!SettingsService.getSetting('settings_loaded')){
if (!SettingsService.getSetting('settings_loaded')) {
$rootScope.$on('settings_loaded', function () {
settingsLoaded();
});
@ -107,7 +112,7 @@
}
};
$scope.toggleAutoLogout = function(){
$scope.toggleAutoLogout = function () {
$scope.auto_logout_timer = !$scope.auto_logout_timer;
};
@ -119,6 +124,12 @@
$scope.selectVault = function (vault) {
$scope.list_selected_vault = vault;
if(!$scope.vault_tries[vault.guid]) {
$scope.vault_tries[vault.guid] = {
tries: 0,
timeout: 0
};
}
};
$scope.sharing_keys = {};
$scope.newVault = function () {
@ -144,9 +155,9 @@
var _vault = angular.copy(vault);
_vault.vaultKey = angular.copy(vault_key);
delete _vault.credentials;
var timer = parseInt($scope.logout_timer);
if($scope.auto_logout_timer && timer > 0 ){
$rootScope.$broadcast('logout_timer_set', timer*60);
var timer = parseInt($scope.logout_timer);
if ($scope.auto_logout_timer && timer > 0) {
$rootScope.$broadcast('logout_timer_set', timer * 60);
}
VaultService.setActiveVault(_vault);
@ -158,6 +169,16 @@
$scope.logout_timer = time;
};
var tickLockTimer = function (guid) {
$scope.vault_tries[guid].timeout = $scope.vault_tries[guid].timeout - 1;
if($scope.vault_tries[guid].timeout <= 0){
$interval.cancel($scope.vault_tries[guid].timer);
$scope.vault_tries[guid].timeout = 0;
}
};
$scope.vault_tries = {};
$scope.vaultDecryptionKey = '';
$scope.loginToVault = function (vault, vault_key) {
$scope.error = false;
@ -174,8 +195,23 @@
} catch (e) {
$scope.error = $translate.instant('invalid.vault.key');
}
$scope.vault_tries[vault.guid].tries = $scope.vault_tries[vault.guid].tries + 1;
if($scope.vault_tries[vault.guid].tries >= 3){
var duration = (Math.pow(2, 1 / 7) * Math.pow(15, 4 / 7)) * Math.pow((Math.pow(2, 2 / 7) * Math.pow(15, 1 / 7)), $scope.vault_tries[vault.guid].tries);
$scope.vault_tries[vault.guid].timeout = duration;
if($scope.vault_tries[vault.guid].hasOwnProperty('timer')){
$interval.cancel($scope.vault_tries[vault.guid].timer);
}
$scope.vault_tries[vault.guid].timer = $interval(function () {
tickLockTimer(vault.guid);
} ,1000);
}
}
};

View file

@ -47,6 +47,9 @@
}
}
}
},
resetTags: function () {
_tags = [];
}
};

View file

@ -132,3 +132,19 @@ var PassmanImporter = {};
return JSON.parse(string);
};
})(window, $, PassmanImporter);
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
Array.prototype.clean = function(deleteValue) {
for (var i = 0; i < this.length; i++) {
if (this[i] === deleteValue) {
this.splice(i, 1);
i--;
}
}
return this;
};

View file

@ -29,7 +29,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'Clipperz.is',
id: 'clippers',
description: 'Go to menu -> Export -> Download HTML + JSON. Fields will be imported as custom fields.'
exportSteps: ['Go to menu -> Export -> Download HTML + JSON. Fields will be imported as custom fields.']
}
};

View file

@ -30,7 +30,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'Dashlane 4 csv',
id: 'dashLaneCsv',
description: 'Create an csv export. Go to File -> export -> Unsecured archive (readable) in CSV format'
exportSteps: ['Create an csv export. Go to File -> export -> Unsecured archive (readable) in CSV format']
}
};

View file

@ -0,0 +1,121 @@
/**
* Nextcloud - passman
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Importers should always start with this
/** global: PassmanImporter */
var PassmanImporter = PassmanImporter || {};
(function(window, $, PassmanImporter) {
'use strict';
// Define the importer
PassmanImporter.EnPassTXT = {
info: {
name: 'EnPass text file',
id: 'EnPassTXT',
exportSteps: ['Access your Enpass Database. Select "File" > "Export" > "As Text"']
}
};
function parseEnpass(fileData){
var lastProperty, matches, loginBlocks, property;
loginBlocks = fileData.replaceAll("Title :","<~passman~>\nTitle :").split('<~passman~>\n').clean("");
var regex = /(.*) : (.*)/;
var results = [];
for(var l = 0; l < loginBlocks.length; l++){
var loginBlock = loginBlocks[l];
var lrow = loginBlock.split('\n');
var result = {};
for(var r = 0; r < lrow.length; r++){
var row = lrow[r];
matches = regex.exec(row);
if(matches){
property = matches[1];
result[property] = matches[2];
} else {
if(lastProperty){
result[lastProperty] += "\n" + row;
}
}
if(property) {
lastProperty = property;
}
}
results.push(result)
}
return results
}
PassmanImporter.EnPassTXT.readFile = function (file_data) {
var mapper = {
'Title': 'label',
'Username': 'username',
'Password': 'password',
'Email': 'email',
'Url': 'url',
'Note': 'description'
};
var secret_fields = ['cvc', 'pin', 'security answer'];
/** global: C_Promise */
return new C_Promise(function(){
var credential_list = [];
var credentials = parseEnpass(file_data);
for (var i = 0; i < credentials.length; i++) {
var enpass_credential = credentials[i];
var new_credential = PassmanImporter.newCredential();
for(var key in enpass_credential){
if(!enpass_credential.hasOwnProperty(key)){
continue;
}
if(mapper.hasOwnProperty(key)){
var prop = mapper[key];
new_credential[prop] = enpass_credential[key];
} else {
if(key !== 'TOTP') {
var isSecret = (secret_fields.indexOf(key.toLowerCase()) !== -1) ? 1 : 0;
new_credential.custom_fields.push({
'label': key,
'value': enpass_credential[key],
'secret': isSecret
})
}
}
}
if(enpass_credential.hasOwnProperty('TOTP')){
new_credential.otp.secret = enpass_credential['TOTP'];
}
var progress = {
percent: i/credentials.length*100,
loaded: i,
total: credentials.length
};
credential_list.push(new_credential);
this.call_progress(progress);
}
this.call_then(credential_list);
});
};
})(window, $, PassmanImporter);

View file

@ -30,7 +30,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'KeePass csv',
id: 'keepassCsv',
description: 'Create an csv export with the following options enabled: http://i.imgur.com/CaeTA4d.png'
exportSteps: ['Create an csv export with the following options enabled: http://i.imgur.com/CaeTA4d.png']
}
};

View file

@ -30,7 +30,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'LastPass csv',
id: 'lastpassCsv',
description: 'Create an csv export. Go to More options -> Advanced -> Export -> Last Pass CSV File'
exportSteps: ['Create an csv export. Go to More options -> Advanced -> Export -> Last Pass CSV File']
}
};

View file

@ -0,0 +1,71 @@
/**
* Nextcloud - passman
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Importers should always start with this
/** global: PassmanImporter */
var PassmanImporter = PassmanImporter || {};
(function (window, $, PassmanImporter) {
'use strict';
// Define the importer
var steps = [
'Backups for the Passwords app need to be enabled on the Admin panel (they are disabled by default).',
'On the Passwords App, in the bottom left corner, press Settings',
'Press "Download Backup"',
'Confirm the export and save the file'
];
PassmanImporter.passwordsApp = {
info: {
name: 'Passwords App csv',
id: 'passwordsApp',
exportSteps: steps
}
};
PassmanImporter.passwordsApp.readFile = function (file_data) {
/** global: C_Promise */
var p = new C_Promise(function () {
var parsed_csv = PassmanImporter.readCsv(file_data);
var credential_list = [];
for (var i = 0; i < parsed_csv.length; i++) {
var row = parsed_csv[i];
var _credential = PassmanImporter.newCredential();
_credential.label = row.website + ' - '+ row.username;
_credential.username = row.username;
_credential.password = row.password;
_credential.url = row.fulladdress;
_credential.description = row.notes;
credential_list.push(_credential);
var progress = {
percent: i / parsed_csv.length * 100,
loaded: i,
total: parsed_csv.length
};
this.call_progress(progress);
}
this.call_then(credential_list);
});
return p;
};
})(window, $, PassmanImporter);

View file

@ -29,7 +29,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'Passman JSON',
id: 'passmanJson',
description: 'Export the item in passman as passman json, with all fields enabled'
exportSteps: ['Export the item in passman as passman json, with all fields enabled']
}
};

View file

@ -30,7 +30,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'Passpack csv',
id: 'passpackCsv',
description: 'Go to Tools -> Export. Select Comma Separated Values, All entries then continue.'
exportSteps: ['Go to Tools -> Export. Select Comma Separated Values, All entries then continue.']
}
};

View file

@ -32,7 +32,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'Random data',
id: 'randomData',
description: 'Create\'s 50 random credentials for testing purposes.'
exportSteps: ['Create\'s 50 random credentials for testing purposes.']
}
};

View file

@ -0,0 +1,62 @@
/**
* Nextcloud - passman
*
* @copyright Copyright (c) 2016, Sander Brand (brantje@gmail.com)
* @copyright Copyright (c) 2016, Marcos Zuriaga Miguel (wolfi@wolfi.es)
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Importers should always start with this
/** global: PassmanImporter */
var PassmanImporter = PassmanImporter || {};
(function(window, $, PassmanImporter) {
'use strict';
// Define the importer
PassmanImporter.teamPassCsv = {
info: {
name: 'TeamPass csv',
id: 'teamPassCsv',
exportSteps: ['Go to Tools -> Export. Select Comma Separated Values, All entries then continue.']
}
};
PassmanImporter.teamPassCsv.readFile = function (file_data) {
/** global: C_Promise */
return new C_Promise(function(){
var parsed_csv = PassmanImporter.readCsv(file_data, false);
var credential_list = [];
for (var i = 0; i < parsed_csv.length; i++) {
var row = parsed_csv[i];
var _credential = PassmanImporter.newCredential();
_credential.label = row[1];
_credential.description = row[2];
_credential.password = row[3];
_credential.username = row[4];
var progress = {
percent: i/parsed_csv.length*100,
loaded: i,
total: parsed_csv.length
};
credential_list.push(_credential);
this.call_progress(progress);
}
this.call_then(credential_list);
});
};
})(window, $, PassmanImporter);

View file

@ -32,7 +32,7 @@ var PassmanImporter = PassmanImporter || {};
info: {
name: 'ZOHO csv',
id: 'zohoCsv',
description: 'Create an csv export. Go to Tools -> Export secrets -> Select "General CSV" and click "Export Secrets"'
exportSteps: ['Create an csv export. Go to Tools -> Export secrets -> Select "General CSV" and click "Export Secrets"']
}
};

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,11 @@
namespace OCA\Passman;
use OCP\IURLGenerator;
class Activity implements \OCP\Activity\IExtension {
const FILTER_PASSMAN = 'passman';
const APP_NAME = 'passman';
const TYPE_ITEM_ACTION = 'passman_item_action';
const TYPE_ITEM_EXPIRED = 'passman_item_expired';
const TYPE_ITEM_SHARED = 'passman_item_shared';
@ -49,6 +53,13 @@ class Activity implements \OCP\Activity\IExtension {
const SUBJECT_ITEM_RENAMED_SELF = 'item_renamed_self';
protected $URLGenerator;
public function __construct( IURLGenerator $URLGenerator) {
$this->URLGenerator = $URLGenerator;
}
/**
* The extension can return an array of additional notification types.
* If no additional types are to be added false is to be returned
@ -57,7 +68,7 @@ class Activity implements \OCP\Activity\IExtension {
* @return array|false
*/
public function getNotificationTypes($languageCode) {
$l = \OC::$server->getL10N('passman', $languageCode);
$l = \OC::$server->getL10N(self::APP_NAME, $languageCode);
return array(
self::TYPE_ITEM_ACTION => $l->t('A Passman item has been created, modified or deleted'),
self::TYPE_ITEM_EXPIRED => $l->t('A Passman item has expired'),
@ -75,7 +86,7 @@ class Activity implements \OCP\Activity\IExtension {
* @return array|false
*/
public function filterNotificationTypes($types, $filter) {
return $types;
return $filter === self::FILTER_PASSMAN ? [self::TYPE_ITEM_ACTION, self::TYPE_ITEM_EXPIRED, self::TYPE_ITEM_SHARED, self::TYPE_ITEM_RENAMED] : $types;
}
/**
@ -116,8 +127,8 @@ class Activity implements \OCP\Activity\IExtension {
* @return string|false
*/
public function translate($app, $text, $params, $stripPath, $highlightParams, $languageCode) {
$l = \OC::$server->getL10NFactory()->get('passman', $languageCode);
if ($app === 'passman') {
$l = \OC::$server->getL10NFactory()->get(self::APP_NAME, $languageCode);
if ($app === self::APP_NAME) {
switch ($text) {
case self::SUBJECT_ITEM_CREATED:
return $l->t('%1$s has been created by %2$s', $params);
@ -172,7 +183,7 @@ class Activity implements \OCP\Activity\IExtension {
* @return array|false
*/
public function getSpecialParameterList($app, $text) {
if ($app === 'passman') {
if ($app === self::APP_NAME) {
switch ($text) {
case self::SUBJECT_ITEM_CREATED:
case self::SUBJECT_ITEM_CREATED_SELF:
@ -247,14 +258,14 @@ class Activity implements \OCP\Activity\IExtension {
* @return array|false
*/
public function getNavigation() {
$l = \OC::$server->getL10N('passman');
$l = \OC::$server->getL10N(self::APP_NAME);
return array(
'top' => array(),
'apps' => array(
'apps' => array( self::FILTER_PASSMAN =>
array(
'id' => 'passman',
'name' => (string) $l->t('Passwords'),
'url' => '',//FIXME: Currenlty we cannot link directly to links, so this needs to be fixed... this->URLGenerator->linkToRoute('activity.Activities.showList', array('filter' => 'passman')),
'url' => $this->URLGenerator->linkToRoute('activity.Activities.showList', ['filter' => self::FILTER_PASSMAN]),
),
),
);
@ -267,7 +278,7 @@ class Activity implements \OCP\Activity\IExtension {
* @return boolean
*/
public function isFilterValid($filterValue) {
return $filterValue === 'passman';
return $filterValue === self::FILTER_PASSMAN;
}
/**
@ -280,9 +291,13 @@ class Activity implements \OCP\Activity\IExtension {
* @return array|false
*/
public function getQueryForFilter($filter) {
if ($filter === 'passman') {
return array('`app` = ?', array('passman'));
if ($filter === self::FILTER_PASSMAN) {
return [
'(`app` = ?)',
[self::APP_NAME],
];
}
return false;
}
}

View file

@ -106,4 +106,15 @@
//Fix NC fucking up our style
input[type="checkbox"]{
min-height: inherit;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}

View file

@ -375,6 +375,9 @@
.file_tab {
}
.otpText{
padding-right: 10px;
}
}
.app_sidebar {
h2{

View file

@ -40,4 +40,11 @@
}
.tab_container.settings{
margin-bottom: 50px;
}
.import-steps{
padding-left: 16px;
li{
list-style-type: disc;
}
margin-bottom: 10px;
}

View file

@ -90,6 +90,9 @@
margin-top: 10px;
}
}
.login_opts{
margin-bottom: 10px;
}
}
@media screen and (max-width: 768px){

View file

@ -82,7 +82,10 @@ script('passman', 'importers/importer-lastpasscsv');
script('passman', 'importers/importer-dashlanecsv');
script('passman', 'importers/importer-zohocsv');
script('passman', 'importers/importer-passmanjson');
script('passman', 'importers/importer-ocpasswords');
script('passman', 'importers/importer-clipperz');
script('passman', 'importers/importer-teampass');
script('passman', 'importers/importer-enpass');
script('passman', 'importers/importer-passpackcsv');
script('passman', 'importers/importer-randomdata');
script('passman', 'exporters/exporter-main');
@ -96,7 +99,7 @@ script('passman', 'exporters/exporter-csv');
/*build-css-start*/
style('passman', 'vendor/ng-password-meter/ng-password-meter');
style('passman', 'vendor/bootstrap/bootstrap.min');
style('passman', 'vendor/bootstrap/bootstrap-theme.min');
style('passman', 'vendor/font-awesome/font-awesome.min');
style('passman', 'vendor/angular-xeditable/xeditable.min');
style('passman', 'vendor/ng-tags-input/ng-tags-input.min');

View file

@ -9,17 +9,31 @@ $checkVersion = OC::$server->getConfig()->getAppValue('passman', 'check_version'
$AppInstance = new App();
$localVersion = $AppInstance->getAppInfo("passman")["version"];
if ($checkVersion) {
// get latest master version
$doc = new DOMDocument();
$doc->load('https://raw.githubusercontent.com/nextcloud/passman/master/appinfo/info.xml');
$root = $doc->getElementsByTagName("info");
// get latest master version
$version = false;
$githubVersion = $l->t('Unable to get version info');
foreach ($root as $element) {
$versions = $element->getElementsByTagName("version");
$version = $versions->item(0)->nodeValue;
$url = 'https://raw.githubusercontent.com/nextcloud/passman/master/appinfo/info.xml';
try {
$client = OC::$server->getHTTPClientService()->newClient();
$response = $client->get($url);
$xml = $response->getBody();
} catch (\Exception $e) {
$xml = false;
}
if ($version) {
if ($xml) {
$loadEntities = libxml_disable_entity_loader(true);
$data = @simplexml_load_string($xml);
libxml_disable_entity_loader($loadEntities);
if ($data !== false) {
$version = (string)$data->version;
} else {
libxml_clear_errors();
}
}
if ($version !== false) {
$githubVersion = $version;
}
}
@ -36,7 +50,7 @@ $ciphers = openssl_get_cipher_methods();
} ?>
Local version: <?php p($localVersion); ?><br/>
<?php
if (version_compare($githubVersion, $localVersion) === 1) {
if ($checkVersion && version_compare($githubVersion, $localVersion) === 1) {
p($l->t('A newer version of passman is available'));
}
?>

View file

@ -1,18 +1,24 @@
<div class="row">
<div class="col-xs-12">
<div class="col-xs-2 nopadding">
{{ 'upload.qr' | translate}}
<div class="col-xs-4 nopadding">
<span class="otpText">{{ 'upload.qr' | translate}}</span>
<select ng-model="otpType">
<option value="qrcode">Upload QR code</option>
<option value="secret">Enter secret</option>
</select>
</div>
<div class="col-xs-6 nopadding">
<input type="file" qrread on-read="parseQR(qrdata)"
class="input_secret"
on-read="parseQR(qrdata)"/>
on-read="parseQR(qrdata)" ng-show="otpType === 'qrcode'"/>
<input type="text" ng-model="storedCredential.otp.secret" ng-show="otpType === 'secret'">
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12" ng-if="storedCredential.otp">{{ 'current.qr' | translate}}
<div class="col-xs-12" ng-if="storedCredential.otp">
<b>{{ 'current.qr' | translate}}</b>
</div>
</div>
<div class="row">
@ -22,23 +28,23 @@
<div class="col-sm-4 col-sm-5 col-md-5">
<table ng-show="storedCredential.otp">
<tr ng-show="storedCredential.otp.type">
<td>{{ 'type' | translate}}:</td>
<td>{{ 'type' | translate}}: </td>
<td>{{storedCredential.otp.type}}</td>
</tr>
<tr ng-show="storedCredential.otp.label">
<td>{{ 'label' | translate}}:</td>
<td>{{ 'label' | translate}}: </td>
<td>{{storedCredential.otp.label}}</td>
</tr>
<tr ng-show="storedCredential.otp.issuer">
<td>{{ 'issuer' | translate}}:</td>
<td>{{ 'issuer' | translate}}: </td>
<td>{{storedCredential.otp.issuer}}</td>
</tr>
<tr ng-show="storedCredential.otp.secret">
<td>{{ 'secret' | translate}}:</td>
<td>{{ 'secret' | translate}}: </td>
<td>{{storedCredential.otp.secret}}</td>
</tr>
<tr ng-show="storedCredential.otp.secret">
<td>{{ 'otp' | translate}}:</td>
<td>{{ 'otp' | translate}}: </td>
<td><span otp-generator
secret="storedCredential.otp.secret"></span>
</td>

View file

@ -10,13 +10,26 @@
</option>
</select></label>
<div><b>{{selectedExporter.description}}</b></div>
<button class="button" ng-click="startExport()"
ng-if="selectedExporter">{{ 'export' | translate}}
</button>
<div ng-show="selectedExporter" class="col-xs-3 nopadding">
<label>{{ 'export.confirm.text' | translate }}</label>
<input type="password" ng-model="confirm_key">
<br />
<div class="alert alert-warning" ng-show="error">
{{error}}
</div>
</div>
<div class="clearfix">
<button class=" button" ng-click="startExport()"
ng-if="selectedExporter">{{ 'export' | translate}}
</button>
</div>
</div>
<div class="col-xs-6">
<div ng-if="log" class="import_log">
<textarea id="import_log" auto-scroll="log">{{log.join('\n')}}</textarea>
<textarea id="import_log"
auto-scroll="log">{{log.join('\n')}}</textarea>
</div>
</div>
</div>

View file

@ -9,7 +9,12 @@
{{importer.name}}
</option>
</select></label>
<div><b>{{selectedImporter.description}}</b></div>
<div ng-show="selectedImporter">
<b>{{ 'import.steps' | translate }}</b>
<ul class="import-steps">
<li ng-repeat="step in selectedImporter.exportSteps">{{step}}</li>
</ul>
</div>
<input ng-if="selectedImporter" type="file" file-select
success="fileLoaded" error="fileLoadError"
progress="fileSelectProgress"><br/>

View file

@ -22,6 +22,11 @@
</button>
</td>
</tr>
<tr>
<td colspan="2">
<small>{{ 'search.result.missing' | translate}}</small>
</td>
</tr>
</thead>
</table>

View file

@ -27,9 +27,9 @@
<div class="tab_container share_credential" ng-show="currentTab">
<div ng-include="currentTab.url"></div>
<button ng-click="applyShare()" ng-disabled="!share_settings.linkSharing.enabled || share_settings.credentialSharedWithUserAndGroup.length > 0">{{ 'share' | translate}}</button>
<button ng-click="applyShare()" ng-disabled="share_settings.linkSharing.enabled === false && share_settings.credentialSharedWithUserAndGroup.length === 0">{{ 'share' | translate}}</button>
<button ng-click="cancel()">{{ 'cancel' | translate}}</button>
<button class="btn btn-danger" ng-click="unshareCredential(storedCredential)">{{ 'unshare' | translate}}</button>
<button class="btn btn-danger" ng-disabled="!storedCredential.shared_key" ng-click="unshareCredential(storedCredential)">{{ 'unshare' | translate}}</button>
</div>

View file

@ -6,7 +6,7 @@
<a><i class="fa fa-home"></i></a>
</div>
<div class="crumb svg last" ng-click="clearState()">
<a>{{active_vault.name}}</a>
<span>{{active_vault.name}}</span>
</div>
</div>
</div>

View file

@ -95,7 +95,7 @@
<span ng-if="list_selected_vault.last_access === 0">Never</span>
</small>
</div>
<div>
<div class="login_opts">
<div>
<label>
<input type="checkbox" ng-checked="default_vault"
@ -128,10 +128,12 @@
</select>
</div>
</div>
<div class="button button-geen"
ng-click="loginToVault(list_selected_vault, vault_key)">
<div class="alert alert-danger" ng-show="vault_tries[list_selected_vault.guid].timeout !== 0" translate="vault.locked" translate-value-tries="{{ vault_tries[list_selected_vault.guid].tries }}" translate-value-time="{{ vault_tries[list_selected_vault.guid].timeout | toHHMMSS }}"></div>
<button class="button button-geen"
ng-click="loginToVault(list_selected_vault, vault_key)" ng-disabled="vault_tries[list_selected_vault.guid].timeout !== 0">
{{ 'vault.decrypt' | translate}}
</div>
</button>
</div>
</div>