angular .module('bit', [ 'ui.router', 'ngMessages', 'angular-jwt', 'ui.bootstrap.showErrors', 'toastr', 'angulartics', 'angulartics.google.analytics', 'angular-stripe', 'credit-cards', 'angular-promise-polyfill', 'bit.directives', 'bit.filters', 'bit.services', 'bit.global', 'bit.accounts', 'bit.vault', 'bit.settings', 'bit.tools', 'bit.organization', 'bit.reports' ]); angular.module("bit") .constant("appSettings", {"apiUri":"/api","identityUri":"/identity","iconsUri":"https://icons.bitwarden.com","stripeKey":"pk_live_bpN0P37nMxrMQkcaHXtAybJk","braintreeKey":"production_qfbsv8kc_njj2zjtyngtjmbjd","selfHosted":false,"version":"1.26.0","environment":"Production"}); angular .module('bit.accounts', ['ui.bootstrap', 'ngCookies']); angular .module('bit.directives', []); angular .module('bit.filters', []); angular .module('bit.global', []); angular .module('bit.organization', ['ui.bootstrap']); angular .module('bit.services', ['ngResource', 'ngStorage', 'angular-jwt']); angular .module('bit.reports', ['toastr', 'ngSanitize']); angular .module('bit.settings', ['ui.bootstrap', 'toastr']); angular .module('bit.vault', ['ui.bootstrap', 'ngclipboard']); angular .module('bit.tools', ['ui.bootstrap', 'toastr']); angular .module('bit') .factory('apiInterceptor', ["$injector", "$q", "toastr", "appSettings", "utilsService", function ($injector, $q, toastr, appSettings, utilsService) { return { request: function (config) { if (config.url.indexOf(appSettings.apiUri + '/') === 0) { config.headers['Device-Type'] = utilsService.getDeviceType(); } return config; }, response: function (response) { if (response.status === 401 || response.status === 403) { $injector.get('authService').logOut(); $injector.get('$state').go('frontend.login.info').then(function () { toastr.warning('Your login session has expired.', 'Logged out'); }); } return response || $q.when(response); }, responseError: function (rejection) { if (rejection.status === 401 || rejection.status === 403) { $injector.get('authService').logOut(); $injector.get('$state').go('frontend.login.info').then(function () { toastr.warning('Your login session has expired.', 'Logged out'); }); } return $q.reject(rejection); } }; }]); angular .module('bit') .config(["$stateProvider", "$urlRouterProvider", "$httpProvider", "jwtInterceptorProvider", "jwtOptionsProvider", "$uibTooltipProvider", "toastrConfig", "$locationProvider", "$qProvider", "appSettings", "stripeProvider", function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, jwtOptionsProvider, $uibTooltipProvider, toastrConfig, $locationProvider, $qProvider, appSettings /* jshint ignore:start */ , stripeProvider /* jshint ignore:end */ ) { angular.extend(appSettings, window.bitwardenAppSettings); $qProvider.errorOnUnhandledRejections(false); $locationProvider.hashPrefix(''); jwtOptionsProvider.config({ whiteListedDomains: ['localhost', 'api.bitwarden.com', 'vault.bitwarden.com', 'haveibeenpwned.com'] }); var refreshPromise; jwtInterceptorProvider.tokenGetter = /*@ngInject*/ ["options", "tokenService", "authService", function (options, tokenService, authService) { if (options.url.indexOf(appSettings.apiUri + '/') !== 0) { return; } if (refreshPromise) { return refreshPromise; } var token = tokenService.getToken(); if (!token) { return; } if (!tokenService.tokenNeedsRefresh(token)) { return token; } var p = authService.refreshAccessToken(); if (!p) { return; } refreshPromise = p.then(function (newToken) { refreshPromise = null; return newToken || token; }); return refreshPromise; }]; stripeProvider.setPublishableKey(appSettings.stripeKey); angular.extend(toastrConfig, { closeButton: true, progressBar: true, showMethod: 'slideDown', target: '.toast-target' }); $uibTooltipProvider.options({ popupDelay: 600, appendToBody: true }); // stop IE from caching get requests if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) { if (!$httpProvider.defaults.headers.get) { $httpProvider.defaults.headers.get = {}; } $httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache'; $httpProvider.defaults.headers.get.Pragma = 'no-cache'; } $httpProvider.interceptors.push('apiInterceptor'); $httpProvider.interceptors.push('jwtInterceptor'); $urlRouterProvider.otherwise('/'); $stateProvider // Backend .state('backend', { templateUrl: 'app/views/backendLayout.html', abstract: true, data: { authorize: true } }) .state('backend.user', { templateUrl: 'app/views/userLayout.html', abstract: true }) .state('backend.user.vault', { url: '^/vault', templateUrl: 'app/vault/views/vault.html', controller: 'vaultController', data: { pageTitle: 'My Vault', controlSidebar: true }, params: { refreshFromServer: false } }) .state('backend.user.settings', { url: '^/settings', templateUrl: 'app/settings/views/settings.html', controller: 'settingsController', data: { pageTitle: 'Settings' } }) .state('backend.user.settingsDomains', { url: '^/settings/domains', templateUrl: 'app/settings/views/settingsDomains.html', controller: 'settingsDomainsController', data: { pageTitle: 'Domain Settings' } }) .state('backend.user.settingsTwoStep', { url: '^/settings/two-step', templateUrl: 'app/settings/views/settingsTwoStep.html', controller: 'settingsTwoStepController', data: { pageTitle: 'Two-step Login' } }) .state('backend.user.settingsCreateOrg', { url: '^/settings/create-organization', templateUrl: 'app/settings/views/settingsCreateOrganization.html', controller: 'settingsCreateOrganizationController', data: { pageTitle: 'Create Organization' } }) .state('backend.user.settingsBilling', { url: '^/settings/billing', templateUrl: 'app/settings/views/settingsBilling.html', controller: 'settingsBillingController', data: { pageTitle: 'Billing' } }) .state('backend.user.settingsPremium', { url: '^/settings/premium', templateUrl: 'app/settings/views/settingsPremium.html', controller: 'settingsPremiumController', data: { pageTitle: 'Go Premium' } }) .state('backend.user.tools', { url: '^/tools', templateUrl: 'app/tools/views/tools.html', controller: 'toolsController', data: { pageTitle: 'Tools' } }) .state('backend.user.reportsBreach', { url: '^/reports/breach', templateUrl: 'app/reports/views/reportsBreach.html', controller: 'reportsBreachController', data: { pageTitle: 'Data Breach Report' } }) .state('backend.org', { templateUrl: 'app/views/organizationLayout.html', abstract: true }) .state('backend.org.dashboard', { url: '^/organization/:orgId', templateUrl: 'app/organization/views/organizationDashboard.html', controller: 'organizationDashboardController', data: { pageTitle: 'Organization Dashboard' } }) .state('backend.org.people', { url: '/organization/:orgId/people?viewEvents&search', templateUrl: 'app/organization/views/organizationPeople.html', controller: 'organizationPeopleController', data: { pageTitle: 'Organization People' } }) .state('backend.org.collections', { url: '/organization/:orgId/collections?search', templateUrl: 'app/organization/views/organizationCollections.html', controller: 'organizationCollectionsController', data: { pageTitle: 'Organization Collections' } }) .state('backend.org.settings', { url: '/organization/:orgId/settings', templateUrl: 'app/organization/views/organizationSettings.html', controller: 'organizationSettingsController', data: { pageTitle: 'Organization Settings' } }) .state('backend.org.billing', { url: '/organization/:orgId/billing', templateUrl: 'app/organization/views/organizationBilling.html', controller: 'organizationBillingController', data: { pageTitle: 'Organization Billing' } }) .state('backend.org.vault', { url: '/organization/:orgId/vault?viewEvents&search', templateUrl: 'app/organization/views/organizationVault.html', controller: 'organizationVaultController', data: { pageTitle: 'Organization Vault', controlSidebar: true } }) .state('backend.org.groups', { url: '/organization/:orgId/groups?search', templateUrl: 'app/organization/views/organizationGroups.html', controller: 'organizationGroupsController', data: { pageTitle: 'Organization Groups' } }) .state('backend.org.events', { url: '/organization/:orgId/events', templateUrl: 'app/organization/views/organizationEvents.html', controller: 'organizationEventsController', data: { pageTitle: 'Organization Events' } }) // Frontend .state('frontend', { templateUrl: 'app/views/frontendLayout.html', abstract: true, data: { authorize: false } }) .state('frontend.login', { templateUrl: 'app/accounts/views/accountsLogin.html', controller: 'accountsLoginController', params: { returnState: null, email: null, premium: null, org: null }, data: { bodyClass: 'login-page' } }) .state('frontend.login.info', { url: '^/?org&premium&email', templateUrl: 'app/accounts/views/accountsLoginInfo.html', data: { pageTitle: 'Log In' } }) .state('frontend.login.twoFactor', { url: '^/two-step?org&premium&email', templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html', data: { pageTitle: 'Log In (Two-step)' } }) .state('frontend.logout', { url: '^/logout', controller: 'accountsLogoutController', data: { authorize: true } }) .state('frontend.passwordHint', { url: '^/password-hint', templateUrl: 'app/accounts/views/accountsPasswordHint.html', controller: 'accountsPasswordHintController', data: { pageTitle: 'Master Password Hint', bodyClass: 'login-page' } }) .state('frontend.recover', { url: '^/recover', templateUrl: 'app/accounts/views/accountsRecover.html', controller: 'accountsRecoverController', data: { pageTitle: 'Recover Account', bodyClass: 'login-page' } }) .state('frontend.recover-delete', { url: '^/recover-delete', templateUrl: 'app/accounts/views/accountsRecoverDelete.html', controller: 'accountsRecoverDeleteController', data: { pageTitle: 'Delete Account', bodyClass: 'login-page' } }) .state('frontend.verify-recover-delete', { url: '^/verify-recover-delete?userId&token&email', templateUrl: 'app/accounts/views/accountsVerifyRecoverDelete.html', controller: 'accountsVerifyRecoverDeleteController', data: { pageTitle: 'Confirm Delete Account', bodyClass: 'login-page' } }) .state('frontend.register', { url: '^/register?org&premium', templateUrl: 'app/accounts/views/accountsRegister.html', controller: 'accountsRegisterController', params: { returnState: null, email: null, org: null, premium: null }, data: { pageTitle: 'Register', bodyClass: 'register-page' } }) .state('frontend.organizationAccept', { url: '^/accept-organization?organizationId&organizationUserId&token&email&organizationName', templateUrl: 'app/accounts/views/accountsOrganizationAccept.html', controller: 'accountsOrganizationAcceptController', data: { pageTitle: 'Accept Organization Invite', bodyClass: 'login-page', skipAuthorize: true } }) .state('frontend.verifyEmail', { url: '^/verify-email?userId&token', templateUrl: 'app/accounts/views/accountsVerifyEmail.html', controller: 'accountsVerifyEmailController', data: { pageTitle: 'Verifying Email', bodyClass: 'login-page', skipAuthorize: true } }); }]) .run(["$rootScope", "authService", "$state", function ($rootScope, authService, $state) { $rootScope.$on('$stateChangeSuccess', function () { $('html, body').animate({ scrollTop: 0 }, 200); }); $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { if (!toState.data || !toState.data.authorize) { if (toState.data && toState.data.skipAuthorize) { return; } if (!authService.isAuthenticated()) { return; } event.preventDefault(); $state.go('backend.user.vault'); return; } if (!authService.isAuthenticated()) { event.preventDefault(); authService.logOut(); $state.go('frontend.login.info'); return; } // user is guaranteed to be authenticated becuase of previous check if (toState.name.indexOf('backend.org.') > -1 && toParams.orgId) { // clear vault rootScope when visiting org admin section $rootScope.vaultCiphers = $rootScope.vaultFolders = $rootScope.vaultCollections = null; authService.getUserProfile().then(function (profile) { var orgs = profile.organizations; if (!orgs || !(toParams.orgId in orgs) || orgs[toParams.orgId].status !== 2 || orgs[toParams.orgId].type === 2) { event.preventDefault(); $state.go('backend.user.vault'); } }); } }); }]); angular.module('bit') .constant('constants', { rememberedEmailCookieName: 'bit.rememberedEmail', encType: { AesCbc256_B64: 0, AesCbc128_HmacSha256_B64: 1, AesCbc256_HmacSha256_B64: 2, Rsa2048_OaepSha256_B64: 3, Rsa2048_OaepSha1_B64: 4, Rsa2048_OaepSha256_HmacSha256_B64: 5, Rsa2048_OaepSha1_HmacSha256_B64: 6 }, orgUserType: { owner: 0, admin: 1, user: 2 }, orgUserStatus: { invited: 0, accepted: 1, confirmed: 2 }, twoFactorProvider: { u2f: 4, yubikey: 3, duo: 2, authenticator: 0, email: 1, remember: 5, organizationDuo: 6 }, cipherType: { login: 1, secureNote: 2, card: 3, identity: 4 }, fieldType: { text: 0, hidden: 1, boolean: 2 }, deviceType: { android: 0, ios: 1, chromeExt: 2, firefoxExt: 3, operaExt: 4, edgeExt: 5, windowsDesktop: 6, macOsDesktop: 7, linuxDesktop: 8, chrome: 9, firefox: 10, opera: 11, edge: 12, ie: 13, unknown: 14, uwp: 16, safari: 17, vivaldi: 18, vivaldiExt: 19 }, eventType: { User_LoggedIn: 1000, User_ChangedPassword: 1001, User_Enabled2fa: 1002, User_Disabled2fa: 1003, User_Recovered2fa: 1004, User_FailedLogIn: 1005, User_FailedLogIn2fa: 1006, Cipher_Created: 1100, Cipher_Updated: 1101, Cipher_Deleted: 1102, Cipher_AttachmentCreated: 1103, Cipher_AttachmentDeleted: 1104, Cipher_Shared: 1105, Cipher_UpdatedCollections: 1106, Collection_Created: 1300, Collection_Updated: 1301, Collection_Deleted: 1302, Group_Created: 1400, Group_Updated: 1401, Group_Deleted: 1402, OrganizationUser_Invited: 1500, OrganizationUser_Confirmed: 1501, OrganizationUser_Updated: 1502, OrganizationUser_Removed: 1503, OrganizationUser_UpdatedGroups: 1504, Organization_Updated: 1600 }, twoFactorProviderInfo: [ { type: 0, name: 'Authenticator App', description: 'Use an authenticator app (such as Authy or Google Authenticator) to generate time-based ' + 'verification codes.', enabled: false, active: true, free: true, image: 'authapp.png', displayOrder: 0, priority: 1, requiresUsb: false, organization: false }, { type: 3, name: 'YubiKey OTP Security Key', description: 'Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices.', enabled: false, active: true, image: 'yubico.png', displayOrder: 1, priority: 3, requiresUsb: true, organization: false }, { type: 2, name: 'Duo', description: 'Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.', enabled: false, active: true, image: 'duo.png', displayOrder: 2, priority: 2, requiresUsb: false, organization: false }, { type: 4, name: 'FIDO U2F Security Key', description: 'Use any FIDO U2F enabled security key to access your account.', enabled: false, active: true, image: 'fido.png', displayOrder: 3, priority: 4, requiresUsb: true, organization: false }, { type: 1, name: 'Email', description: 'Verification codes will be emailed to you.', enabled: false, active: true, free: true, image: 'gmail.png', displayOrder: 4, priority: 0, requiresUsb: false, organization: false }, { type: 6, name: 'Duo (Organization)', description: 'Verify with Duo Security for your organization using the Duo Mobile app, SMS, ' + 'phone call, or U2F security key.', enabled: false, active: true, image: 'duo.png', displayOrder: 1, priority: 10, requiresUsb: false, organization: true } ], plans: { free: { basePrice: 0, noAdditionalSeats: true, noPayment: true, upgradeSortOrder: -1 }, families: { basePrice: 1, annualBasePrice: 12, baseSeats: 5, noAdditionalSeats: true, annualPlanType: 'familiesAnnually', upgradeSortOrder: 1 }, teams: { basePrice: 5, annualBasePrice: 60, monthlyBasePrice: 8, baseSeats: 5, seatPrice: 2, annualSeatPrice: 24, monthlySeatPrice: 2.5, monthPlanType: 'teamsMonthly', annualPlanType: 'teamsAnnually', upgradeSortOrder: 2 }, enterprise: { seatPrice: 3, annualSeatPrice: 36, monthlySeatPrice: 4, monthPlanType: 'enterpriseMonthly', annualPlanType: 'enterpriseAnnually', upgradeSortOrder: 3 } }, storageGb: { price: 0.33, monthlyPrice: 0.50, yearlyPrice: 4 }, premium: { price: 10, yearlyPrice: 10 } }); angular .module('bit.accounts') .controller('accountsLoginController', ["$scope", "$rootScope", "$cookies", "apiService", "cryptoService", "authService", "$state", "constants", "$analytics", "$uibModal", "$timeout", "$window", "$filter", "toastr", function ($scope, $rootScope, $cookies, apiService, cryptoService, authService, $state, constants, $analytics, $uibModal, $timeout, $window, $filter, toastr) { $scope.state = $state; $scope.twoFactorProviderConstants = constants.twoFactorProvider; $scope.rememberTwoFactor = { checked: false }; var stopU2fCheck = true; $scope.returnState = $state.params.returnState; $scope.stateEmail = $state.params.email; if (!$scope.returnState && $state.params.org) { $scope.returnState = { name: 'backend.user.settingsCreateOrg', params: { plan: $state.params.org } }; } else if (!$scope.returnState && $state.params.premium) { $scope.returnState = { name: 'backend.user.settingsPremium' }; } if ($state.current.name.indexOf('twoFactor') > -1 && (!$scope.twoFactorProviders || !$scope.twoFactorProviders.length)) { $state.go('frontend.login.info', { returnState: $scope.returnState }); } var rememberedEmail = $cookies.get(constants.rememberedEmailCookieName); if (rememberedEmail || $scope.stateEmail) { $scope.model = { email: $scope.stateEmail || rememberedEmail, rememberEmail: rememberedEmail !== null }; $timeout(function () { $("#masterPassword").focus(); }); } else { $timeout(function () { $("#email").focus(); }); } var _email, _masterPassword; $scope.twoFactorProviders = null; $scope.twoFactorProvider = null; $scope.login = function (model) { $scope.loginPromise = authService.logIn(model.email, model.masterPassword).then(function (twoFactorProviders) { if (model.rememberEmail) { var cookieExpiration = new Date(); cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10); $cookies.put( constants.rememberedEmailCookieName, model.email, { expires: cookieExpiration }); } else { $cookies.remove(constants.rememberedEmailCookieName); } if (twoFactorProviders && Object.keys(twoFactorProviders).length > 0) { _email = model.email; _masterPassword = model.masterPassword; $scope.twoFactorProviders = cleanProviders(twoFactorProviders); $scope.twoFactorProvider = getDefaultProvider($scope.twoFactorProviders); $analytics.eventTrack('Logged In To Two-step'); $state.go('frontend.login.twoFactor', { returnState: $scope.returnState }).then(function () { $timeout(function () { $("#code").focus(); init(); }); }); } else { $analytics.eventTrack('Logged In'); loggedInGo(); } model.masterPassword = ''; }); }; function getDefaultProvider(twoFactorProviders) { var keys = Object.keys(twoFactorProviders); var providerType = null; var providerPriority = -1; for (var i = 0; i < keys.length; i++) { var provider = $filter('filter')(constants.twoFactorProviderInfo, { type: keys[i], active: true }); if (provider.length && provider[0].priority > providerPriority) { if (provider[0].type === constants.twoFactorProvider.u2f && !u2f.isSupported) { continue; } providerType = provider[0].type; providerPriority = provider[0].priority; } } if (providerType === null) { return null; } return parseInt(providerType); } function cleanProviders(twoFactorProviders) { if (canUseSecurityKey()) { return twoFactorProviders; } var keys = Object.keys(twoFactorProviders); for (var i = 0; i < keys.length; i++) { var provider = $filter('filter')(constants.twoFactorProviderInfo, { type: keys[i], active: true, requiresUsb: false }); if (!provider.length) { delete twoFactorProviders[keys[i]]; } } return twoFactorProviders; } // ref: https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser function canUseSecurityKey() { var mobile = false; (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) { mobile = true; } })(navigator.userAgent || navigator.vendor || window.opera); return !mobile && !navigator.userAgent.match(/iPad/i); } $scope.twoFactor = function (token) { if ($scope.twoFactorProvider === constants.twoFactorProvider.email || $scope.twoFactorProvider === constants.twoFactorProvider.authenticator) { token = token.replace(' ', ''); } $scope.twoFactorPromise = authService.logIn(_email, _masterPassword, token, $scope.twoFactorProvider, $scope.rememberTwoFactor.checked || false); $scope.twoFactorPromise.then(function () { $analytics.eventTrack('Logged In From Two-step'); loggedInGo(); }, function () { if ($scope.twoFactorProvider === constants.twoFactorProvider.u2f) { init(); } }); }; $scope.anotherMethod = function () { var modal = $uibModal.open({ animation: true, templateUrl: 'app/accounts/views/accountsTwoFactorMethods.html', controller: 'accountsTwoFactorMethodsController', resolve: { providers: function () { return $scope.twoFactorProviders; } } }); modal.result.then(function (provider) { $scope.twoFactorProvider = provider; $timeout(function () { $("#code").focus(); init(); }); }); }; $scope.sendEmail = function (doToast) { if ($scope.twoFactorProvider !== constants.twoFactorProvider.email) { return; } return cryptoService.makeKeyAndHash(_email, _masterPassword).then(function (result) { return apiService.twoFactor.sendEmailLogin({ email: _email, masterPasswordHash: result.hash }).$promise; }).then(function () { if (doToast) { toastr.success('Verification email sent to ' + $scope.twoFactorEmail + '.'); } }, function () { toastr.error('Could not send verification email.'); }); }; $scope.$on('$destroy', function () { stopU2fCheck = true; }); function loggedInGo() { if ($scope.returnState) { $state.go($scope.returnState.name, $scope.returnState.params); } else { $state.go('backend.user.vault'); } } function init() { stopU2fCheck = true; var params; if ($scope.twoFactorProvider === constants.twoFactorProvider.duo || $scope.twoFactorProvider === constants.twoFactorProvider.organizationDuo) { params = $scope.twoFactorProviders[$scope.twoFactorProvider]; $window.Duo.init({ host: params.Host, sig_request: params.Signature, submit_callback: function (theForm) { var response = $(theForm).find('input[name="sig_response"]').val(); $scope.twoFactor(response); } }); } else if ($scope.twoFactorProvider === constants.twoFactorProvider.u2f) { stopU2fCheck = false; params = $scope.twoFactorProviders[constants.twoFactorProvider.u2f]; var challenges = JSON.parse(params.Challenges); initU2f(challenges); } else if ($scope.twoFactorProvider === constants.twoFactorProvider.email) { params = $scope.twoFactorProviders[constants.twoFactorProvider.email]; $scope.twoFactorEmail = params.Email; if (Object.keys($scope.twoFactorProviders).length > 1) { $scope.sendEmail(false); } } } function initU2f(challenges) { if (stopU2fCheck) { return; } if (challenges.length < 1 || $scope.twoFactorProvider !== constants.twoFactorProvider.u2f) { return; } console.log('listening for u2f key...'); $window.u2f.sign(challenges[0].appId, challenges[0].challenge, [{ version: challenges[0].version, keyHandle: challenges[0].keyHandle }], function (data) { if ($scope.twoFactorProvider !== constants.twoFactorProvider.u2f) { return; } if (data.errorCode) { console.log(data.errorCode); $timeout(function () { initU2f(challenges); }, data.errorCode === 5 ? 0 : 1000); return; } $scope.twoFactor(JSON.stringify(data)); }, 10); } }]); angular .module('bit.accounts') .controller('accountsLogoutController', ["$scope", "authService", "$state", "$analytics", function ($scope, authService, $state, $analytics) { authService.logOut(); $analytics.eventTrack('Logged Out'); $state.go('frontend.login.info'); }]); angular .module('bit.accounts') .controller('accountsOrganizationAcceptController', ["$scope", "$state", "apiService", "authService", "toastr", "$analytics", function ($scope, $state, apiService, authService, toastr, $analytics) { $scope.state = { name: $state.current.name, params: $state.params }; if (!$state.params.organizationId || !$state.params.organizationUserId || !$state.params.token || !$state.params.email || !$state.params.organizationName) { $state.go('frontend.login.info').then(function () { toastr.error('Invalid parameters.'); }); return; } $scope.$on('$viewContentLoaded', function () { if (authService.isAuthenticated()) { $scope.accepting = true; apiService.organizationUsers.accept( { orgId: $state.params.organizationId, id: $state.params.organizationUserId }, { token: $state.params.token }, function () { $analytics.eventTrack('Accepted Invitation'); $state.go('backend.user.vault', null, { location: 'replace' }).then(function () { toastr.success('You can access this organization once an administrator confirms your membership.' + ' We\'ll send an email when that happens.', 'Invite Accepted', { timeOut: 10000 }); }); }, function () { $analytics.eventTrack('Failed To Accept Invitation'); $state.go('backend.user.vault', null, { location: 'replace' }).then(function () { toastr.error('Unable to accept invitation.', 'Error'); }); }); } else { $scope.loading = false; } }); }]); angular .module('bit.accounts') .controller('accountsPasswordHintController', ["$scope", "$rootScope", "apiService", "$analytics", function ($scope, $rootScope, apiService, $analytics) { $scope.success = false; $scope.submit = function (model) { $scope.submitPromise = apiService.accounts.postPasswordHint({ email: model.email }, function () { $analytics.eventTrack('Requested Password Hint'); $scope.success = true; }).$promise; }; }]); angular .module('bit.accounts') .controller('accountsRecoverController', ["$scope", "apiService", "cryptoService", "$analytics", function ($scope, apiService, cryptoService, $analytics) { $scope.success = false; $scope.submit = function (model) { var email = model.email.toLowerCase(); $scope.submitPromise = cryptoService.makeKeyAndHash(model.email, model.masterPassword).then(function (result) { return apiService.twoFactor.recover({ email: email, masterPasswordHash: result.hash, recoveryCode: model.code.replace(/\s/g, '').toLowerCase() }).$promise; }).then(function () { $analytics.eventTrack('Recovered 2FA'); $scope.success = true; }); }; }]); angular .module('bit.accounts') .controller('accountsRecoverDeleteController', ["$scope", "$rootScope", "apiService", "$analytics", function ($scope, $rootScope, apiService, $analytics) { $scope.success = false; $scope.submit = function (model) { $scope.submitPromise = apiService.accounts.postDeleteRecover({ email: model.email }, function () { $analytics.eventTrack('Started Delete Recovery'); $scope.success = true; }).$promise; }; }]); angular .module('bit.accounts') .controller('accountsRegisterController', ["$scope", "$location", "apiService", "cryptoService", "validationService", "$analytics", "$state", "$timeout", function ($scope, $location, apiService, cryptoService, validationService, $analytics, $state, $timeout) { var params = $location.search(); var stateParams = $state.params; $scope.createOrg = stateParams.org; if (!stateParams.returnState && stateParams.org) { $scope.returnState = { name: 'backend.user.settingsCreateOrg', params: { plan: $state.params.org } }; } else if (!stateParams.returnState && stateParams.premium) { $scope.returnState = { name: 'backend.user.settingsPremium', params: { plan: $state.params.org } }; } else { $scope.returnState = stateParams.returnState; } $scope.success = false; $scope.model = { email: params.email ? params.email : stateParams.email }; $scope.readOnlyEmail = stateParams.email !== null; $timeout(function () { if ($scope.model.email) { $("#name").focus(); } else { $("#email").focus(); } }); $scope.registerPromise = null; $scope.register = function (form) { var error = false; if ($scope.model.masterPassword.length < 8) { validationService.addError(form, 'MasterPassword', 'Master password must be at least 8 characters long.', true); error = true; } if ($scope.model.masterPassword !== $scope.model.confirmMasterPassword) { validationService.addError(form, 'ConfirmMasterPassword', 'Master password confirmation does not match.', true); error = true; } if (error) { return; } var email = $scope.model.email.toLowerCase(); var makeResult, encKey; $scope.registerPromise = cryptoService.makeKeyAndHash(email, $scope.model.masterPassword).then(function (result) { makeResult = result; encKey = cryptoService.makeEncKey(result.key); return cryptoService.makeKeyPair(encKey.encKey); }).then(function (result) { var request = { name: $scope.model.name, email: email, masterPasswordHash: makeResult.hash, masterPasswordHint: $scope.model.masterPasswordHint, key: encKey.encKeyEnc, keys: { publicKey: result.publicKey, encryptedPrivateKey: result.privateKeyEnc } }; return apiService.accounts.register(request).$promise; }, function (errors) { validationService.addError(form, null, 'Problem generating keys.', true); return false; }).then(function (result) { if (result === false) { return; } $scope.success = true; $analytics.eventTrack('Registered'); }); }; }]); angular .module('bit.accounts') .controller('accountsTwoFactorMethodsController', ["$scope", "$uibModalInstance", "$analytics", "providers", "constants", function ($scope, $uibModalInstance, $analytics, providers, constants) { $analytics.eventTrack('accountsTwoFactorMethodsController', { category: 'Modal' }); $scope.providers = []; if (providers.hasOwnProperty(constants.twoFactorProvider.organizationDuo)) { add(constants.twoFactorProvider.organizationDuo); } if (providers.hasOwnProperty(constants.twoFactorProvider.authenticator)) { add(constants.twoFactorProvider.authenticator); } if (providers.hasOwnProperty(constants.twoFactorProvider.yubikey)) { add(constants.twoFactorProvider.yubikey); } if (providers.hasOwnProperty(constants.twoFactorProvider.email)) { add(constants.twoFactorProvider.email); } if (providers.hasOwnProperty(constants.twoFactorProvider.duo)) { add(constants.twoFactorProvider.duo); } if (providers.hasOwnProperty(constants.twoFactorProvider.u2f) && u2f.isSupported) { add(constants.twoFactorProvider.u2f); } $scope.choose = function (provider) { $uibModalInstance.close(provider.type); }; $scope.close = function () { $uibModalInstance.dismiss('close'); }; function add(type) { for (var i = 0; i < constants.twoFactorProviderInfo.length; i++) { if (constants.twoFactorProviderInfo[i].type === type) { $scope.providers.push(constants.twoFactorProviderInfo[i]); } } } }]); angular .module('bit.accounts') .controller('accountsVerifyEmailController', ["$scope", "$state", "apiService", "toastr", "$analytics", function ($scope, $state, apiService, toastr, $analytics) { if (!$state.params.userId || !$state.params.token) { $state.go('frontend.login.info').then(function () { toastr.error('Invalid parameters.'); }); return; } $scope.$on('$viewContentLoaded', function () { apiService.accounts.verifyEmailToken({}, { token: $state.params.token, userId: $state.params.userId }, function () { $analytics.eventTrack('Verified Email'); $state.go('frontend.login.info', null, { location: 'replace' }).then(function () { toastr.success('Your email has been verified. Thank you.', 'Success'); }); }, function () { $state.go('frontend.login.info', null, { location: 'replace' }).then(function () { toastr.error('Unable to verify email.', 'Error'); }); }); }); }]); angular .module('bit.accounts') .controller('accountsVerifyRecoverDeleteController', ["$scope", "$state", "apiService", "toastr", "$analytics", function ($scope, $state, apiService, toastr, $analytics) { if (!$state.params.userId || !$state.params.token || !$state.params.email) { $state.go('frontend.login.info').then(function () { toastr.error('Invalid parameters.'); }); return; } $scope.email = $state.params.email; $scope.delete = function () { if (!confirm('Are you sure you want to delete this account? This cannot be undone.')) { return; } $scope.deleting = true; apiService.accounts.postDeleteRecoverToken({}, { token: $state.params.token, userId: $state.params.userId }, function () { $analytics.eventTrack('Recovered Delete'); $state.go('frontend.login.info', null, { location: 'replace' }).then(function () { toastr.success('Your account has been deleted. You can register a new account again if you like.', 'Success'); }); }, function () { $state.go('frontend.login.info', null, { location: 'replace' }).then(function () { toastr.error('Unable to delete account.', 'Error'); }); }); }; }]); angular .module('bit.directives') .directive('apiField', function () { var linkFn = function (scope, element, attrs, ngModel) { ngModel.$registerApiError = registerApiError; ngModel.$validators.apiValidate = apiValidator; function apiValidator() { ngModel.$setValidity('api', true); return true; } function registerApiError() { ngModel.$setValidity('api', false); } }; return { require: 'ngModel', restrict: 'A', compile: function (elem, attrs) { if (!attrs.name || attrs.name === '') { throw 'api-field element does not have a valid name attribute'; } return linkFn; } }; }); angular .module('bit.directives') .directive('apiForm', ["$rootScope", "validationService", "$timeout", function ($rootScope, validationService, $timeout) { return { require: 'form', restrict: 'A', link: function (scope, element, attrs, formCtrl) { var watchPromise = attrs.apiForm || null; if (watchPromise !== void 0) { scope.$watch(watchPromise, formSubmitted.bind(null, formCtrl, scope)); } } }; function formSubmitted(form, scope, promise) { if (!promise || !promise.then) { return; } // reset errors form.$errors = null; // start loading form.$loading = true; promise.then(function success(response) { $timeout(function () { form.$loading = false; }); }, function failure(reason) { $timeout(function () { form.$loading = false; if (typeof reason === 'string') { validationService.addError(form, null, reason, true); } else { validationService.addErrors(form, reason); } scope.$broadcast('show-errors-check-validity'); $('html, body').animate({ scrollTop: 0 }, 200); }); }); } }]); angular .module('bit.directives') .directive('fallbackSrc', function () { return function (scope, element, attrs) { var el = $(element); el.bind('error', function (event) { el.attr('src', attrs.fallbackSrc); }); }; }); angular .module('bit.directives') // adaptation of https://github.com/uttesh/ngletteravatar .directive('letterAvatar', function () { // ref: http://stackoverflow.com/a/16348977/1090359 function stringToColor(str) { var hash = 0, i = 0; for (i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } var color = '#'; for (i = 0; i < 3; i++) { var value = (hash >> (i * 8)) & 0xFF; color += ('00' + value.toString(16)).substr(-2); } return color; } function getFirstLetters(data, count) { var parts = data.split(' '); if (parts && parts.length > 1) { var text = ''; for (var i = 0; i < count; i++) { text += parts[i].substr(0, 1); } return text; } return null; } function getSvg(width, height, color) { var svgTag = angular.element('') .attr({ 'xmlns': 'http://www.w3.org/2000/svg', 'pointer-events': 'none', 'width': width, 'height': height }) .css({ 'background-color': color, 'width': width + 'px', 'height': height + 'px' }); return svgTag; } function getCharText(character, textColor, fontFamily, fontWeight, fontsize) { var textTag = angular.element('') .attr({ 'y': '50%', 'x': '50%', 'dy': '0.35em', 'pointer-events': 'auto', 'fill': textColor, 'font-family': fontFamily }) .text(character) .css({ 'font-weight': fontWeight, 'font-size': fontsize + 'px', }); return textTag; } return { restrict: 'AE', replace: true, scope: { data: '@' }, link: function (scope, element, attrs) { var params = { charCount: attrs.charcount || 2, data: attrs.data, textColor: attrs.textcolor || '#ffffff', bgColor: attrs.bgcolor, height: attrs.avheight || 45, width: attrs.avwidth || 45, fontSize: attrs.fontsize || 20, fontWeight: attrs.fontweight || 300, fontFamily: attrs.fontfamily || 'Open Sans, HelveticaNeue-Light, Helvetica Neue Light, ' + 'Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif', round: attrs.round || 'true', dynamic: attrs.dynamic || 'true', class: attrs.avclass || '', border: attrs.avborder || 'false', borderStyle: attrs.borderstyle || '3px solid white' }; if (params.dynamic === 'true') { scope.$watch('data', function () { generateLetterAvatar(); }); } else { generateLetterAvatar(); } function generateLetterAvatar() { var c = null, upperData = scope.data.toUpperCase(); if (params.charCount > 1) { c = getFirstLetters(upperData, params.charCount); } if (!c) { c = upperData.substr(0, params.charCount); } var cobj = getCharText(c, params.textColor, params.fontFamily, params.fontWeight, params.fontSize); var color = params.bgColor ? params.bgColor : stringToColor(upperData); var svg = getSvg(params.width, params.height, color); svg.append(cobj); var lvcomponent = angular.element('
').append(svg).html(); var svgHtml = window.btoa(unescape(encodeURIComponent(lvcomponent))); var src = 'data:image/svg+xml;base64,' + svgHtml; var img = angular.element('').attr({ src: src, title: scope.data }); if (params.round === 'true') { img.css('border-radius', '50%'); } if (params.border === 'true') { img.css('border', params.borderStyle); } if (params.class) { img.addClass(params.class); } if (params.dynamic === 'true') { element.empty(); element.append(img); } else { element.replaceWith(img); } } } }; }); angular .module('bit.directives') .directive('masterPassword', ["cryptoService", "authService", function (cryptoService, authService) { return { require: 'ngModel', restrict: 'A', link: function (scope, elem, attr, ngModel) { authService.getUserProfile().then(function (profile) { // For DOM -> model validation ngModel.$parsers.unshift(function (value) { if (!value) { return undefined; } return cryptoService.makeKey(value, profile.email).then(function (result) { var valid = result.keyB64 === cryptoService.getKey().keyB64; ngModel.$setValidity('masterPassword', valid); return valid ? value : undefined; }); }); // For model -> DOM validation ngModel.$formatters.unshift(function (value) { if (!value) { return undefined; } return cryptoService.makeKey(value, profile.email).then(function (result) { var valid = result.keyB64 === cryptoService.getKey().keyB64; ngModel.$setValidity('masterPassword', valid); return value; }); }); }); } }; }]); angular .module('bit.directives') .directive('pageTitle', ["$rootScope", "$timeout", "appSettings", function ($rootScope, $timeout, appSettings) { return { link: function (scope, element) { var listener = function (event, toState, toParams, fromState, fromParams) { // Default title var title = 'Bitwarden Web Vault'; if (toState.data && toState.data.pageTitle) { title = toState.data.pageTitle + ' - ' + title; } $timeout(function () { element.text(title); }); }; $rootScope.$on('$stateChangeStart', listener); } }; }]); angular .module('bit.directives') .directive('passwordMeter', function () { return { template: '
{{value}}%
', restrict: 'A', scope: { password: '=passwordMeter', username: '=passwordMeterUsername', outerClass: '@?' }, link: function (scope) { var measureStrength = function (username, password) { if (!password || password === username) { return 0; } var strength = password.length; if (username && username !== '') { if (username.indexOf(password) !== -1) strength -= 15; if (password.indexOf(username) !== -1) strength -= username.length; } if (password.length > 0 && password.length <= 4) strength += password.length; else if (password.length >= 5 && password.length <= 7) strength += 6; else if (password.length >= 8 && password.length <= 15) strength += 12; else if (password.length >= 16) strength += 18; if (password.match(/[a-z]/)) strength += 1; if (password.match(/[A-Z]/)) strength += 5; if (password.match(/\d/)) strength += 5; if (password.match(/.*\d.*\d.*\d/)) strength += 5; if (password.match(/[!,@,#,$,%,^,&,*,?,_,~]/)) strength += 5; if (password.match(/.*[!,@,#,$,%,^,&,*,?,_,~].*[!,@,#,$,%,^,&,*,?,_,~]/)) strength += 5; if (password.match(/(?=.*[a-z])(?=.*[A-Z])/)) strength += 2; if (password.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)) strength += 2; if (password.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!,@,#,$,%,^,&,*,?,_,~])/)) strength += 2; strength = Math.round(strength * 2); return Math.max(0, Math.min(100, strength)); }; var getClass = function (strength) { switch (Math.round(strength / 33)) { case 0: case 1: return 'danger'; case 2: return 'warning'; case 3: return 'success'; } }; var updateMeter = function (scope) { scope.value = measureStrength(scope.username, scope.password); scope.valueClass = getClass(scope.value); }; scope.$watch('password', function () { updateMeter(scope); }); scope.$watch('username', function () { updateMeter(scope); }); }, }; }); angular .module('bit.directives') .directive('passwordViewer', function () { return { restrict: 'A', link: function (scope, element, attr) { var passwordViewer = attr.passwordViewer; if (!passwordViewer) { return; } element.onclick = function (event) { }; element.on('click', function (event) { var passwordElement = $(passwordViewer); if (passwordElement && passwordElement.attr('type') === 'password') { element.removeClass('fa-eye').addClass('fa-eye-slash'); passwordElement.attr('type', 'text'); } else if (passwordElement && passwordElement.attr('type') === 'text') { element.removeClass('fa-eye-slash').addClass('fa-eye'); passwordElement.attr('type', 'password'); } }); } }; }); angular .module('bit.directives') // ref: https://stackoverflow.com/a/14165848/1090359 .directive('stopClick', function () { return function (scope, element, attrs) { $(element).click(function (event) { event.preventDefault(); }); }; }); angular .module('bit.directives') .directive('stopProp', function () { return function (scope, element, attrs) { $(element).click(function (event) { event.stopPropagation(); }); }; }); angular .module('bit.directives') .directive('totp', ["$timeout", "$q", function ($timeout, $q) { return { template: '
' + '{{sec}}' + '' + '' + '{{codeFormatted}}' + '' + '' + '
', restrict: 'A', scope: { key: '=totp' }, link: function (scope) { var interval = null; var Totp = function () { var b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; var leftpad = function (s, l, p) { if (l + 1 >= s.length) { s = Array(l + 1 - s.length).join(p) + s; } return s; }; var dec2hex = function (d) { return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); }; var hex2dec = function (s) { return parseInt(s, 16); }; var hex2bytes = function (s) { var bytes = new Uint8Array(s.length / 2); for (var i = 0; i < s.length; i += 2) { bytes[i / 2] = parseInt(s.substr(i, 2), 16); } return bytes; }; var buff2hex = function (buff) { var bytes = new Uint8Array(buff); var hex = []; for (var i = 0; i < bytes.length; i++) { hex.push((bytes[i] >>> 4).toString(16)); hex.push((bytes[i] & 0xF).toString(16)); } return hex.join(''); }; var b32tohex = function (s) { s = s.toUpperCase(); var cleanedInput = ''; var i; for (i = 0; i < s.length; i++) { if (b32Chars.indexOf(s[i]) < 0) { continue; } cleanedInput += s[i]; } s = cleanedInput; var bits = ''; var hex = ''; for (i = 0; i < s.length; i++) { var byteIndex = b32Chars.indexOf(s.charAt(i)); if (byteIndex < 0) { continue; } bits += leftpad(byteIndex.toString(2), 5, '0'); } for (i = 0; i + 4 <= bits.length; i += 4) { var chunk = bits.substr(i, 4); hex = hex + parseInt(chunk, 2).toString(16); } return hex; }; var b32tobytes = function (s) { return hex2bytes(b32tohex(s)); }; var sign = function (keyBytes, timeBytes) { return window.crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: { name: 'SHA-1' } }, false, ['sign']).then(function (key) { return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-1' } }, key, timeBytes); }).then(function (signature) { return buff2hex(signature); }).catch(function (err) { return null; }); }; this.getCode = function (keyb32) { var epoch = Math.round(new Date().getTime() / 1000.0); var timeHex = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); var timeBytes = hex2bytes(timeHex); var keyBytes = b32tobytes(keyb32); if (!keyBytes.length || !timeBytes.length) { return $q(function (resolve, reject) { resolve(null); }); } return sign(keyBytes, timeBytes).then(function (hashHex) { if (!hashHex) { return null; } var offset = hex2dec(hashHex.substring(hashHex.length - 1)); var otp = (hex2dec(hashHex.substr(offset * 2, 8)) & hex2dec('7fffffff')) + ''; otp = (otp).substr(otp.length - 6, 6); return otp; }); }; }; var totp = new Totp(); var updateCode = function (scope) { totp.getCode(scope.key).then(function (code) { $timeout(function () { if (code) { scope.codeFormatted = code.substring(0, 3) + ' ' + code.substring(3); scope.code = code; } else { scope.code = null; if (interval) { clearInterval(interval); } } }); }); }; var tick = function (scope) { $timeout(function () { var epoch = Math.round(new Date().getTime() / 1000.0); var mod = epoch % 30; var sec = 30 - mod; scope.sec = sec; scope.dash = (2.62 * mod).toFixed(2); scope.low = sec <= 7; if (mod === 0) { updateCode(scope); } }); }; scope.$watch('key', function () { if (!scope.key) { scope.code = null; if (interval) { clearInterval(interval); } return; } updateCode(scope); tick(scope); if (interval) { clearInterval(interval); } interval = setInterval(function () { tick(scope); }, 1000); }); scope.$on('$destroy', function () { if (interval) { clearInterval(interval); } }); scope.clipboardError = function (e) { alert('Your web browser does not support easy clipboard copying.'); }; }, }; }]); angular .module('bit.filters') .filter('enumLabelClass', function () { return function (input, name) { if (typeof input !== 'number') { return input.toString(); } var output; switch (name) { case 'OrgUserStatus': switch (input) { case 0: output = 'label-default'; break; case 1: output = 'label-warning'; break; case 2: /* falls through */ default: output = 'label-success'; } break; default: output = 'label-default'; } return output; }; }); angular .module('bit.filters') .filter('enumName', function () { return function (input, name) { if (typeof input !== 'number') { return input.toString(); } var output; switch (name) { case 'OrgUserStatus': switch (input) { case 0: output = 'Invited'; break; case 1: output = 'Accepted'; break; case 2: /* falls through */ default: output = 'Confirmed'; } break; case 'OrgUserType': switch (input) { case 0: output = 'Owner'; break; case 1: output = 'Admin'; break; case 2: /* falls through */ default: output = 'User'; } break; default: output = input.toString(); } return output; }; }); angular .module('bit.global') .controller('mainController', ["$scope", "$state", "authService", "appSettings", "toastr", "$window", "$document", "cryptoService", "$uibModal", "apiService", function ($scope, $state, authService, appSettings, toastr, $window, $document, cryptoService, $uibModal, apiService) { var vm = this; vm.skinClass = appSettings.selfHosted ? 'skin-blue-light' : 'skin-blue'; vm.bodyClass = ''; vm.usingControlSidebar = vm.openControlSidebar = false; vm.searchVaultText = null; vm.version = appSettings.version; vm.outdatedBrowser = $window.navigator.userAgent.indexOf('MSIE') !== -1 || $window.navigator.userAgent.indexOf('SamsungBrowser') !== -1; $scope.currentYear = new Date().getFullYear(); $scope.$on('$viewContentLoaded', function () { authService.getUserProfile().then(function (profile) { vm.userProfile = profile; }); if ($.AdminLTE) { if ($.AdminLTE.layout) { $.AdminLTE.layout.fix(); $.AdminLTE.layout.fixSidebar(); } if ($.AdminLTE.pushMenu) { $.AdminLTE.pushMenu.expandOnHover(); } $document.off('click', '.sidebar li a'); } }); $scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { vm.usingEncKey = !!cryptoService.getEncKey(); vm.searchVaultText = null; if (toState.data.bodyClass) { vm.bodyClass = toState.data.bodyClass; return; } else { vm.bodyClass = ''; } vm.usingControlSidebar = !!toState.data.controlSidebar; vm.openControlSidebar = vm.usingControlSidebar && $document.width() > 768; }); $scope.addCipher = function () { $scope.$broadcast('vaultAddCipher'); }; $scope.addFolder = function () { $scope.$broadcast('vaultAddFolder'); }; $scope.addOrganizationCipher = function () { $scope.$broadcast('organizationVaultAddCipher'); }; $scope.addOrganizationCollection = function () { $scope.$broadcast('organizationCollectionsAdd'); }; $scope.inviteOrganizationUser = function () { $scope.$broadcast('organizationPeopleInvite'); }; $scope.addOrganizationGroup = function () { $scope.$broadcast('organizationGroupsAdd'); }; $scope.updateKey = function () { $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsUpdateKey.html', controller: 'settingsUpdateKeyController' }); }; $scope.verifyEmail = function () { if ($scope.sendingVerify) { return; } $scope.sendingVerify = true; apiService.accounts.verifyEmail({}, null).$promise.then(function () { toastr.success('Verification email sent.'); $scope.sendingVerify = false; $scope.verifyEmailSent = true; }).catch(function () { toastr.success('Verification email failed.'); $scope.sendingVerify = false; }); }; $scope.updateBrowser = function () { $window.open('https://browser-update.org/update.html', '_blank'); }; // Append dropdown menu somewhere else var bodyScrollbarWidth, appendedDropdownMenu, appendedDropdownMenuParent; var dropdownHelpers = { scrollbarWidth: function () { if (!bodyScrollbarWidth) { var bodyElem = $('body'); bodyElem.addClass('bit-position-body-scrollbar-measure'); bodyScrollbarWidth = $window.innerWidth - bodyElem[0].clientWidth; bodyScrollbarWidth = isFinite(bodyScrollbarWidth) ? bodyScrollbarWidth : 0; bodyElem.removeClass('bit-position-body-scrollbar-measure'); } return bodyScrollbarWidth; }, scrollbarInfo: function () { return { width: dropdownHelpers.scrollbarWidth(), visible: $document.height() > $($window).height() }; } }; $(window).on('show.bs.dropdown', function (e) { /*jshint -W120 */ var target = appendedDropdownMenuParent = $(e.target); var appendTo = target.data('appendTo'); if (!appendTo) { return true; } appendedDropdownMenu = target.find('.dropdown-menu'); var appendToEl = $(appendTo); appendToEl.append(appendedDropdownMenu.detach()); var offset = target.offset(); var css = { display: 'block', top: offset.top + target.outerHeight() - (appendTo !== 'body' ? $(window).scrollTop() : 0) }; if (appendedDropdownMenu.hasClass('dropdown-menu-right')) { var scrollbarInfo = dropdownHelpers.scrollbarInfo(); var scrollbarWidth = 0; if (scrollbarInfo.visible && scrollbarInfo.width) { scrollbarWidth = scrollbarInfo.width; } css.right = $window.innerWidth - scrollbarWidth - (offset.left + target.prop('offsetWidth')) + 'px'; css.left = 'auto'; } else { css.left = offset.left + 'px'; css.right = 'auto'; } appendedDropdownMenu.css(css); }); $(window).on('hide.bs.dropdown', function (e) { if (!appendedDropdownMenu) { return true; } $(e.target).append(appendedDropdownMenu.detach()); appendedDropdownMenu.hide(); appendedDropdownMenu = null; appendedDropdownMenuParent = null; }); $scope.$on('removeAppendedDropdownMenu', function (event, args) { if (!appendedDropdownMenu && !appendedDropdownMenuParent) { return true; } appendedDropdownMenuParent.append(appendedDropdownMenu.detach()); appendedDropdownMenu.hide(); appendedDropdownMenu = null; appendedDropdownMenuParent = null; }); }]); angular .module('bit.global') .controller('paidOrgRequiredController', ["$scope", "$state", "$uibModalInstance", "$analytics", "$uibModalStack", "orgId", "constants", "authService", function ($scope, $state, $uibModalInstance, $analytics, $uibModalStack, orgId, constants, authService) { $analytics.eventTrack('paidOrgRequiredController', { category: 'Modal' }); authService.getUserProfile().then(function (profile) { $scope.admin = profile.organizations[orgId].type !== constants.orgUserType.user; }); $scope.go = function () { if (!$scope.admin) { return; } $analytics.eventTrack('Get Paid Org'); $state.go('backend.org.billing', { orgId: orgId }).then(function () { $uibModalStack.dismissAll(); }); }; $scope.close = function () { $uibModalInstance.dismiss('close'); }; }]); angular .module('bit.global') .controller('premiumRequiredController', ["$scope", "$state", "$uibModalInstance", "$analytics", "$uibModalStack", function ($scope, $state, $uibModalInstance, $analytics, $uibModalStack) { $analytics.eventTrack('premiumRequiredController', { category: 'Modal' }); $scope.go = function () { $analytics.eventTrack('Get Premium'); $state.go('backend.user.settingsPremium').then(function () { $uibModalStack.dismissAll(); }); }; $scope.close = function () { $uibModalInstance.dismiss('close'); }; }]); angular .module('bit.global') .controller('sideNavController', ["$scope", "$state", "authService", "toastr", "$analytics", "constants", "appSettings", function ($scope, $state, authService, toastr, $analytics, constants, appSettings) { $scope.$state = $state; $scope.params = $state.params; $scope.orgs = []; $scope.name = ''; if(appSettings.selfHosted) { $scope.orgIconBgColor = '#ffffff'; $scope.orgIconBorder = '3px solid #a0a0a0'; $scope.orgIconTextColor = '#333333'; } else { $scope.orgIconBgColor = '#2c3b41'; $scope.orgIconBorder = '3px solid #1a2226'; $scope.orgIconTextColor = '#ffffff'; } authService.getUserProfile().then(function (userProfile) { $scope.name = userProfile.extended && userProfile.extended.name ? userProfile.extended.name : userProfile.email; if (!userProfile.organizations) { return; } if ($state.includes('backend.org') && ($state.params.orgId in userProfile.organizations)) { $scope.orgProfile = userProfile.organizations[$state.params.orgId]; } else { var orgs = []; for (var orgId in userProfile.organizations) { if (userProfile.organizations.hasOwnProperty(orgId) && (userProfile.organizations[orgId].enabled || userProfile.organizations[orgId].type < 2)) { // 2 = User orgs.push(userProfile.organizations[orgId]); } } $scope.orgs = orgs; } }); $scope.viewOrganization = function (org) { if (org.type === constants.orgUserType.user) { toastr.error('You cannot manage this organization.'); return; } $analytics.eventTrack('View Organization From Side Nav'); $state.go('backend.org.dashboard', { orgId: org.id }); }; $scope.isOrgOwner = function (org) { return org && org.type === constants.orgUserType.owner; }; }]); angular .module('bit.global') .controller('topNavController', ["$scope", function ($scope) { $scope.toggleControlSidebar = function () { var bod = $('body'); if (!bod.hasClass('control-sidebar-open')) { bod.addClass('control-sidebar-open'); } else { bod.removeClass('control-sidebar-open'); } }; }]); angular .module('bit.organization') .controller('organizationBillingAdjustSeatsController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "add", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, add) { $analytics.eventTrack('organizationBillingAdjustSeatsController', { category: 'Modal' }); $scope.add = add; $scope.seatAdjustment = 0; $scope.submit = function () { var request = { seatAdjustment: $scope.seatAdjustment }; if (!add) { request.seatAdjustment *= -1; } $scope.submitPromise = apiService.organizations.putSeat({ id: $state.params.orgId }, request) .$promise.then(function (response) { if (add) { $analytics.eventTrack('Added Seats'); toastr.success('You have added ' + $scope.seatAdjustment + ' seats.'); } else { $analytics.eventTrack('Removed Seats'); toastr.success('You have removed ' + $scope.seatAdjustment + ' seats.'); } $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationBillingAdjustStorageController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "add", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, add) { $analytics.eventTrack('organizationBillingAdjustStorageController', { category: 'Modal' }); $scope.add = add; $scope.storageAdjustment = 0; $scope.submit = function () { var request = { storageGbAdjustment: $scope.storageAdjustment }; if (!add) { request.storageGbAdjustment *= -1; } $scope.submitPromise = apiService.organizations.putStorage({ id: $state.params.orgId }, request) .$promise.then(function (response) { if (add) { $analytics.eventTrack('Added Organization Storage'); toastr.success('You have added ' + $scope.storageAdjustment + ' GB.'); } else { $analytics.eventTrack('Removed Organization Storage'); toastr.success('You have removed ' + $scope.storageAdjustment + ' GB.'); } $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationBillingChangePaymentController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "existingPaymentMethod", "stripe", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, existingPaymentMethod /* jshint ignore:start */ , stripe /* jshint ignore:end */ ) { $analytics.eventTrack('organizationBillingChangePaymentController', { category: 'Modal' }); $scope.existingPaymentMethod = existingPaymentMethod; $scope.paymentMethod = 'card'; $scope.showPaymentOptions = true; $scope.hidePaypal = true; $scope.card = {}; $scope.bank = {}; $scope.changePaymentMethod = function (val) { $scope.paymentMethod = val; }; $scope.submit = function () { var stripeReq = null; if ($scope.paymentMethod === 'card') { stripeReq = stripe.card.createToken($scope.card); } else if ($scope.paymentMethod === 'bank') { $scope.bank.currency = 'USD'; $scope.bank.country = 'US'; stripeReq = stripe.bankAccount.createToken($scope.bank); } else { return; } $scope.submitPromise = stripeReq.then(function (response) { var request = { paymentToken: response.id }; return apiService.organizations.putPayment({ id: $state.params.orgId }, request).$promise; }, function (err) { throw err.message; }).then(function (response) { $scope.card = null; if (existingPaymentMethod) { $analytics.eventTrack('Changed Organization Payment Method'); toastr.success('You have changed your payment method.'); } else { $analytics.eventTrack('Added Organization Payment Method'); toastr.success('You have added a payment method.'); } $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationBillingChangePlanController', ["$scope", "$state", "apiService", "$uibModalInstance", "toastr", "$analytics", function ($scope, $state, apiService, $uibModalInstance, toastr, $analytics) { $analytics.eventTrack('organizationBillingChangePlanController', { category: 'Modal' }); $scope.submit = function () { }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationBillingController', ["$scope", "apiService", "$state", "$uibModal", "toastr", "$analytics", "appSettings", "tokenService", "$window", function ($scope, apiService, $state, $uibModal, toastr, $analytics, appSettings, tokenService, $window) { $scope.selfHosted = appSettings.selfHosted; $scope.charges = []; $scope.paymentSource = null; $scope.plan = null; $scope.subscription = null; $scope.loading = true; var license = null; $scope.expiration = null; $scope.$on('$viewContentLoaded', function () { load(); }); $scope.changePayment = function () { if ($scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingChangePayment.html', controller: 'organizationBillingChangePaymentController', resolve: { existingPaymentMethod: function () { return $scope.paymentSource ? $scope.paymentSource.description : null; } } }); modal.result.then(function () { load(); }); }; $scope.changePlan = function () { if ($scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationBillingChangePlan.html', controller: 'organizationBillingChangePlanController', resolve: { plan: function () { return $scope.plan; } } }); modal.result.then(function () { load(); }); }; $scope.adjustSeats = function (add) { if ($scope.selfHosted || !$scope.canAdjustSeats) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationBillingAdjustSeats.html', controller: 'organizationBillingAdjustSeatsController', resolve: { add: function () { return add; } } }); modal.result.then(function () { load(); }); }; $scope.adjustStorage = function (add) { if ($scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html', controller: 'organizationBillingAdjustStorageController', resolve: { add: function () { return add; } } }); modal.result.then(function () { load(); }); }; $scope.verifyBank = function () { if ($scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationBillingVerifyBank.html', controller: 'organizationBillingVerifyBankController' }); modal.result.then(function () { load(); }); }; $scope.cancel = function () { if ($scope.selfHosted) { return; } if (!confirm('Are you sure you want to cancel? All users will lose access to the organization ' + 'at the end of this billing cycle.')) { return; } apiService.organizations.putCancel({ id: $state.params.orgId }, {}) .$promise.then(function (response) { $analytics.eventTrack('Canceled Plan'); toastr.success('Organization subscription has been canceled.'); load(); }); }; $scope.reinstate = function () { if ($scope.selfHosted) { return; } if (!confirm('Are you sure you want to remove the cancellation request and reinstate this organization?')) { return; } apiService.organizations.putReinstate({ id: $state.params.orgId }, {}) .$promise.then(function (response) { $analytics.eventTrack('Reinstated Plan'); toastr.success('Organization cancellation request has been removed.'); load(); }); }; $scope.updateLicense = function () { if (!$scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingUpdateLicense.html', controller: 'organizationBillingUpdateLicenseController' }); modal.result.then(function () { load(); }); }; $scope.license = function () { if ($scope.selfHosted) { return; } var installationId = prompt("Enter your installation id"); if (!installationId || installationId === '') { return; } apiService.organizations.getLicense({ id: $state.params.orgId, installationId: installationId }, function (license) { var licenseString = JSON.stringify(license, null, 2); var licenseBlob = new Blob([licenseString]); // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(licenseBlob, 'bitwarden_organization_license.json'); } else { var a = window.document.createElement('a'); a.href = window.URL.createObjectURL(licenseBlob, { type: 'text/plain' }); a.download = 'bitwarden_organization_license.json'; document.body.appendChild(a); // IE: "Access is denied". // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access a.click(); document.body.removeChild(a); } }, function (err) { if (err.status === 400) { toastr.error("Invalid installation id."); } else { toastr.error("Unable to generate license."); } }); }; $scope.viewInvoice = function (charge) { if ($scope.selfHosted) { return; } var url = appSettings.apiUri + '/organizations/' + $state.params.orgId + '/billing-invoice/' + charge.invoiceId + '?access_token=' + tokenService.getToken(); $window.open(url); }; function load() { apiService.organizations.getBilling({ id: $state.params.orgId }, function (org) { $scope.loading = false; $scope.noSubscription = org.PlanType === 0; $scope.canAdjustSeats = org.PlanType > 1; var i = 0; $scope.expiration = org.Expiration; license = org.License; $scope.plan = { name: org.Plan, type: org.PlanType, seats: org.Seats }; $scope.storage = null; if ($scope && org.MaxStorageGb) { $scope.storage = { currentGb: org.StorageGb || 0, maxGb: org.MaxStorageGb, currentName: org.StorageName || '0 GB' }; $scope.storage.percentage = +(100 * ($scope.storage.currentGb / $scope.storage.maxGb)).toFixed(2); } $scope.subscription = null; if (org.Subscription) { $scope.subscription = { trialEndDate: org.Subscription.TrialEndDate, cancelledDate: org.Subscription.CancelledDate, status: org.Subscription.Status, cancelled: org.Subscription.Cancelled, markedForCancel: !org.Subscription.Cancelled && org.Subscription.CancelAtEndDate }; } $scope.nextInvoice = null; if (org.UpcomingInvoice) { $scope.nextInvoice = { date: org.UpcomingInvoice.Date, amount: org.UpcomingInvoice.Amount }; } if (org.Subscription && org.Subscription.Items) { $scope.subscription.items = []; for (i = 0; i < org.Subscription.Items.length; i++) { $scope.subscription.items.push({ amount: org.Subscription.Items[i].Amount, name: org.Subscription.Items[i].Name, interval: org.Subscription.Items[i].Interval, qty: org.Subscription.Items[i].Quantity }); } } $scope.paymentSource = null; if (org.PaymentSource) { $scope.paymentSource = { type: org.PaymentSource.Type, description: org.PaymentSource.Description, cardBrand: org.PaymentSource.CardBrand, needsVerification: org.PaymentSource.NeedsVerification }; } var charges = []; for (i = 0; i < org.Charges.length; i++) { charges.push({ date: org.Charges[i].CreatedDate, paymentSource: org.Charges[i].PaymentSource ? org.Charges[i].PaymentSource.Description : '-', amount: org.Charges[i].Amount, status: org.Charges[i].Status, failureMessage: org.Charges[i].FailureMessage, refunded: org.Charges[i].Refunded, partiallyRefunded: org.Charges[i].PartiallyRefunded, refundedAmount: org.Charges[i].RefundedAmount, invoiceId: org.Charges[i].InvoiceId }); } $scope.charges = charges; }); } }]); angular .module('bit.organization') .controller('organizationBillingUpdateLicenseController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "validationService", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, validationService) { $analytics.eventTrack('organizationBillingUpdateLicenseController', { category: 'Modal' }); $scope.submit = function (form) { var fileEl = document.getElementById('file'); var files = fileEl.files; if (!files || !files.length) { validationService.addError(form, 'file', 'Select a license file.', true); return; } var fd = new FormData(); fd.append('license', files[0]); $scope.submitPromise = apiService.organizations.putLicense({ id: $state.params.orgId }, fd) .$promise.then(function (response) { $analytics.eventTrack('Updated License'); toastr.success('You have updated your license.'); $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationBillingVerifyBankController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr) { $analytics.eventTrack('organizationBillingVerifyBankController', { category: 'Modal' }); $scope.submit = function () { var request = { amount1: $scope.amount1, amount2: $scope.amount2 }; $scope.submitPromise = apiService.organizations.postVerifyBank({ id: $state.params.orgId }, request) .$promise.then(function (response) { $analytics.eventTrack('Verified Bank Account'); toastr.success('You have successfully verified your bank account.'); $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationCollectionsAddController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "$analytics", "authService", function ($scope, $state, $uibModalInstance, apiService, cipherService, $analytics, authService) { $analytics.eventTrack('organizationCollectionsAddController', { category: 'Modal' }); var groupsLength = 0; $scope.groups = []; $scope.selectedGroups = {}; $scope.loading = true; $scope.useGroups = false; $uibModalInstance.opened.then(function () { return authService.getUserProfile(); }).then(function (profile) { if (profile.organizations) { var org = profile.organizations[$state.params.orgId]; $scope.useGroups = !!org.useGroups; } if ($scope.useGroups) { return apiService.groups.listOrganization({ orgId: $state.params.orgId }).$promise; } return null; }).then(function (groups) { if (!groups) { $scope.loading = false; return; } var groupsArr = []; for (var i = 0; i < groups.Data.length; i++) { groupsArr.push({ id: groups.Data[i].Id, name: groups.Data[i].Name, accessAll: groups.Data[i].AccessAll }); if (!groups.Data[i].AccessAll) { groupsLength++; } } $scope.groups = groupsArr; $scope.loading = false; }); $scope.toggleGroupSelectionAll = function ($event) { var groups = {}; if ($event.target.checked) { for (var i = 0; i < $scope.groups.length; i++) { groups[$scope.groups[i].id] = { id: $scope.groups[i].id, readOnly: ($scope.groups[i].id in $scope.selectedGroups) ? $scope.selectedGroups[$scope.groups[i].id].readOnly : false }; } } $scope.selectedGroups = groups; }; $scope.toggleGroupSelection = function (id) { if (id in $scope.selectedGroups) { delete $scope.selectedGroups[id]; } else { $scope.selectedGroups[id] = { id: id, readOnly: false }; } }; $scope.toggleGroupReadOnlySelection = function (group) { if (group.id in $scope.selectedGroups) { $scope.selectedGroups[group.id].readOnly = !group.accessAll && !!!$scope.selectedGroups[group.id].readOnly; } }; $scope.groupSelected = function (group) { return group.id in $scope.selectedGroups || group.accessAll; }; $scope.allSelected = function () { return Object.keys($scope.selectedGroups).length >= groupsLength; }; $scope.submit = function (model) { var collection = cipherService.encryptCollection(model, $state.params.orgId); if ($scope.useGroups) { collection.groups = []; for (var groupId in $scope.selectedGroups) { if ($scope.selectedGroups.hasOwnProperty(groupId)) { for (var i = 0; i < $scope.groups.length; i++) { if ($scope.groups[i].id === $scope.selectedGroups[groupId].id) { if (!$scope.groups[i].accessAll) { collection.groups.push($scope.selectedGroups[groupId]); } break; } } } } } $scope.submitPromise = apiService.collections.post({ orgId: $state.params.orgId }, collection, function (response) { $analytics.eventTrack('Created Collection'); var decCollection = cipherService.decryptCollection(response, $state.params.orgId, true); $uibModalInstance.close(decCollection); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationCollectionsController', ["$scope", "$state", "apiService", "$uibModal", "cipherService", "$filter", "toastr", "$analytics", "$uibModalStack", function ($scope, $state, apiService, $uibModal, cipherService, $filter, toastr, $analytics, $uibModalStack) { $scope.collections = []; $scope.loading = true; $scope.$on('$viewContentLoaded', function () { loadList(); }); $scope.$on('organizationCollectionsAdd', function (event, args) { $scope.add(); }); $scope.add = function () { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationCollectionsAdd.html', controller: 'organizationCollectionsAddController' }); modal.result.then(function (collection) { $scope.collections.push(collection); }); }; $scope.edit = function (collection) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationCollectionsEdit.html', controller: 'organizationCollectionsEditController', resolve: { id: function () { return collection.id; } } }); modal.result.then(function (editedCollection) { var existingCollections = $filter('filter')($scope.collections, { id: editedCollection.id }, true); if (existingCollections && existingCollections.length > 0) { existingCollections[0].name = editedCollection.name; } }); }; $scope.users = function (collection) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationCollectionsUsers.html', controller: 'organizationCollectionsUsersController', size: 'lg', resolve: { collection: function () { return collection; } } }); modal.result.then(function () { // nothing to do }); }; $scope.groups = function (collection) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationCollectionsGroups.html', controller: 'organizationCollectionsGroupsController', resolve: { collection: function () { return collection; } } }); modal.result.then(function () { // nothing to do }); }; $scope.delete = function (collection) { if (!confirm('Are you sure you want to delete this collection (' + collection.name + ')?')) { return; } apiService.collections.del({ orgId: $state.params.orgId, id: collection.id }, function () { var index = $scope.collections.indexOf(collection); if (index > -1) { $scope.collections.splice(index, 1); } $analytics.eventTrack('Deleted Collection'); toastr.success(collection.name + ' has been deleted.', 'Collection Deleted'); }, function () { toastr.error(collection.name + ' was not able to be deleted.', 'Error'); }); }; function loadList() { apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) { $scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true); $scope.loading = false; if ($state.params.search) { $uibModalStack.dismissAll(); $scope.filterSearch = $state.params.search; $('#filterSearch').focus(); } }); } }]); angular .module('bit.organization') .controller('organizationCollectionsEditController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "$analytics", "id", "authService", function ($scope, $state, $uibModalInstance, apiService, cipherService, $analytics, id, authService) { $analytics.eventTrack('organizationCollectionsEditController', { category: 'Modal' }); var groupsLength = 0; $scope.collection = {}; $scope.groups = []; $scope.selectedGroups = {}; $scope.loading = true; $scope.useGroups = false; $uibModalInstance.opened.then(function () { return apiService.collections.getDetails({ orgId: $state.params.orgId, id: id }).$promise; }).then(function (collection) { $scope.collection = cipherService.decryptCollection(collection); var groups = {}; if (collection.Groups) { for (var i = 0; i < collection.Groups.length; i++) { groups[collection.Groups[i].Id] = { id: collection.Groups[i].Id, readOnly: collection.Groups[i].ReadOnly }; } } $scope.selectedGroups = groups; return authService.getUserProfile(); }).then(function (profile) { if (profile.organizations) { var org = profile.organizations[$state.params.orgId]; $scope.useGroups = !!org.useGroups; } if ($scope.useGroups) { return apiService.groups.listOrganization({ orgId: $state.params.orgId }).$promise; } return null; }).then(function (groups) { if (!groups) { $scope.loading = false; return; } var groupsArr = []; for (var i = 0; i < groups.Data.length; i++) { groupsArr.push({ id: groups.Data[i].Id, name: groups.Data[i].Name, accessAll: groups.Data[i].AccessAll }); if (!groups.Data[i].AccessAll) { groupsLength++; } } $scope.groups = groupsArr; $scope.loading = false; }); $scope.toggleGroupSelectionAll = function ($event) { var groups = {}; if ($event.target.checked) { for (var i = 0; i < $scope.groups.length; i++) { groups[$scope.groups[i].id] = { id: $scope.groups[i].id, readOnly: ($scope.groups[i].id in $scope.selectedGroups) ? $scope.selectedGroups[$scope.groups[i].id].readOnly : false }; } } $scope.selectedGroups = groups; }; $scope.toggleGroupSelection = function (id) { if (id in $scope.selectedGroups) { delete $scope.selectedGroups[id]; } else { $scope.selectedGroups[id] = { id: id, readOnly: false }; } }; $scope.toggleGroupReadOnlySelection = function (group) { if (group.id in $scope.selectedGroups) { $scope.selectedGroups[group.id].readOnly = !group.accessAll && !!!$scope.selectedGroups[group.id].readOnly; } }; $scope.groupSelected = function (group) { return group.id in $scope.selectedGroups || group.accessAll; }; $scope.allSelected = function () { return Object.keys($scope.selectedGroups).length >= groupsLength; }; $scope.submit = function (model) { var collection = cipherService.encryptCollection(model, $state.params.orgId); if ($scope.useGroups) { collection.groups = []; for (var groupId in $scope.selectedGroups) { if ($scope.selectedGroups.hasOwnProperty(groupId)) { for (var i = 0; i < $scope.groups.length; i++) { if ($scope.groups[i].id === $scope.selectedGroups[groupId].id) { if (!$scope.groups[i].accessAll) { collection.groups.push($scope.selectedGroups[groupId]); } break; } } } } } $scope.submitPromise = apiService.collections.put({ orgId: $state.params.orgId, id: id }, collection, function (response) { $analytics.eventTrack('Edited Collection'); var decCollection = cipherService.decryptCollection(response, $state.params.orgId, true); $uibModalInstance.close(decCollection); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationCollectionsUsersController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "$analytics", "collection", "toastr", function ($scope, $state, $uibModalInstance, apiService, cipherService, $analytics, collection, toastr) { $analytics.eventTrack('organizationCollectionsUsersController', { category: 'Modal' }); $scope.loading = true; $scope.collection = collection; $scope.users = []; $uibModalInstance.opened.then(function () { $scope.loading = false; apiService.collections.listUsers( { orgId: $state.params.orgId, id: collection.id }, function (userList) { if (userList && userList.Data.length) { var users = []; for (var i = 0; i < userList.Data.length; i++) { users.push({ organizationUserId: userList.Data[i].OrganizationUserId, name: userList.Data[i].Name, email: userList.Data[i].Email, type: userList.Data[i].Type, status: userList.Data[i].Status, readOnly: userList.Data[i].ReadOnly, accessAll: userList.Data[i].AccessAll }); } $scope.users = users; } }); }); $scope.remove = function (user) { if (!confirm('Are you sure you want to remove this user (' + user.email + ') from this ' + 'collection (' + collection.name + ')?')) { return; } apiService.collections.delUser( { orgId: $state.params.orgId, id: collection.id, orgUserId: user.organizationUserId }, null, function () { toastr.success(user.email + ' has been removed.', 'User Removed'); $analytics.eventTrack('Removed User From Collection'); var index = $scope.users.indexOf(user); if (index > -1) { $scope.users.splice(index, 1); } }, function () { toastr.error('Unable to remove user.', 'Error'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationDashboardController', ["$scope", "authService", "$state", "appSettings", function ($scope, authService, $state, appSettings) { $scope.selfHosted = appSettings.selfHosted; $scope.$on('$viewContentLoaded', function () { authService.getUserProfile().then(function (userProfile) { if (!userProfile.organizations) { return; } $scope.orgProfile = userProfile.organizations[$state.params.orgId]; }); }); $scope.goBilling = function () { $state.go('backend.org.billing', { orgId: $state.params.orgId }); }; }]); angular .module('bit.organization') .controller('organizationDeleteController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics) { $analytics.eventTrack('organizationDeleteController', { category: 'Modal' }); $scope.submit = function () { $scope.submitPromise = cryptoService.hashPassword($scope.masterPassword).then(function (hash) { return apiService.organizations.del({ id: $state.params.orgId }, { masterPasswordHash: hash }).$promise; }).then(function () { $uibModalInstance.dismiss('cancel'); authService.removeProfileOrganization($state.params.orgId); $analytics.eventTrack('Deleted Organization'); return $state.go('backend.user.vault'); }).then(function () { toastr.success('This organization and all associated data has been deleted.', 'Organization Deleted'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationEventsController', ["$scope", "$state", "apiService", "$uibModal", "$filter", "toastr", "$analytics", "constants", "eventService", "$compile", "$sce", function ($scope, $state, apiService, $uibModal, $filter, toastr, $analytics, constants, eventService, $compile, $sce) { $scope.events = []; $scope.orgUsers = []; $scope.loading = true; $scope.continuationToken = null; var defaultFilters = eventService.getDefaultDateFilters(); $scope.filterStart = defaultFilters.start; $scope.filterEnd = defaultFilters.end; $scope.$on('$viewContentLoaded', function () { load(); }); $scope.refresh = function () { loadEvents(true); }; $scope.next = function () { loadEvents(false); }; var i = 0, orgUsersUserIdDict = {}, orgUsersIdDict = {}; function load() { apiService.organizationUsers.list({ orgId: $state.params.orgId }).$promise.then(function (list) { var users = []; for (i = 0; i < list.Data.length; i++) { var user = { id: list.Data[i].Id, userId: list.Data[i].UserId, name: list.Data[i].Name, email: list.Data[i].Email }; users.push(user); var displayName = user.name || user.email; orgUsersUserIdDict[user.userId] = displayName; orgUsersIdDict[user.id] = displayName; } $scope.orgUsers = users; return loadEvents(true); }); } function loadEvents(clearExisting) { var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd); if (filterResult.error) { alert(filterResult.error); return; } if (clearExisting) { $scope.continuationToken = null; $scope.events = []; } $scope.loading = true; return apiService.events.listOrganization({ orgId: $state.params.orgId, start: filterResult.start, end: filterResult.end, continuationToken: $scope.continuationToken }).$promise.then(function (list) { $scope.continuationToken = list.ContinuationToken; var events = []; for (i = 0; i < list.Data.length; i++) { var userId = list.Data[i].ActingUserId || list.Data[i].UserId; var eventInfo = eventService.getEventInfo(list.Data[i]); var htmlMessage = $compile('' + eventInfo.message + '')($scope); events.push({ message: $sce.trustAsHtml(htmlMessage[0].outerHTML), appIcon: eventInfo.appIcon, appName: eventInfo.appName, userId: userId, userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-', date: list.Data[i].Date, ip: list.Data[i].IpAddress }); } if ($scope.events && $scope.events.length > 0) { $scope.events = $scope.events.concat(events); } else { $scope.events = events; } $scope.loading = false; }); } }]); angular .module('bit.organization') .controller('organizationGroupsAddController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "$analytics", function ($scope, $state, $uibModalInstance, apiService, cipherService, $analytics) { $analytics.eventTrack('organizationGroupsAddController', { category: 'Modal' }); $scope.collections = []; $scope.selectedCollections = {}; $scope.loading = true; $uibModalInstance.opened.then(function () { return apiService.collections.listOrganization({ orgId: $state.params.orgId }).$promise; }).then(function (collections) { $scope.collections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true); $scope.loading = false; }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { collections[$scope.collections[i].id] = { id: $scope.collections[i].id, readOnly: ($scope.collections[i].id in $scope.selectedCollections) ? $scope.selectedCollections[$scope.collections[i].id].readOnly : false }; } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = { id: id, readOnly: false }; } }; $scope.toggleCollectionReadOnlySelection = function (id) { if (id in $scope.selectedCollections) { $scope.selectedCollections[id].readOnly = !!!$scope.selectedCollections[id].readOnly; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { return Object.keys($scope.selectedCollections).length === $scope.collections.length; }; $scope.submit = function (model) { var group = { name: model.name, accessAll: !!model.accessAll, externalId: model.externalId }; if (!group.accessAll) { group.collections = []; for (var collectionId in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(collectionId)) { group.collections.push($scope.selectedCollections[collectionId]); } } } $scope.submitPromise = apiService.groups.post({ orgId: $state.params.orgId }, group, function (response) { $analytics.eventTrack('Created Group'); $uibModalInstance.close({ id: response.Id, name: response.Name }); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationGroupsController', ["$scope", "$state", "apiService", "$uibModal", "$filter", "toastr", "$analytics", "$uibModalStack", function ($scope, $state, apiService, $uibModal, $filter, toastr, $analytics, $uibModalStack) { $scope.groups = []; $scope.loading = true; $scope.$on('$viewContentLoaded', function () { loadList(); }); $scope.$on('organizationGroupsAdd', function (event, args) { $scope.add(); }); $scope.add = function () { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationGroupsAdd.html', controller: 'organizationGroupsAddController' }); modal.result.then(function (group) { $scope.groups.push(group); }); }; $scope.edit = function (group) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationGroupsEdit.html', controller: 'organizationGroupsEditController', resolve: { id: function () { return group.id; } } }); modal.result.then(function (editedGroup) { var existingGroups = $filter('filter')($scope.groups, { id: editedGroup.id }, true); if (existingGroups && existingGroups.length > 0) { existingGroups[0].name = editedGroup.name; } }); }; $scope.users = function (group) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationGroupsUsers.html', controller: 'organizationGroupsUsersController', size: 'lg', resolve: { group: function () { return group; } } }); modal.result.then(function () { // nothing to do }); }; $scope.delete = function (group) { if (!confirm('Are you sure you want to delete this group (' + group.name + ')?')) { return; } apiService.groups.del({ orgId: $state.params.orgId, id: group.id }, function () { var index = $scope.groups.indexOf(group); if (index > -1) { $scope.groups.splice(index, 1); } $analytics.eventTrack('Deleted Group'); toastr.success(group.name + ' has been deleted.', 'Group Deleted'); }, function () { toastr.error(group.name + ' was not able to be deleted.', 'Error'); }); }; function loadList() { apiService.groups.listOrganization({ orgId: $state.params.orgId }, function (list) { var groups = []; for (var i = 0; i < list.Data.length; i++) { groups.push({ id: list.Data[i].Id, name: list.Data[i].Name }); } $scope.groups = groups; $scope.loading = false; if ($state.params.search) { $uibModalStack.dismissAll(); $scope.filterSearch = $state.params.search; $('#filterSearch').focus(); } }); } }]); angular .module('bit.organization') .controller('organizationGroupsEditController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "$analytics", "id", function ($scope, $state, $uibModalInstance, apiService, cipherService, $analytics, id) { $analytics.eventTrack('organizationGroupsEditController', { category: 'Modal' }); $scope.collections = []; $scope.selectedCollections = {}; $scope.loading = true; $uibModalInstance.opened.then(function () { return apiService.groups.getDetails({ orgId: $state.params.orgId, id: id }).$promise; }).then(function (group) { $scope.group = { id: id, name: group.Name, externalId: group.ExternalId, accessAll: group.AccessAll }; var collections = {}; if (group.Collections) { for (var i = 0; i < group.Collections.length; i++) { collections[group.Collections[i].Id] = { id: group.Collections[i].Id, readOnly: group.Collections[i].ReadOnly }; } } $scope.selectedCollections = collections; return apiService.collections.listOrganization({ orgId: $state.params.orgId }).$promise; }).then(function (collections) { $scope.collections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true); $scope.loading = false; }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { collections[$scope.collections[i].id] = { id: $scope.collections[i].id, readOnly: ($scope.collections[i].id in $scope.selectedCollections) ? $scope.selectedCollections[$scope.collections[i].id].readOnly : false }; } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = { id: id, readOnly: false }; } }; $scope.toggleCollectionReadOnlySelection = function (id) { if (id in $scope.selectedCollections) { $scope.selectedCollections[id].readOnly = !!!$scope.selectedCollections[id].readOnly; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { return Object.keys($scope.selectedCollections).length === $scope.collections.length; }; $scope.submit = function () { var group = { name: $scope.group.name, accessAll: !!$scope.group.accessAll, externalId: $scope.group.externalId }; if (!group.accessAll) { group.collections = []; for (var collectionId in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(collectionId)) { group.collections.push($scope.selectedCollections[collectionId]); } } } $scope.submitPromise = apiService.groups.put({ orgId: $state.params.orgId, id: id }, group, function (response) { $analytics.eventTrack('Edited Group'); $uibModalInstance.close({ id: response.Id, name: response.Name }); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationGroupsUsersController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "group", "toastr", function ($scope, $state, $uibModalInstance, apiService, $analytics, group, toastr) { $analytics.eventTrack('organizationGroupUsersController', { category: 'Modal' }); $scope.loading = true; $scope.group = group; $scope.users = []; $uibModalInstance.opened.then(function () { return apiService.groups.listUsers({ orgId: $state.params.orgId, id: group.id }).$promise; }).then(function (userList) { var users = []; if (userList && userList.Data.length) { for (var i = 0; i < userList.Data.length; i++) { users.push({ organizationUserId: userList.Data[i].OrganizationUserId, name: userList.Data[i].Name, email: userList.Data[i].Email, type: userList.Data[i].Type, status: userList.Data[i].Status, accessAll: userList.Data[i].AccessAll }); } } $scope.users = users; $scope.loading = false; }); $scope.remove = function (user) { if (!confirm('Are you sure you want to remove this user (' + user.email + ') from this ' + 'group (' + group.name + ')?')) { return; } apiService.groups.delUser({ orgId: $state.params.orgId, id: group.id, orgUserId: user.organizationUserId }, null, function () { toastr.success(user.email + ' has been removed.', 'User Removed'); $analytics.eventTrack('Removed User From Group'); var index = $scope.users.indexOf(user); if (index > -1) { $scope.users.splice(index, 1); } }, function () { toastr.error('Unable to remove user.', 'Error'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationPeopleController', ["$scope", "$state", "$uibModal", "cryptoService", "apiService", "authService", "toastr", "$analytics", "$filter", "$uibModalStack", function ($scope, $state, $uibModal, cryptoService, apiService, authService, toastr, $analytics, $filter, $uibModalStack) { $scope.users = []; $scope.useGroups = false; $scope.useEvents = false; $scope.$on('$viewContentLoaded', function () { loadList(); authService.getUserProfile().then(function (profile) { if (profile.organizations) { var org = profile.organizations[$state.params.orgId]; $scope.useGroups = !!org.useGroups; $scope.useEvents = !!org.useEvents; } }); }); $scope.reinvite = function (user) { apiService.organizationUsers.reinvite({ orgId: $state.params.orgId, id: user.id }, null, function () { $analytics.eventTrack('Reinvited User'); toastr.success(user.email + ' has been invited again.', 'User Invited'); }, function () { toastr.error('Unable to invite user.', 'Error'); }); }; $scope.delete = function (user) { if (!confirm('Are you sure you want to remove this user (' + user.email + ')?')) { return; } apiService.organizationUsers.del({ orgId: $state.params.orgId, id: user.id }, null, function () { $analytics.eventTrack('Deleted User'); toastr.success(user.email + ' has been removed.', 'User Removed'); var index = $scope.users.indexOf(user); if (index > -1) { $scope.users.splice(index, 1); } }, function () { toastr.error('Unable to remove user.', 'Error'); }); }; $scope.confirm = function (user) { apiService.users.getPublicKey({ id: user.userId }, function (userKey) { var orgKey = cryptoService.getOrgKey($state.params.orgId); if (!orgKey) { toastr.error('Unable to confirm user.', 'Error'); return; } var key = cryptoService.rsaEncrypt(orgKey.key, userKey.PublicKey); apiService.organizationUsers.confirm({ orgId: $state.params.orgId, id: user.id }, { key: key }, function () { user.status = 2; $analytics.eventTrack('Confirmed User'); toastr.success(user.email + ' has been confirmed.', 'User Confirmed'); }, function () { toastr.error('Unable to confirm user.', 'Error'); }); }, function () { toastr.error('Unable to confirm user.', 'Error'); }); }; $scope.$on('organizationPeopleInvite', function (event, args) { $scope.invite(); }); $scope.invite = function () { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationPeopleInvite.html', controller: 'organizationPeopleInviteController' }); modal.result.then(function () { loadList(); }); }; $scope.edit = function (orgUser) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationPeopleEdit.html', controller: 'organizationPeopleEditController', resolve: { orgUser: function () { return orgUser; } } }); modal.result.then(function () { loadList(); }); }; $scope.groups = function (user) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationPeopleGroups.html', controller: 'organizationPeopleGroupsController', resolve: { orgUser: function () { return user; } } }); modal.result.then(function () { }); }; $scope.events = function (user) { $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationPeopleEvents.html', controller: 'organizationPeopleEventsController', resolve: { orgUser: function () { return user; }, orgId: function () { return $state.params.orgId; } } }); }; function loadList() { apiService.organizationUsers.list({ orgId: $state.params.orgId }, function (list) { var users = []; for (var i = 0; i < list.Data.length; i++) { var user = { id: list.Data[i].Id, userId: list.Data[i].UserId, name: list.Data[i].Name, email: list.Data[i].Email, status: list.Data[i].Status, type: list.Data[i].Type, accessAll: list.Data[i].AccessAll }; users.push(user); } $scope.users = users; if ($state.params.search) { $uibModalStack.dismissAll(); $scope.filterSearch = $state.params.search; $('#filterSearch').focus(); } if ($state.params.viewEvents) { $uibModalStack.dismissAll(); var eventUser = $filter('filter')($scope.users, { id: $state.params.viewEvents }); if (eventUser && eventUser.length) { $scope.events(eventUser[0]); } } }); } }]); angular .module('bit.organization') .controller('organizationPeopleEditController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "orgUser", "$analytics", function ($scope, $state, $uibModalInstance, apiService, cipherService, orgUser, $analytics) { $analytics.eventTrack('organizationPeopleEditController', { category: 'Modal' }); $scope.loading = true; $scope.collections = []; $scope.selectedCollections = {}; $uibModalInstance.opened.then(function () { apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) { $scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true); $scope.loading = false; }); apiService.organizationUsers.get({ orgId: $state.params.orgId, id: orgUser.id }, function (user) { var collections = {}; if (user && user.Collections) { for (var i = 0; i < user.Collections.length; i++) { collections[user.Collections[i].Id] = { id: user.Collections[i].Id, readOnly: user.Collections[i].ReadOnly }; } } $scope.email = orgUser.email; $scope.type = user.Type; $scope.accessAll = user.AccessAll; $scope.selectedCollections = collections; }); }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { collections[$scope.collections[i].id] = { id: $scope.collections[i].id, readOnly: ($scope.collections[i].id in $scope.selectedCollections) ? $scope.selectedCollections[$scope.collections[i].id].readOnly : false }; } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = { id: id, readOnly: false }; } }; $scope.toggleCollectionReadOnlySelection = function (id) { if (id in $scope.selectedCollections) { $scope.selectedCollections[id].readOnly = !!!$scope.selectedCollections[id].readOnly; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { return Object.keys($scope.selectedCollections).length === $scope.collections.length; }; $scope.submitPromise = null; $scope.submit = function (model) { var collections = []; if (!$scope.accessAll) { for (var collectionId in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(collectionId)) { collections.push($scope.selectedCollections[collectionId]); } } } $scope.submitPromise = apiService.organizationUsers.put( { orgId: $state.params.orgId, id: orgUser.id }, { type: $scope.type, collections: collections, accessAll: $scope.accessAll }, function () { $analytics.eventTrack('Edited User'); $uibModalInstance.close(); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationPeopleEventsController', ["$scope", "apiService", "$uibModalInstance", "orgUser", "$analytics", "eventService", "orgId", "$compile", "$sce", function ($scope, apiService, $uibModalInstance, orgUser, $analytics, eventService, orgId, $compile, $sce) { $analytics.eventTrack('organizationPeopleEventsController', { category: 'Modal' }); $scope.email = orgUser.email; $scope.events = []; $scope.loading = true; $scope.continuationToken = null; var defaultFilters = eventService.getDefaultDateFilters(); $scope.filterStart = defaultFilters.start; $scope.filterEnd = defaultFilters.end; $uibModalInstance.opened.then(function () { loadEvents(true); }); $scope.refresh = function () { loadEvents(true); }; $scope.next = function () { loadEvents(false); }; function loadEvents(clearExisting) { var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd); if (filterResult.error) { alert(filterResult.error); return; } if (clearExisting) { $scope.continuationToken = null; $scope.events = []; } $scope.loading = true; return apiService.events.listOrganizationUser({ orgId: orgId, id: orgUser.id, start: filterResult.start, end: filterResult.end, continuationToken: $scope.continuationToken }).$promise.then(function (list) { $scope.continuationToken = list.ContinuationToken; var events = []; for (var i = 0; i < list.Data.length; i++) { var eventInfo = eventService.getEventInfo(list.Data[i]); var htmlMessage = $compile('' + eventInfo.message + '')($scope); events.push({ message: $sce.trustAsHtml(htmlMessage[0].outerHTML), appIcon: eventInfo.appIcon, appName: eventInfo.appName, date: list.Data[i].Date, ip: list.Data[i].IpAddress }); } if ($scope.events && $scope.events.length > 0) { $scope.events = $scope.events.concat(events); } else { $scope.events = events; } $scope.loading = false; }); } $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationPeopleGroupsController', ["$scope", "$state", "$uibModalInstance", "apiService", "orgUser", "$analytics", function ($scope, $state, $uibModalInstance, apiService, orgUser, $analytics) { $analytics.eventTrack('organizationPeopleGroupsController', { category: 'Modal' }); $scope.loading = true; $scope.groups = []; $scope.selectedGroups = {}; $scope.orgUser = orgUser; $uibModalInstance.opened.then(function () { return apiService.groups.listOrganization({ orgId: $state.params.orgId }).$promise; }).then(function (groupsList) { var groups = []; for (var i = 0; i < groupsList.Data.length; i++) { groups.push({ id: groupsList.Data[i].Id, name: groupsList.Data[i].Name }); } $scope.groups = groups; return apiService.organizationUsers.listGroups({ orgId: $state.params.orgId, id: orgUser.id }).$promise; }).then(function (groupIds) { var selectedGroups = {}; if (groupIds) { for (var i = 0; i < groupIds.length; i++) { selectedGroups[groupIds[i]] = true; } } $scope.selectedGroups = selectedGroups; $scope.loading = false; }); $scope.toggleGroupSelectionAll = function ($event) { var groups = {}; if ($event.target.checked) { for (var i = 0; i < $scope.groups.length; i++) { groups[$scope.groups[i].id] = true; } } $scope.selectedGroups = groups; }; $scope.toggleGroupSelection = function (id) { if (id in $scope.selectedGroups) { delete $scope.selectedGroups[id]; } else { $scope.selectedGroups[id] = true; } }; $scope.groupSelected = function (group) { return group.id in $scope.selectedGroups; }; $scope.allSelected = function () { return Object.keys($scope.selectedGroups).length === $scope.groups.length; }; $scope.submitPromise = null; $scope.submit = function (model) { var groups = []; for (var groupId in $scope.selectedGroups) { if ($scope.selectedGroups.hasOwnProperty(groupId)) { groups.push(groupId); } } $scope.submitPromise = apiService.organizationUsers.putGroups({ orgId: $state.params.orgId, id: orgUser.id }, { groupIds: groups, }, function () { $analytics.eventTrack('Edited User Groups'); $uibModalInstance.close(); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationPeopleInviteController', ["$scope", "$state", "$uibModalInstance", "apiService", "cipherService", "$analytics", function ($scope, $state, $uibModalInstance, apiService, cipherService, $analytics) { $analytics.eventTrack('organizationPeopleInviteController', { category: 'Modal' }); $scope.loading = true; $scope.collections = []; $scope.selectedCollections = {}; $scope.model = { type: 'User' }; $uibModalInstance.opened.then(function () { apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (list) { $scope.collections = cipherService.decryptCollections(list.Data, $state.params.orgId, true); $scope.loading = false; }); }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { collections[$scope.collections[i].id] = { id: $scope.collections[i].id, readOnly: ($scope.collections[i].id in $scope.selectedCollections) ? $scope.selectedCollections[$scope.collections[i].id].readOnly : false }; } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = { id: id, readOnly: false }; } }; $scope.toggleCollectionReadOnlySelection = function (id) { if (id in $scope.selectedCollections) { $scope.selectedCollections[id].readOnly = !!!$scope.selectedCollections[id].readOnly; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { return Object.keys($scope.selectedCollections).length === $scope.collections.length; }; $scope.submitPromise = null; $scope.submit = function (model) { var collections = []; if (!model.accessAll) { for (var collectionId in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(collectionId)) { collections.push($scope.selectedCollections[collectionId]); } } } var splitEmails = model.emails.trim().split(/\s*,\s*/); $scope.submitPromise = apiService.organizationUsers.invite({ orgId: $state.params.orgId }, { emails: splitEmails, type: model.type, collections: collections, accessAll: model.accessAll }, function () { $analytics.eventTrack('Invited User'); $uibModalInstance.close(); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationSettingsController', ["$scope", "$state", "apiService", "toastr", "authService", "$uibModal", "$analytics", "appSettings", "constants", "$filter", function ($scope, $state, apiService, toastr, authService, $uibModal, $analytics, appSettings, constants, $filter) { $scope.selfHosted = appSettings.selfHosted; $scope.model = {}; $scope.twoStepProviders = $filter('filter')(constants.twoFactorProviderInfo, { organization: true }); $scope.use2fa = false; $scope.$on('$viewContentLoaded', function () { apiService.organizations.get({ id: $state.params.orgId }).$promise.then(function (org) { $scope.model = { name: org.Name, billingEmail: org.BillingEmail, businessName: org.BusinessName, businessAddress1: org.BusinessAddress1, businessAddress2: org.BusinessAddress2, businessAddress3: org.BusinessAddress3, businessCountry: org.BusinessCountry, businessTaxNumber: org.BusinessTaxNumber }; $scope.use2fa = org.Use2fa; if (org.Use2fa) { return apiService.twoFactor.listOrganization({ orgId: $state.params.orgId }).$promise; } else { return null; } }).then(function (response) { if (!response || !response.Data) { return; } for (var i = 0; i < response.Data.length; i++) { if (!response.Data[i].Enabled) { continue; } var provider = $filter('filter')($scope.twoStepProviders, { type: response.Data[i].Type }); if (provider.length) { provider[0].enabled = true; } } }); }); $scope.generalSave = function () { if ($scope.selfHosted) { return; } $scope.generalPromise = apiService.organizations.put({ id: $state.params.orgId }, $scope.model, function (org) { authService.updateProfileOrganization(org).then(function (updatedOrg) { $analytics.eventTrack('Updated Organization Settings'); toastr.success('Organization has been updated.', 'Success!'); }); }).$promise; }; $scope.import = function () { $uibModal.open({ animation: true, templateUrl: 'app/tools/views/toolsImport.html', controller: 'organizationSettingsImportController' }); }; $scope.export = function () { $uibModal.open({ animation: true, templateUrl: 'app/tools/views/toolsExport.html', controller: 'organizationSettingsExportController' }); }; $scope.delete = function () { $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationDelete.html', controller: 'organizationDeleteController' }); }; $scope.edit = function (provider) { if (provider.type === constants.twoFactorProvider.organizationDuo) { typeName = 'Duo'; } else { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html', controller: 'settingsTwoStep' + typeName + 'Controller', resolve: { enabled: function () { return provider.enabled; }, orgId: function () { return $state.params.orgId; } } }); modal.result.then(function (enabled) { if (enabled || enabled === false) { // do not adjust when undefined or null provider.enabled = enabled; } }); }; }]); angular .module('bit.organization') .controller('organizationSettingsExportController', ["$scope", "apiService", "$uibModalInstance", "cipherService", "$q", "toastr", "$analytics", "$state", "constants", function ($scope, apiService, $uibModalInstance, cipherService, $q, toastr, $analytics, $state, constants) { $analytics.eventTrack('organizationSettingsExportController', { category: 'Modal' }); $scope.export = function (model) { $scope.startedExport = true; var decCiphers = [], decCollections = []; var collectionsPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (collections) { decCollections = cipherService.decryptCollections(collections.Data, $state.params.orgId, true); }).$promise; var ciphersPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId }, function (ciphers) { decCiphers = cipherService.decryptCiphers(ciphers.Data); }).$promise; $q.all([collectionsPromise, ciphersPromise]).then(function () { if (!decCiphers.length) { toastr.error('Nothing to export.', 'Error!'); $scope.close(); return; } var i; var collectionsDict = {}; for (i = 0; i < decCollections.length; i++) { collectionsDict[decCollections[i].id] = decCollections[i]; } try { var exportCiphers = []; for (i = 0; i < decCiphers.length; i++) { // only export logins and secure notes if (decCiphers[i].type !== constants.cipherType.login && decCiphers[i].type !== constants.cipherType.secureNote) { continue; } var cipher = { collections: [], type: null, name: decCiphers[i].name, notes: decCiphers[i].notes, fields: null, // Login props login_uri: null, login_username: null, login_password: null, login_totp: null }; var j; if (decCiphers[i].collectionIds) { for (j = 0; j < decCiphers[i].collectionIds.length; j++) { if (collectionsDict.hasOwnProperty(decCiphers[i].collectionIds[j])) { cipher.collections.push(collectionsDict[decCiphers[i].collectionIds[j]].name); } } } if (decCiphers[i].fields) { for (j = 0; j < decCiphers[i].fields.length; j++) { if (!cipher.fields) { cipher.fields = ''; } else { cipher.fields += '\n'; } cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value); } } switch (decCiphers[i].type) { case constants.cipherType.login: cipher.type = 'login'; cipher.login_uri = null; cipher.login_username = decCiphers[i].login.username; cipher.login_password = decCiphers[i].login.password; cipher.login_totp = decCiphers[i].login.totp; if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) { cipher.login_uri = []; for (j = 0; j < decCiphers[i].login.uris.length; j++) { cipher.login_uri.push(decCiphers[i].login.uris[j].uri); } } break; case constants.cipherType.secureNote: cipher.type = 'note'; break; default: continue; } exportCiphers.push(cipher); } var csvString = Papa.unparse(exportCiphers); var csvBlob = new Blob([csvString]); // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(csvBlob, makeFileName()); } else { var a = window.document.createElement('a'); a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); a.download = makeFileName(); document.body.appendChild(a); // IE: "Access is denied". // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access a.click(); document.body.removeChild(a); } $analytics.eventTrack('Exported Organization Data'); toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!'); $scope.close(); } catch (err) { toastr.error('Something went wrong. Please try again.', 'Error!'); $scope.close(); } }, function () { toastr.error('Something went wrong. Please try again.', 'Error!'); $scope.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; function makeFileName() { var now = new Date(); var dateString = now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) + padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + padNumber(now.getSeconds(), 2); return 'bitwarden_org_export_' + dateString + '.csv'; } function padNumber(number, width, paddingCharacter) { paddingCharacter = paddingCharacter || '0'; number = number + ''; return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; } }]); angular .module('bit.organization') .controller('organizationSettingsImportController', ["$scope", "$state", "apiService", "$uibModalInstance", "cipherService", "toastr", "importService", "$analytics", "$sce", "validationService", "cryptoService", function ($scope, $state, apiService, $uibModalInstance, cipherService, toastr, importService, $analytics, $sce, validationService, cryptoService) { $analytics.eventTrack('organizationSettingsImportController', { category: 'Modal' }); $scope.model = { source: '' }; $scope.source = {}; $scope.splitFeatured = false; $scope.options = [ { id: 'bitwardencsv', name: 'Bitwarden (csv)', featured: true, sort: 1, instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' + 'Log into the web vault and navigate to your organization\'s admin area. Then to go ' + '"Settings" > "Tools" > "Export".') }, { id: 'lastpass', name: 'LastPass (csv)', featured: true, sort: 2, instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-lastpass/') } ]; $scope.setSource = function () { for (var i = 0; i < $scope.options.length; i++) { if ($scope.options[i].id === $scope.model.source) { $scope.source = $scope.options[i]; break; } } }; $scope.setSource(); $scope.import = function (model, form) { if (!model.source || model.source === '') { validationService.addError(form, 'source', 'Select the format of the import file.', true); return; } var file = document.getElementById('file').files[0]; if (!file && (!model.fileContents || model.fileContents === '')) { validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true); return; } $scope.processing = true; importService.importOrg(model.source, file || model.fileContents, importSuccess, importError); }; function importSuccess(collections, ciphers, collectionRelationships) { if (!collections.length && !ciphers.length) { importError('Nothing was imported.'); return; } else if (ciphers.length) { var halfway = Math.floor(ciphers.length / 2); var last = ciphers.length - 1; if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) { importError('Data is not formatted correctly. Please check your import file and try again.'); return; } } apiService.ciphers.importOrg({ orgId: $state.params.orgId }, { collections: cipherService.encryptCollections(collections, $state.params.orgId), ciphers: cipherService.encryptCiphers(ciphers, cryptoService.getOrgKey($state.params.orgId)), collectionRelationships: collectionRelationships }, function () { $uibModalInstance.dismiss('cancel'); $state.go('backend.org.vault', { orgId: $state.params.orgId }).then(function () { $analytics.eventTrack('Imported Org Data', { label: $scope.model.source }); toastr.success('Data has been successfully imported into your vault.', 'Import Success'); }); }, importError); } function cipherIsBadData(cipher) { return (cipher.name === null || cipher.name === '--') && (cipher.login && (cipher.login.password === null || cipher.login.password === '')); } function importError(error) { $analytics.eventTrack('Import Org Data Failed', { label: $scope.model.source }); $uibModalInstance.dismiss('cancel'); if (error) { var data = error.data; if (data && data.ValidationErrors) { var message = ''; for (var key in data.ValidationErrors) { if (!data.ValidationErrors.hasOwnProperty(key)) { continue; } for (var i = 0; i < data.ValidationErrors[key].length; i++) { message += (key + ': ' + data.ValidationErrors[key][i] + ' '); } } if (message !== '') { toastr.error(message); return; } } else if (data && data.Message) { toastr.error(data.Message); return; } else { toastr.error(error); return; } } toastr.error('Something went wrong. Try again.', 'Oh No!'); } $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationVaultAddCipherController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "passwordService", "$analytics", "authService", "orgId", "$uibModal", "constants", "selectedType", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, $analytics, authService, orgId, $uibModal, constants, selectedType) { $analytics.eventTrack('organizationVaultAddCipherController', { category: 'Modal' }); $scope.constants = constants; $scope.selectedType = selectedType ? selectedType.toString() : constants.cipherType.login.toString(); $scope.cipher = { type: selectedType || constants.cipherType.login, login: { uris: [{ uri: null, match: null, matchValue: null }] }, identity: {}, card: {}, secureNote: { type: '0' } }; $scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true; authService.getUserProfile().then(function (userProfile) { var orgProfile = userProfile.organizations[orgId]; $scope.useTotp = orgProfile.useTotp; }); $scope.typeChanged = function () { $scope.cipher.type = parseInt($scope.selectedType); }; $scope.savePromise = null; $scope.save = function () { $scope.cipher.organizationId = orgId; var cipher = cipherService.encryptCipher($scope.cipher); $scope.savePromise = apiService.ciphers.postAdmin(cipher, function (cipherResponse) { $analytics.eventTrack('Created Organization Cipher'); var decCipher = cipherService.decryptCipherPreview(cipherResponse); $uibModalInstance.close(decCipher); }).$promise; }; $scope.generatePassword = function () { if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) { $analytics.eventTrack('Generated Password From Add'); $scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true }); } }; $scope.addUri = function () { if (!$scope.cipher.login) { return; } if (!$scope.cipher.login.uris) { $scope.cipher.login.uris = []; } $scope.cipher.login.uris.push({ uri: null, match: null, matchValue: null }); }; $scope.removeUri = function (uri) { if (!$scope.cipher.login || !$scope.cipher.login.uris) { return; } var index = $scope.cipher.login.uris.indexOf(uri); if (index > -1) { $scope.cipher.login.uris.splice(index, 1); } }; $scope.uriMatchChanged = function (uri) { if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') { uri.match = null; } else { uri.match = parseInt(uri.matchValue); } }; $scope.addField = function () { if (!$scope.cipher.fields) { $scope.cipher.fields = []; } $scope.cipher.fields.push({ type: constants.fieldType.text.toString(), name: null, value: null }); }; $scope.removeField = function (field) { var index = $scope.cipher.fields.indexOf(field); if (index > -1) { $scope.cipher.fields.splice(index, 1); } }; $scope.clipboardSuccess = function (e) { e.clearSelection(); selectPassword(e); }; $scope.clipboardError = function (e, password) { if (password) { selectPassword(e); } alert('Your web browser does not support easy clipboard copying. Copy it manually instead.'); }; function selectPassword(e) { var target = $(e.trigger).parent().prev(); if (target.attr('type') === 'text') { target.select(); } } $scope.close = function () { $uibModalInstance.dismiss('close'); }; $scope.showUpgrade = function () { $uibModal.open({ animation: true, templateUrl: 'app/views/paidOrgRequired.html', controller: 'paidOrgRequiredController', resolve: { orgId: function () { return orgId; } } }); }; }]); angular .module('bit.organization') .controller('organizationVaultAttachmentsController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "cipherId", "$analytics", "validationService", "toastr", "$timeout", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, cipherId, $analytics, validationService, toastr, $timeout) { $analytics.eventTrack('organizationVaultAttachmentsController', { category: 'Modal' }); $scope.cipher = {}; $scope.loading = true; $scope.isPremium = true; $scope.canUseAttachments = true; var closing = false; apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) { $scope.cipher = cipherService.decryptCipher(cipher); $scope.loading = false; }, function () { $scope.loading = false; }); $scope.save = function (form) { var files = document.getElementById('file').files; if (!files || !files.length) { validationService.addError(form, 'file', 'Select a file.', true); return; } var key = cryptoService.getOrgKey($scope.cipher.organizationId); $scope.savePromise = cipherService.encryptAttachmentFile(key, files[0]).then(function (encValue) { var fd = new FormData(); var blob = new Blob([encValue.data], { type: 'application/octet-stream' }); fd.append('data', blob, encValue.fileName); return apiService.ciphers.postAttachmentAdmin({ id: cipherId }, fd).$promise; }).then(function (response) { $analytics.eventTrack('Added Attachment'); toastr.success('The attachment has been added.'); closing = true; $uibModalInstance.close(true); }, function (e) { var errors = validationService.parseErrors(e); toastr.error(errors.length ? errors[0] : 'An error occurred.'); }); }; $scope.download = function (attachment) { attachment.loading = true; var key = cryptoService.getOrgKey($scope.cipher.organizationId); cipherService.downloadAndDecryptAttachment(key, attachment, true).then(function (res) { $timeout(function () { attachment.loading = false; }); }, function () { $timeout(function () { attachment.loading = false; }); }); }; $scope.remove = function (attachment) { if (!confirm('Are you sure you want to delete this attachment (' + attachment.fileName + ')?')) { return; } attachment.loading = true; apiService.ciphers.delAttachmentAdmin({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () { attachment.loading = false; $analytics.eventTrack('Deleted Organization Attachment'); var index = $scope.cipher.attachments.indexOf(attachment); if (index > -1) { $scope.cipher.attachments.splice(index, 1); } }, function () { toastr.error('Cannot delete attachment.'); attachment.loading = false; }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; } e.preventDefault(); closing = true; $uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0); }); }]); angular .module('bit.organization') .controller('organizationVaultCipherCollectionsController', ["$scope", "apiService", "$uibModalInstance", "cipherService", "cipher", "$analytics", "collections", function ($scope, apiService, $uibModalInstance, cipherService, cipher, $analytics, collections) { $analytics.eventTrack('organizationVaultCipherCollectionsController', { category: 'Modal' }); $scope.cipher = {}; $scope.collections = []; $scope.selectedCollections = {}; $uibModalInstance.opened.then(function () { var collectionUsed = []; for (var i = 0; i < collections.length; i++) { if (collections[i].id) { collectionUsed.push(collections[i]); } } $scope.collections = collectionUsed; $scope.cipher = cipher; var selectedCollections = {}; if ($scope.cipher.collectionIds) { for (i = 0; i < $scope.cipher.collectionIds.length; i++) { selectedCollections[$scope.cipher.collectionIds[i]] = true; } } $scope.selectedCollections = selectedCollections; }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { collections[$scope.collections[i].id] = true; } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = true; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { return Object.keys($scope.selectedCollections).length === $scope.collections.length; }; $scope.submit = function () { var request = { collectionIds: [] }; for (var id in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(id)) { request.collectionIds.push(id); } } $scope.submitPromise = apiService.ciphers.putCollectionsAdmin({ id: cipher.id }, request) .$promise.then(function (response) { $analytics.eventTrack('Edited Cipher Collections'); $uibModalInstance.close({ action: 'collectionsEdit', collectionIds: request.collectionIds }); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationVaultCipherEventsController', ["$scope", "apiService", "$uibModalInstance", "cipher", "$analytics", "eventService", function ($scope, apiService, $uibModalInstance, cipher, $analytics, eventService) { $analytics.eventTrack('organizationVaultCipherEventsController', { category: 'Modal' }); $scope.cipher = cipher; $scope.events = []; $scope.loading = true; $scope.continuationToken = null; var defaultFilters = eventService.getDefaultDateFilters(); $scope.filterStart = defaultFilters.start; $scope.filterEnd = defaultFilters.end; $uibModalInstance.opened.then(function () { load(); }); $scope.refresh = function () { loadEvents(true); }; $scope.next = function () { loadEvents(false); }; var i = 0, orgUsersUserIdDict = {}, orgUsersIdDict = {}; function load() { apiService.organizationUsers.list({ orgId: cipher.organizationId }).$promise.then(function (list) { var users = []; for (i = 0; i < list.Data.length; i++) { var user = { id: list.Data[i].Id, userId: list.Data[i].UserId, name: list.Data[i].Name, email: list.Data[i].Email }; users.push(user); var displayName = user.name || user.email; orgUsersUserIdDict[user.userId] = displayName; orgUsersIdDict[user.id] = displayName; } $scope.orgUsers = users; return loadEvents(true); }); } function loadEvents(clearExisting) { var filterResult = eventService.formatDateFilters($scope.filterStart, $scope.filterEnd); if (filterResult.error) { alert(filterResult.error); return; } if (clearExisting) { $scope.continuationToken = null; $scope.events = []; } $scope.loading = true; return apiService.events.listCipher({ id: cipher.id, start: filterResult.start, end: filterResult.end, continuationToken: $scope.continuationToken }).$promise.then(function (list) { $scope.continuationToken = list.ContinuationToken; var events = []; for (i = 0; i < list.Data.length; i++) { var userId = list.Data[i].ActingUserId || list.Data[i].UserId; var eventInfo = eventService.getEventInfo(list.Data[i], { cipherInfo: false }); events.push({ message: eventInfo.message, appIcon: eventInfo.appIcon, appName: eventInfo.appName, userId: userId, userName: userId ? (orgUsersUserIdDict[userId] || '-') : '-', date: list.Data[i].Date, ip: list.Data[i].IpAddress }); } if ($scope.events && $scope.events.length > 0) { $scope.events = $scope.events.concat(events); } else { $scope.events = events; } $scope.loading = false; }); } $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('organizationVaultController', ["$scope", "apiService", "cipherService", "$analytics", "$q", "$state", "$localStorage", "$uibModal", "$filter", "authService", "$uibModalStack", "constants", "$timeout", function ($scope, apiService, cipherService, $analytics, $q, $state, $localStorage, $uibModal, $filter, authService, $uibModalStack, constants, $timeout) { $scope.ciphers = []; $scope.collections = []; $scope.loading = true; $scope.useEvents = false; $scope.constants = constants; $scope.filter = undefined; $scope.selectedType = undefined; $scope.selectedCollection = undefined; $scope.selectedAll = true; $scope.selectedTitle = 'All'; $scope.selectedIcon = 'fa-th'; $scope.$on('$viewContentLoaded', function () { authService.getUserProfile().then(function (profile) { if (profile.organizations) { var org = profile.organizations[$state.params.orgId]; $scope.useEvents = !!org.useEvents; } }); var collectionPromise = apiService.collections.listOrganization({ orgId: $state.params.orgId }, function (collections) { var decCollections = [{ id: null, name: 'Unassigned' }]; for (var i = 0; i < collections.Data.length; i++) { var decCollection = cipherService.decryptCollection(collections.Data[i], null, true); decCollections.push(decCollection); } $scope.collections = decCollections; }).$promise; var cipherPromise = apiService.ciphers.listOrganizationDetails({ organizationId: $state.params.orgId }, function (ciphers) { var decCiphers = []; for (var i = 0; i < ciphers.Data.length; i++) { var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]); decCiphers.push(decCipher); } $scope.ciphers = decCiphers; }).$promise; $q.all([collectionPromise, cipherPromise]).then(function () { $scope.loading = false; $timeout(function () { if ($('body').hasClass('control-sidebar-open')) { $("#search").focus(); } }, 500); if ($state.params.search) { $uibModalStack.dismissAll(); $scope.searchVaultText = $state.params.search; } if ($state.params.viewEvents) { $uibModalStack.dismissAll(); var cipher = $filter('filter')($scope.ciphers, { id: $state.params.viewEvents }); if (cipher && cipher.length) { $scope.viewEvents(cipher[0]); } } }); }); $scope.collectionSort = function (item) { if (!item.id) { return ''; } return item.name.toLowerCase(); }; $scope.editCipher = function (cipher) { var editModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultEditCipher.html', controller: 'organizationVaultEditCipherController', resolve: { cipherId: function () { return cipher.id; }, orgId: function () { return $state.params.orgId; } } }); editModel.result.then(function (returnVal) { var index; if (returnVal.action === 'edit') { index = $scope.ciphers.indexOf(cipher); if (index > -1) { returnVal.data.collectionIds = $scope.ciphers[index].collectionIds; $scope.ciphers[index] = returnVal.data; } } else if (returnVal.action === 'delete') { index = $scope.ciphers.indexOf(cipher); if (index > -1) { $scope.ciphers.splice(index, 1); } } }); }; $scope.$on('organizationVaultAddCipher', function (event, args) { $scope.addCipher(); }); $scope.addCipher = function () { var addModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultAddCipher.html', controller: 'organizationVaultAddCipherController', resolve: { orgId: function () { return $state.params.orgId; }, selectedType: function () { return $scope.selectedType; } } }); addModel.result.then(function (addedCipher) { $scope.ciphers.push(addedCipher); }); }; $scope.editCollections = function (cipher) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationVaultCipherCollections.html', controller: 'organizationVaultCipherCollectionsController', resolve: { cipher: function () { return cipher; }, collections: function () { return $scope.collections; } } }); modal.result.then(function (response) { if (response.collectionIds) { cipher.collectionIds = response.collectionIds; } }); }; $scope.viewEvents = function (cipher) { $uibModal.open({ animation: true, templateUrl: 'app/organization/views/organizationVaultCipherEvents.html', controller: 'organizationVaultCipherEventsController', resolve: { cipher: function () { return cipher; } } }); }; $scope.attachments = function (cipher) { authService.getUserProfile().then(function (profile) { return !!profile.organizations[cipher.organizationId].maxStorageGb; }).then(function (useStorage) { if (!useStorage) { $uibModal.open({ animation: true, templateUrl: 'app/views/paidOrgRequired.html', controller: 'paidOrgRequiredController', resolve: { orgId: function () { return cipher.organizationId; } } }); return; } var attachmentModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultAttachments.html', controller: 'organizationVaultAttachmentsController', resolve: { cipherId: function () { return cipher.id; } } }); attachmentModel.result.then(function (hasAttachments) { cipher.hasAttachments = hasAttachments; }); }); }; $scope.deleteCipher = function (cipher) { if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) { return; } apiService.ciphers.delAdmin({ id: cipher.id }, function () { $analytics.eventTrack('Deleted Cipher'); var index = $scope.ciphers.indexOf(cipher); if (index > -1) { $scope.ciphers.splice(index, 1); } }); }; $scope.filterCollection = function (col) { resetSelected(); $scope.selectedCollection = col; $scope.selectedIcon = 'fa-cube'; if (col.id) { $scope.filter = function (c) { return c.collectionIds && c.collectionIds.indexOf(col.id) > -1; }; } else { $scope.filter = function (c) { return !c.collectionIds || c.collectionIds.length === 0; }; } fixLayout(); }; $scope.filterType = function (t) { resetSelected(); $scope.selectedType = t; switch (t) { case constants.cipherType.login: $scope.selectedTitle = 'Login'; $scope.selectedIcon = 'fa-globe'; break; case constants.cipherType.card: $scope.selectedTitle = 'Card'; $scope.selectedIcon = 'fa-credit-card'; break; case constants.cipherType.identity: $scope.selectedTitle = 'Identity'; $scope.selectedIcon = 'fa-id-card-o'; break; case constants.cipherType.secureNote: $scope.selectedTitle = 'Secure Note'; $scope.selectedIcon = 'fa-sticky-note-o'; break; default: break; } $scope.filter = function (c) { return c.type === t; }; fixLayout(); }; $scope.filterAll = function () { resetSelected(); $scope.selectedAll = true; $scope.selectedTitle = 'All'; $scope.selectedIcon = 'fa-th'; $scope.filter = null; fixLayout(); }; function resetSelected() { $scope.selectedCollection = undefined; $scope.selectedType = undefined; $scope.selectedAll = false; } function fixLayout() { if ($.AdminLTE && $.AdminLTE.layout) { $timeout(function () { $.AdminLTE.layout.fix(); }, 0); } } $scope.cipherFilter = function () { return function (cipher) { return !$scope.filter || $scope.filter(cipher); }; }; }]); angular .module('bit.organization') .controller('organizationVaultEditCipherController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "passwordService", "cipherId", "$analytics", "orgId", "$uibModal", "constants", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, cipherId, $analytics, orgId, $uibModal, constants) { $analytics.eventTrack('organizationVaultEditCipherController', { category: 'Modal' }); $scope.cipher = {}; $scope.hideFolders = $scope.hideFavorite = $scope.fromOrg = true; $scope.constants = constants; apiService.ciphers.getAdmin({ id: cipherId }, function (cipher) { $scope.cipher = cipherService.decryptCipher(cipher); $scope.useTotp = $scope.cipher.organizationUseTotp; setUriMatchValues(); }); $scope.save = function (model) { var cipher = cipherService.encryptCipher(model, $scope.cipher.type); $scope.savePromise = apiService.ciphers.putAdmin({ id: cipherId }, cipher, function (cipherResponse) { $analytics.eventTrack('Edited Organization Cipher'); var decCipher = cipherService.decryptCipherPreview(cipherResponse); $uibModalInstance.close({ action: 'edit', data: decCipher }); }).$promise; }; $scope.generatePassword = function () { if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) { $analytics.eventTrack('Generated Password From Edit'); $scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true }); } }; $scope.addUri = function () { if (!$scope.cipher.login) { return; } if (!$scope.cipher.login.uris) { $scope.cipher.login.uris = []; } $scope.cipher.login.uris.push({ uri: null, match: null, matchValue: null }); }; $scope.removeUri = function (uri) { if (!$scope.cipher.login || !$scope.cipher.login.uris) { return; } var index = $scope.cipher.login.uris.indexOf(uri); if (index > -1) { $scope.cipher.login.uris.splice(index, 1); } }; $scope.uriMatchChanged = function (uri) { if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') { uri.match = null; } else { uri.match = parseInt(uri.matchValue); } }; $scope.addField = function () { if (!$scope.cipher.login.fields) { $scope.cipher.login.fields = []; } $scope.cipher.fields.push({ type: constants.fieldType.text.toString(), name: null, value: null }); }; $scope.removeField = function (field) { var index = $scope.cipher.fields.indexOf(field); if (index > -1) { $scope.cipher.fields.splice(index, 1); } }; $scope.clipboardSuccess = function (e) { e.clearSelection(); selectPassword(e); }; $scope.clipboardError = function (e, password) { if (password) { selectPassword(e); } alert('Your web browser does not support easy clipboard copying. Copy it manually instead.'); }; function selectPassword(e) { var target = $(e.trigger).parent().prev(); if (target.attr('type') === 'text') { target.select(); } } $scope.delete = function () { if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) { return; } apiService.ciphers.delAdmin({ id: $scope.cipher.id }, function () { $analytics.eventTrack('Deleted Organization Cipher From Edit'); $uibModalInstance.close({ action: 'delete', data: $scope.cipher.id }); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; $scope.showUpgrade = function () { $uibModal.open({ animation: true, templateUrl: 'app/views/paidOrgRequired.html', controller: 'paidOrgRequiredController', resolve: { orgId: function () { return orgId; } } }); }; function setUriMatchValues() { if ($scope.cipher.login && $scope.cipher.login.uris) { for (var i = 0; i < $scope.cipher.login.uris.length; i++) { $scope.cipher.login.uris[i].matchValue = $scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ? $scope.cipher.login.uris[i].match.toString() : ''; } } } }]); angular .module('bit.services') .factory('apiService', ["$resource", "tokenService", "appSettings", "$httpParamSerializer", "utilsService", function ($resource, tokenService, appSettings, $httpParamSerializer, utilsService) { var _service = {}, _apiUri = appSettings.apiUri, _identityUri = appSettings.identityUri; _service.folders = $resource(_apiUri + '/folders/:id', {}, { get: { method: 'GET', params: { id: '@id' } }, list: { method: 'GET', params: {} }, post: { method: 'POST', params: {} }, put: { method: 'POST', params: { id: '@id' } }, del: { url: _apiUri + '/folders/:id/delete', method: 'POST', params: { id: '@id' } } }); _service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, { get: { method: 'GET', params: { id: '@id' } }, getAdmin: { url: _apiUri + '/ciphers/:id/admin', method: 'GET', params: { id: '@id' } }, getDetails: { url: _apiUri + '/ciphers/:id/details', method: 'GET', params: { id: '@id' } }, list: { method: 'GET', params: {} }, listOrganizationDetails: { url: _apiUri + '/ciphers/organization-details', method: 'GET', params: {} }, post: { method: 'POST', params: {} }, postAdmin: { url: _apiUri + '/ciphers/admin', method: 'POST', params: {} }, put: { method: 'POST', params: { id: '@id' } }, putAdmin: { url: _apiUri + '/ciphers/:id/admin', method: 'POST', params: { id: '@id' } }, 'import': { url: _apiUri + '/ciphers/import', method: 'POST', params: {} }, importOrg: { url: _apiUri + '/ciphers/import-organization?organizationId=:orgId', method: 'POST', params: { orgId: '@orgId' } }, putPartial: { url: _apiUri + '/ciphers/:id/partial', method: 'POST', params: { id: '@id' } }, putShare: { url: _apiUri + '/ciphers/:id/share', method: 'POST', params: { id: '@id' } }, putCollections: { url: _apiUri + '/ciphers/:id/collections', method: 'POST', params: { id: '@id' } }, putCollectionsAdmin: { url: _apiUri + '/ciphers/:id/collections-admin', method: 'POST', params: { id: '@id' } }, del: { url: _apiUri + '/ciphers/:id/delete', method: 'POST', params: { id: '@id' } }, delAdmin: { url: _apiUri + '/ciphers/:id/delete-admin', method: 'POST', params: { id: '@id' } }, delMany: { url: _apiUri + '/ciphers/delete', method: 'POST' }, moveMany: { url: _apiUri + '/ciphers/move', method: 'POST' }, purge: { url: _apiUri + '/ciphers/purge', method: 'POST' }, postAttachment: { url: _apiUri + '/ciphers/:id/attachment', method: 'POST', headers: { 'Content-Type': undefined }, params: { id: '@id' } }, postAttachmentAdmin: { url: _apiUri + '/ciphers/:id/attachment-admin', method: 'POST', headers: { 'Content-Type': undefined }, params: { id: '@id' } }, postShareAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/share?organizationId=:orgId', method: 'POST', headers: { 'Content-Type': undefined }, params: { id: '@id', attachmentId: '@attachmentId', orgId: '@orgId' } }, delAttachment: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } }, delAttachmentAdmin: { url: _apiUri + '/ciphers/:id/attachment/:attachmentId/delete-admin', method: 'POST', params: { id: '@id', attachmentId: '@attachmentId' } } }); _service.organizations = $resource(_apiUri + '/organizations/:id', {}, { get: { method: 'GET', params: { id: '@id' } }, getBilling: { url: _apiUri + '/organizations/:id/billing', method: 'GET', params: { id: '@id' } }, getLicense: { url: _apiUri + '/organizations/:id/license', method: 'GET', params: { id: '@id' } }, list: { method: 'GET', params: {} }, post: { method: 'POST', params: {} }, put: { method: 'POST', params: { id: '@id' } }, putPayment: { url: _apiUri + '/organizations/:id/payment', method: 'POST', params: { id: '@id' } }, putSeat: { url: _apiUri + '/organizations/:id/seat', method: 'POST', params: { id: '@id' } }, putStorage: { url: _apiUri + '/organizations/:id/storage', method: 'POST', params: { id: '@id' } }, putUpgrade: { url: _apiUri + '/organizations/:id/upgrade', method: 'POST', params: { id: '@id' } }, putCancel: { url: _apiUri + '/organizations/:id/cancel', method: 'POST', params: { id: '@id' } }, putReinstate: { url: _apiUri + '/organizations/:id/reinstate', method: 'POST', params: { id: '@id' } }, postLeave: { url: _apiUri + '/organizations/:id/leave', method: 'POST', params: { id: '@id' } }, postVerifyBank: { url: _apiUri + '/organizations/:id/verify-bank', method: 'POST', params: { id: '@id' } }, del: { url: _apiUri + '/organizations/:id/delete', method: 'POST', params: { id: '@id' } }, postLicense: { url: _apiUri + '/organizations/license', method: 'POST', headers: { 'Content-Type': undefined } }, putLicense: { url: _apiUri + '/organizations/:id/license', method: 'POST', headers: { 'Content-Type': undefined } } }); _service.organizationUsers = $resource(_apiUri + '/organizations/:orgId/users/:id', {}, { get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } }, list: { method: 'GET', params: { orgId: '@orgId' } }, listGroups: { url: _apiUri + '/organizations/:orgId/users/:id/groups', method: 'GET', params: { id: '@id', orgId: '@orgId' }, isArray: true }, invite: { url: _apiUri + '/organizations/:orgId/users/invite', method: 'POST', params: { orgId: '@orgId' } }, reinvite: { url: _apiUri + '/organizations/:orgId/users/:id/reinvite', method: 'POST', params: { id: '@id', orgId: '@orgId' } }, accept: { url: _apiUri + '/organizations/:orgId/users/:id/accept', method: 'POST', params: { id: '@id', orgId: '@orgId' } }, confirm: { url: _apiUri + '/organizations/:orgId/users/:id/confirm', method: 'POST', params: { id: '@id', orgId: '@orgId' } }, put: { method: 'POST', params: { id: '@id', orgId: '@orgId' } }, putGroups: { url: _apiUri + '/organizations/:orgId/users/:id/groups', method: 'POST', params: { id: '@id', orgId: '@orgId' } }, del: { url: _apiUri + '/organizations/:orgId/users/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } } }); _service.collections = $resource(_apiUri + '/organizations/:orgId/collections/:id', {}, { get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } }, getDetails: { url: _apiUri + '/organizations/:orgId/collections/:id/details', method: 'GET', params: { id: '@id', orgId: '@orgId' } }, listMe: { url: _apiUri + '/collections?writeOnly=:writeOnly', method: 'GET', params: { writeOnly: '@writeOnly' } }, listOrganization: { method: 'GET', params: { orgId: '@orgId' } }, listUsers: { url: _apiUri + '/organizations/:orgId/collections/:id/users', method: 'GET', params: { id: '@id', orgId: '@orgId' } }, post: { method: 'POST', params: { orgId: '@orgId' } }, put: { method: 'POST', params: { id: '@id', orgId: '@orgId' } }, del: { url: _apiUri + '/organizations/:orgId/collections/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } }, delUser: { url: _apiUri + '/organizations/:orgId/collections/:id/delete-user/:orgUserId', method: 'POST', params: { id: '@id', orgId: '@orgId', orgUserId: '@orgUserId' } } }); _service.groups = $resource(_apiUri + '/organizations/:orgId/groups/:id', {}, { get: { method: 'GET', params: { id: '@id', orgId: '@orgId' } }, getDetails: { url: _apiUri + '/organizations/:orgId/groups/:id/details', method: 'GET', params: { id: '@id', orgId: '@orgId' } }, listOrganization: { method: 'GET', params: { orgId: '@orgId' } }, listUsers: { url: _apiUri + '/organizations/:orgId/groups/:id/users', method: 'GET', params: { id: '@id', orgId: '@orgId' } }, post: { method: 'POST', params: { orgId: '@orgId' } }, put: { method: 'POST', params: { id: '@id', orgId: '@orgId' } }, del: { url: _apiUri + '/organizations/:orgId/groups/:id/delete', method: 'POST', params: { id: '@id', orgId: '@orgId' } }, delUser: { url: _apiUri + '/organizations/:orgId/groups/:id/delete-user/:orgUserId', method: 'POST', params: { id: '@id', orgId: '@orgId', orgUserId: '@orgUserId' } } }); _service.accounts = $resource(_apiUri + '/accounts', {}, { register: { url: _apiUri + '/accounts/register', method: 'POST', params: {} }, emailToken: { url: _apiUri + '/accounts/email-token', method: 'POST', params: {} }, email: { url: _apiUri + '/accounts/email', method: 'POST', params: {} }, verifyEmailToken: { url: _apiUri + '/accounts/verify-email-token', method: 'POST', params: {} }, verifyEmail: { url: _apiUri + '/accounts/verify-email', method: 'POST', params: {} }, postDeleteRecoverToken: { url: _apiUri + '/accounts/delete-recover-token', method: 'POST', params: {} }, postDeleteRecover: { url: _apiUri + '/accounts/delete-recover', method: 'POST', params: {} }, putPassword: { url: _apiUri + '/accounts/password', method: 'POST', params: {} }, getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} }, putProfile: { url: _apiUri + '/accounts/profile', method: 'POST', params: {} }, getDomains: { url: _apiUri + '/accounts/domains', method: 'GET', params: {} }, putDomains: { url: _apiUri + '/accounts/domains', method: 'POST', params: {} }, postPasswordHint: { url: _apiUri + '/accounts/password-hint', method: 'POST', params: {} }, putSecurityStamp: { url: _apiUri + '/accounts/security-stamp', method: 'POST', params: {} }, putKeys: { url: _apiUri + '/accounts/keys', method: 'POST', params: {} }, putKey: { url: _apiUri + '/accounts/key', method: 'POST', params: {} }, 'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} }, postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} }, putStorage: { url: _apiUri + '/accounts/storage', method: 'POST', params: {} }, putPayment: { url: _apiUri + '/accounts/payment', method: 'POST', params: {} }, putCancelPremium: { url: _apiUri + '/accounts/cancel-premium', method: 'POST', params: {} }, putReinstatePremium: { url: _apiUri + '/accounts/reinstate-premium', method: 'POST', params: {} }, getBilling: { url: _apiUri + '/accounts/billing', method: 'GET', params: {} }, postPremium: { url: _apiUri + '/accounts/premium', method: 'POST', headers: { 'Content-Type': undefined } }, putLicense: { url: _apiUri + '/accounts/license', method: 'POST', headers: { 'Content-Type': undefined } } }); _service.twoFactor = $resource(_apiUri + '/two-factor', {}, { list: { method: 'GET', params: {} }, listOrganization: { url: _apiUri + '/organizations/:orgId/two-factor', method: 'GET', params: { orgId: '@orgId' } }, getEmail: { url: _apiUri + '/two-factor/get-email', method: 'POST', params: {} }, getU2f: { url: _apiUri + '/two-factor/get-u2f', method: 'POST', params: {} }, getDuo: { url: _apiUri + '/two-factor/get-duo', method: 'POST', params: {} }, getOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/get-duo', method: 'POST', params: { orgId: '@orgId' } }, getAuthenticator: { url: _apiUri + '/two-factor/get-authenticator', method: 'POST', params: {} }, getYubi: { url: _apiUri + '/two-factor/get-yubikey', method: 'POST', params: {} }, sendEmail: { url: _apiUri + '/two-factor/send-email', method: 'POST', params: {} }, sendEmailLogin: { url: _apiUri + '/two-factor/send-email-login', method: 'POST', params: {} }, putEmail: { url: _apiUri + '/two-factor/email', method: 'POST', params: {} }, putU2f: { url: _apiUri + '/two-factor/u2f', method: 'POST', params: {} }, putAuthenticator: { url: _apiUri + '/two-factor/authenticator', method: 'POST', params: {} }, putDuo: { url: _apiUri + '/two-factor/duo', method: 'POST', params: {} }, putOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/duo', method: 'POST', params: { orgId: '@orgId' } }, putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} }, disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} }, disableOrganization: { url: _apiUri + '/organizations/:orgId/two-factor/disable', method: 'POST', params: { orgId: '@orgId' } }, recover: { url: _apiUri + '/two-factor/recover', method: 'POST', params: {} }, getRecover: { url: _apiUri + '/two-factor/get-recover', method: 'POST', params: {} } }); _service.settings = $resource(_apiUri + '/settings', {}, { getDomains: { url: _apiUri + '/settings/domains', method: 'GET', params: {} }, putDomains: { url: _apiUri + '/settings/domains', method: 'POST', params: {} }, }); _service.users = $resource(_apiUri + '/users/:id', {}, { getPublicKey: { url: _apiUri + '/users/:id/public-key', method: 'GET', params: { id: '@id' } } }); _service.events = $resource(_apiUri + '/events', {}, { list: { method: 'GET', params: {} }, listOrganization: { url: _apiUri + '/organizations/:orgId/events', method: 'GET', params: { id: '@orgId' } }, listCipher: { url: _apiUri + '/ciphers/:id/events', method: 'GET', params: { id: '@id' } }, listOrganizationUser: { url: _apiUri + '/organizations/:orgId/users/:id/events', method: 'GET', params: { orgId: '@orgId', id: '@id' } } }); _service.identity = $resource(_identityUri + '/connect', {}, { token: { url: _identityUri + '/connect/token', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Device-Type': utilsService.getDeviceType() }, transformRequest: transformUrlEncoded, skipAuthorization: true, params: {} } }); _service.hibp = $resource('https://haveibeenpwned.com/api/v2/breachedaccount/:email', {}, { get: { method: 'GET', params: { email: '@email' }, isArray: true }, }); function transformUrlEncoded(data) { return $httpParamSerializer(data); } return _service; }]); angular .module('bit.services') .factory('authService', ["cryptoService", "apiService", "tokenService", "$q", "jwtHelper", "$rootScope", "constants", function (cryptoService, apiService, tokenService, $q, jwtHelper, $rootScope, constants) { var _service = {}, _userProfile = null; _service.logIn = function (email, masterPassword, token, provider, remember) { email = email.toLowerCase(); var deferred = $q.defer(); var makeResult; cryptoService.makeKeyAndHash(email, masterPassword).then(function (result) { makeResult = result; var request = { username: email, password: result.hash, grant_type: 'password', scope: 'api offline_access', client_id: 'web' }; // TODO: device information one day? if (token && typeof (provider) !== 'undefined' && provider !== null) { remember = remember || remember !== false; request.twoFactorToken = token; request.twoFactorProvider = provider; request.twoFactorRemember = remember ? '1' : '0'; } else if (tokenService.getTwoFactorToken(email)) { request.twoFactorToken = tokenService.getTwoFactorToken(email); request.twoFactorProvider = constants.twoFactorProvider.remember; request.twoFactorRemember = '0'; } return apiService.identity.token(request).$promise; }).then(function (response) { if (!response || !response.access_token) { return; } tokenService.setToken(response.access_token); tokenService.setRefreshToken(response.refresh_token); cryptoService.setKey(makeResult.key); if (response.TwoFactorToken) { tokenService.setTwoFactorToken(response.TwoFactorToken, email); } if (response.Key) { cryptoService.setEncKey(response.Key, makeResult.key); } if (response.PrivateKey) { cryptoService.setPrivateKey(response.PrivateKey); return true; } else { return cryptoService.makeKeyPair(); } }).then(function (keyResults) { if (keyResults === true) { return; } cryptoService.setPrivateKey(keyResults.privateKeyEnc); return apiService.accounts.putKeys({ publicKey: keyResults.publicKey, encryptedPrivateKey: keyResults.privateKeyEnc }).$promise; }).then(function () { return _service.setUserProfile(); }).then(function () { deferred.resolve(); }, function (error) { _service.logOut(); if (error.status === 400 && error.data.TwoFactorProviders2 && Object.keys(error.data.TwoFactorProviders2).length) { tokenService.clearTwoFactorToken(email); deferred.resolve(error.data.TwoFactorProviders2); } else { deferred.reject(error); } }); return deferred.promise; }; _service.logOut = function () { tokenService.clearTokens(); cryptoService.clearKeys(); $rootScope.vaultCiphers = $rootScope.vaultFolders = $rootScope.vaultCollections = null; _userProfile = null; }; _service.getUserProfile = function () { if (!_userProfile) { return _service.setUserProfile(); } var deferred = $q.defer(); deferred.resolve(_userProfile); return deferred.promise; }; var _setDeferred = null; _service.setUserProfile = function () { if (_setDeferred && _setDeferred.promise.$$state.status === 0) { return _setDeferred.promise; } _setDeferred = $q.defer(); var token = tokenService.getToken(); if (!token) { _setDeferred.reject(); return _setDeferred.promise; } apiService.accounts.getProfile({}, function (profile) { _userProfile = { id: profile.Id, email: profile.Email, emailVerified: profile.EmailVerified, premium: profile.Premium, extended: { name: profile.Name, twoFactorEnabled: profile.TwoFactorEnabled, culture: profile.Culture } }; if (profile.Organizations) { var orgs = {}; for (var i = 0; i < profile.Organizations.length; i++) { orgs[profile.Organizations[i].Id] = { id: profile.Organizations[i].Id, name: profile.Organizations[i].Name, key: profile.Organizations[i].Key, status: profile.Organizations[i].Status, type: profile.Organizations[i].Type, enabled: profile.Organizations[i].Enabled, maxCollections: profile.Organizations[i].MaxCollections, maxStorageGb: profile.Organizations[i].MaxStorageGb, seats: profile.Organizations[i].Seats, useGroups: profile.Organizations[i].UseGroups, useDirectory: profile.Organizations[i].UseDirectory, useEvents: profile.Organizations[i].UseEvents, use2fa: profile.Organizations[i].Use2fa, useTotp: profile.Organizations[i].UseTotp }; } _userProfile.organizations = orgs; cryptoService.setOrgKeys(orgs); _setDeferred.resolve(_userProfile); } }, function (error) { _setDeferred.reject(error); }); return _setDeferred.promise; }; _service.addProfileOrganizationOwner = function (org, keyCt) { return _service.getUserProfile().then(function (profile) { if (profile) { if (!profile.organizations) { profile.organizations = {}; } var o = { id: org.Id, name: org.Name, key: keyCt, status: 2, // 2 = Confirmed type: 0, // 0 = Owner enabled: true, maxCollections: org.MaxCollections, maxStorageGb: org.MaxStorageGb, seats: org.Seats, useGroups: org.UseGroups, useDirectory: org.UseDirectory, useEvents: org.UseEvents, use2fa: org.Use2fa, useTotp: org.UseTotp }; profile.organizations[o.id] = o; _userProfile = profile; cryptoService.addOrgKey(o.id, o.key); } }); }; _service.removeProfileOrganization = function (orgId) { return _service.getUserProfile().then(function (profile) { if (profile) { if (profile.organizations && profile.organizations.hasOwnProperty(orgId)) { delete profile.organizations[orgId]; _userProfile = profile; } cryptoService.clearOrgKey(orgId); } }); }; _service.updateProfileOrganization = function (org) { return _service.getUserProfile().then(function (profile) { if (profile) { if (profile.organizations && org.Id in profile.organizations) { profile.organizations[org.Id].name = org.Name; _userProfile = profile; } } }); }; _service.updateProfilePremium = function (isPremium) { return _service.getUserProfile().then(function (profile) { if (profile) { profile.premium = isPremium; _userProfile = profile; } }); }; _service.isAuthenticated = function () { return tokenService.getToken() !== null; }; _service.refreshAccessToken = function () { var refreshToken = tokenService.getRefreshToken(); if (!refreshToken) { return $q(function (resolve, reject) { resolve(null); }); } return apiService.identity.token({ grant_type: 'refresh_token', client_id: 'web', refresh_token: refreshToken }).$promise.then(function (response) { tokenService.setToken(response.access_token); tokenService.setRefreshToken(response.refresh_token); return response.access_token; }, function (response) { }); }; return _service; }]); angular .module('bit.services') .factory('cipherService', ["cryptoService", "apiService", "$q", "$window", "constants", "appSettings", "$localStorage", function (cryptoService, apiService, $q, $window, constants, appSettings, $localStorage) { var _service = { disableWebsiteIcons: $localStorage.disableWebsiteIcons }; _service.decryptCiphers = function (encryptedCiphers) { if (!encryptedCiphers) throw "encryptedCiphers is undefined or null"; var unencryptedCiphers = []; for (var i = 0; i < encryptedCiphers.length; i++) { unencryptedCiphers.push(_service.decryptCipher(encryptedCiphers[i])); } return unencryptedCiphers; }; _service.decryptCipher = function (encryptedCipher) { if (!encryptedCipher) throw "encryptedCipher is undefined or null"; var key = null; if (encryptedCipher.OrganizationId) { key = cryptoService.getOrgKey(encryptedCipher.OrganizationId); } var cipher = { id: encryptedCipher.Id, organizationId: encryptedCipher.OrganizationId, collectionIds: encryptedCipher.CollectionIds || [], 'type': encryptedCipher.Type, name: cryptoService.decrypt(encryptedCipher.Name, key), notes: _service.decryptProperty(encryptedCipher.Notes, key, true, false), fields: _service.decryptFields(key, encryptedCipher.Fields), folderId: encryptedCipher.FolderId, favorite: encryptedCipher.Favorite, edit: encryptedCipher.Edit, organizationUseTotp: encryptedCipher.OrganizationUseTotp, attachments: null, icon: null }; var i; switch (cipher.type) { case constants.cipherType.login: cipher.login = { username: _service.decryptProperty(encryptedCipher.Login.Username, key, true, false), password: _service.decryptProperty(encryptedCipher.Login.Password, key, true, false), totp: _service.decryptProperty(encryptedCipher.Login.Totp, key, true, false), uris: null }; if (encryptedCipher.Login.Uris) { cipher.login.uris = []; for (i = 0; i < encryptedCipher.Login.Uris.length; i++) { cipher.login.uris.push({ uri: _service.decryptProperty(encryptedCipher.Login.Uris[i].Uri, key, true, false), match: encryptedCipher.Login.Uris[i].Match }); } } cipher.icon = 'fa-globe'; break; case constants.cipherType.secureNote: cipher.secureNote = { type: encryptedCipher.SecureNote.Type }; cipher.icon = 'fa-sticky-note-o'; break; case constants.cipherType.card: cipher.card = { cardholderName: _service.decryptProperty(encryptedCipher.Card.CardholderName, key, true, false), number: _service.decryptProperty(encryptedCipher.Card.Number, key, true, false), brand: _service.decryptProperty(encryptedCipher.Card.Brand, key, true, false), expMonth: _service.decryptProperty(encryptedCipher.Card.ExpMonth, key, true, false), expYear: _service.decryptProperty(encryptedCipher.Card.ExpYear, key, true, false), code: _service.decryptProperty(encryptedCipher.Card.Code, key, true, false) }; cipher.icon = 'fa-credit-card'; break; case constants.cipherType.identity: cipher.identity = { title: _service.decryptProperty(encryptedCipher.Identity.Title, key, true, false), firstName: _service.decryptProperty(encryptedCipher.Identity.FirstName, key, true, false), middleName: _service.decryptProperty(encryptedCipher.Identity.MiddleName, key, true, false), lastName: _service.decryptProperty(encryptedCipher.Identity.LastName, key, true, false), address1: _service.decryptProperty(encryptedCipher.Identity.Address1, key, true, false), address2: _service.decryptProperty(encryptedCipher.Identity.Address2, key, true, false), address3: _service.decryptProperty(encryptedCipher.Identity.Address3, key, true, false), city: _service.decryptProperty(encryptedCipher.Identity.City, key, true, false), state: _service.decryptProperty(encryptedCipher.Identity.State, key, true, false), postalCode: _service.decryptProperty(encryptedCipher.Identity.PostalCode, key, true, false), country: _service.decryptProperty(encryptedCipher.Identity.Country, key, true, false), company: _service.decryptProperty(encryptedCipher.Identity.Company, key, true, false), email: _service.decryptProperty(encryptedCipher.Identity.Email, key, true, false), phone: _service.decryptProperty(encryptedCipher.Identity.Phone, key, true, false), ssn: _service.decryptProperty(encryptedCipher.Identity.SSN, key, true, false), username: _service.decryptProperty(encryptedCipher.Identity.Username, key, true, false), passportNumber: _service.decryptProperty(encryptedCipher.Identity.PassportNumber, key, true, false), licenseNumber: _service.decryptProperty(encryptedCipher.Identity.LicenseNumber, key, true, false) }; cipher.icon = 'fa-id-card-o'; break; default: break; } if (!encryptedCipher.Attachments) { return cipher; } cipher.attachments = []; for (i = 0; i < encryptedCipher.Attachments.length; i++) { cipher.attachments.push(_service.decryptAttachment(key, encryptedCipher.Attachments[i])); } return cipher; }; _service.decryptCipherPreview = function (encryptedCipher) { if (!encryptedCipher) throw "encryptedCipher is undefined or null"; var key = null; if (encryptedCipher.OrganizationId) { key = cryptoService.getOrgKey(encryptedCipher.OrganizationId); } var cipher = { id: encryptedCipher.Id, organizationId: encryptedCipher.OrganizationId, collectionIds: encryptedCipher.CollectionIds || [], 'type': encryptedCipher.Type, name: _service.decryptProperty(encryptedCipher.Name, key, false, true), folderId: encryptedCipher.FolderId, favorite: encryptedCipher.Favorite, edit: encryptedCipher.Edit, organizationUseTotp: encryptedCipher.OrganizationUseTotp, hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0, meta: {}, icon: null }; switch (cipher.type) { case constants.cipherType.login: cipher.subTitle = _service.decryptProperty(encryptedCipher.Login.Username, key, true, true); cipher.meta.password = _service.decryptProperty(encryptedCipher.Login.Password, key, true, true); cipher.meta.uri = null; if (encryptedCipher.Login.Uris && encryptedCipher.Login.Uris.length) { cipher.meta.uri = _service.decryptProperty(encryptedCipher.Login.Uris[0].Uri, key, true, true); } setLoginIcon(cipher, cipher.meta.uri, true); break; case constants.cipherType.secureNote: cipher.subTitle = null; cipher.icon = 'fa-sticky-note-o'; break; case constants.cipherType.card: cipher.subTitle = ''; cipher.meta.number = _service.decryptProperty(encryptedCipher.Card.Number, key, true, true); var brand = _service.decryptProperty(encryptedCipher.Card.Brand, key, true, true); if (brand) { cipher.subTitle = brand; } if (cipher.meta.number && cipher.meta.number.length >= 4) { if (cipher.subTitle !== '') { cipher.subTitle += ', '; } cipher.subTitle += ('*' + cipher.meta.number.substr(cipher.meta.number.length - 4)); } cipher.icon = 'fa-credit-card'; break; case constants.cipherType.identity: var firstName = _service.decryptProperty(encryptedCipher.Identity.FirstName, key, true, true); var lastName = _service.decryptProperty(encryptedCipher.Identity.LastName, key, true, true); cipher.subTitle = ''; if (firstName) { cipher.subTitle = firstName; } if (lastName) { if (cipher.subTitle !== '') { cipher.subTitle += ' '; } cipher.subTitle += lastName; } cipher.icon = 'fa-id-card-o'; break; default: break; } if (cipher.subTitle === '') { cipher.subTitle = null; } return cipher; }; function setLoginIcon(cipher, uri, setImage) { if (!_service.disableWebsiteIcons && uri) { var hostnameUri = uri, isWebsite = false; if (hostnameUri.indexOf('androidapp://') === 0) { cipher.icon = 'fa-android'; } else if (hostnameUri.indexOf('iosapp://') === 0) { cipher.icon = 'fa-apple'; } else if (hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) { hostnameUri = "http://" + hostnameUri; isWebsite = true; } else { isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1; } if (setImage && isWebsite) { try { var url = new URL(hostnameUri); cipher.meta.image = appSettings.iconsUri + '/' + url.hostname + '/icon.png'; } catch (e) { } } } if (!cipher.icon) { cipher.icon = 'fa-globe'; } } _service.decryptAttachment = function (key, encryptedAttachment) { if (!encryptedAttachment) throw "encryptedAttachment is undefined or null"; return { id: encryptedAttachment.Id, url: encryptedAttachment.Url, fileName: cryptoService.decrypt(encryptedAttachment.FileName, key), size: encryptedAttachment.SizeName }; }; _service.downloadAndDecryptAttachment = function (key, decryptedAttachment, openDownload) { var deferred = $q.defer(); var req = new XMLHttpRequest(); req.open('GET', decryptedAttachment.url, true); req.responseType = 'arraybuffer'; req.onload = function (evt) { if (!req.response) { deferred.reject('No response'); // error return; } cryptoService.decryptFromBytes(req.response, key).then(function (decBuf) { if (openDownload) { var blob = new Blob([decBuf]); // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx if ($window.navigator.msSaveOrOpenBlob) { $window.navigator.msSaveBlob(blob, decryptedAttachment.fileName); } else { var a = $window.document.createElement('a'); a.href = $window.URL.createObjectURL(blob); a.download = decryptedAttachment.fileName; $window.document.body.appendChild(a); a.click(); $window.document.body.removeChild(a); } } deferred.resolve(new Uint8Array(decBuf)); }); }; req.send(null); return deferred.promise; }; _service.decryptFields = function (key, encryptedFields) { var unencryptedFields = []; if (encryptedFields) { for (var i = 0; i < encryptedFields.length; i++) { unencryptedFields.push(_service.decryptField(key, encryptedFields[i])); } } return unencryptedFields; }; _service.decryptField = function (key, encryptedField) { if (!encryptedField) throw "encryptedField is undefined or null"; return { type: encryptedField.Type.toString(), name: encryptedField.Name && encryptedField.Name !== '' ? cryptoService.decrypt(encryptedField.Name, key) : null, value: encryptedField.Value && encryptedField.Value !== '' ? cryptoService.decrypt(encryptedField.Value, key) : null }; }; _service.decryptFolders = function (encryptedFolders) { if (!encryptedFolders) throw "encryptedFolders is undefined or null"; var unencryptedFolders = []; for (var i = 0; i < encryptedFolders.length; i++) { unencryptedFolders.push(_service.decryptFolder(encryptedFolders[i])); } return unencryptedFolders; }; _service.decryptFolder = function (encryptedFolder) { if (!encryptedFolder) throw "encryptedFolder is undefined or null"; return { id: encryptedFolder.Id, name: cryptoService.decrypt(encryptedFolder.Name) }; }; _service.decryptFolderPreview = function (encryptedFolder) { if (!encryptedFolder) throw "encryptedFolder is undefined or null"; return { id: encryptedFolder.Id, name: _service.decryptProperty(encryptedFolder.Name, null, false, true) }; }; _service.decryptCollections = function (encryptedCollections, orgId, catchError) { if (!encryptedCollections) throw "encryptedCollections is undefined or null"; var unencryptedCollections = []; for (var i = 0; i < encryptedCollections.length; i++) { unencryptedCollections.push(_service.decryptCollection(encryptedCollections[i], orgId, catchError)); } return unencryptedCollections; }; _service.decryptCollection = function (encryptedCollection, orgId, catchError) { if (!encryptedCollection) throw "encryptedCollection is undefined or null"; catchError = catchError === true ? true : false; orgId = orgId || encryptedCollection.OrganizationId; var key = cryptoService.getOrgKey(orgId); return { id: encryptedCollection.Id, name: catchError ? _service.decryptProperty(encryptedCollection.Name, key, false, true) : cryptoService.decrypt(encryptedCollection.Name, key) }; }; _service.decryptProperty = function (property, key, checkEmpty, showError) { if (checkEmpty && (!property || property === '')) { return null; } try { property = cryptoService.decrypt(property, key); } catch (err) { property = null; } return property || (showError ? '[error: cannot decrypt]' : null); }; _service.encryptCiphers = function (unencryptedCiphers, key) { if (!unencryptedCiphers) throw "unencryptedCiphers is undefined or null"; var encryptedCiphers = []; for (var i = 0; i < unencryptedCiphers.length; i++) { encryptedCiphers.push(_service.encryptCipher(unencryptedCiphers[i], null, key)); } return encryptedCiphers; }; _service.encryptCipher = function (unencryptedCipher, type, key, attachments) { if (!unencryptedCipher) throw "unencryptedCipher is undefined or null"; if (unencryptedCipher.organizationId) { key = key || cryptoService.getOrgKey(unencryptedCipher.organizationId); } var cipher = { id: unencryptedCipher.id, 'type': type || unencryptedCipher.type, organizationId: unencryptedCipher.organizationId || null, folderId: unencryptedCipher.folderId === '' ? null : unencryptedCipher.folderId, favorite: unencryptedCipher.favorite !== null ? unencryptedCipher.favorite : false, name: cryptoService.encrypt(unencryptedCipher.name, key), notes: encryptProperty(unencryptedCipher.notes, key), fields: _service.encryptFields(unencryptedCipher.fields, key) }; var i; switch (cipher.type) { case constants.cipherType.login: var loginData = unencryptedCipher.login; cipher.login = { username: encryptProperty(loginData.username, key), password: encryptProperty(loginData.password, key), totp: encryptProperty(loginData.totp, key) }; if (loginData.uris && loginData.uris.length) { cipher.login.uris = []; for (i = 0; i < loginData.uris.length; i++) { cipher.login.uris.push({ uri: encryptProperty(loginData.uris[i].uri, key), match: loginData.uris[i].match }); } } break; case constants.cipherType.secureNote: cipher.secureNote = { type: unencryptedCipher.secureNote.type }; break; case constants.cipherType.card: var cardData = unencryptedCipher.card; cipher.card = { cardholderName: encryptProperty(cardData.cardholderName, key), brand: encryptProperty(cardData.brand, key), number: encryptProperty(cardData.number, key), expMonth: encryptProperty(cardData.expMonth, key), expYear: encryptProperty(cardData.expYear, key), code: encryptProperty(cardData.code, key) }; break; case constants.cipherType.identity: var identityData = unencryptedCipher.identity; cipher.identity = { title: encryptProperty(identityData.title, key), firstName: encryptProperty(identityData.firstName, key), middleName: encryptProperty(identityData.middleName, key), lastName: encryptProperty(identityData.lastName, key), address1: encryptProperty(identityData.address1, key), address2: encryptProperty(identityData.address2, key), address3: encryptProperty(identityData.address3, key), city: encryptProperty(identityData.city, key), state: encryptProperty(identityData.state, key), postalCode: encryptProperty(identityData.postalCode, key), country: encryptProperty(identityData.country, key), company: encryptProperty(identityData.company, key), email: encryptProperty(identityData.email, key), phone: encryptProperty(identityData.phone, key), ssn: encryptProperty(identityData.ssn, key), username: encryptProperty(identityData.username, key), passportNumber: encryptProperty(identityData.passportNumber, key), licenseNumber: encryptProperty(identityData.licenseNumber, key) }; break; default: break; } if (unencryptedCipher.attachments && attachments) { cipher.attachments = {}; for (i = 0; i < unencryptedCipher.attachments.length; i++) { cipher.attachments[unencryptedCipher.attachments[i].id] = cryptoService.encrypt(unencryptedCipher.attachments[i].fileName, key); } } return cipher; }; _service.encryptAttachmentFile = function (key, unencryptedFile) { var deferred = $q.defer(); if (unencryptedFile.size > 104857600) { // 100 MB deferred.reject('Maximum file size is 100 MB.'); return; } var reader = new FileReader(); reader.readAsArrayBuffer(unencryptedFile); reader.onload = function (evt) { cryptoService.encryptToBytes(evt.target.result, key).then(function (encData) { deferred.resolve({ fileName: cryptoService.encrypt(unencryptedFile.name, key), data: new Uint8Array(encData), size: unencryptedFile.size }); }); }; reader.onerror = function (evt) { deferred.reject('Error reading file.'); }; return deferred.promise; }; _service.encryptFields = function (unencryptedFields, key) { if (!unencryptedFields || !unencryptedFields.length) { return null; } var encFields = []; for (var i = 0; i < unencryptedFields.length; i++) { if (!unencryptedFields[i]) { continue; } encFields.push(_service.encryptField(unencryptedFields[i], key)); } return encFields; }; _service.encryptField = function (unencryptedField, key) { if (!unencryptedField) throw "unencryptedField is undefined or null"; return { type: parseInt(unencryptedField.type), name: unencryptedField.name ? cryptoService.encrypt(unencryptedField.name, key) : null, value: unencryptedField.value ? cryptoService.encrypt(unencryptedField.value.toString(), key) : null }; }; _service.encryptFolders = function (unencryptedFolders, key) { if (!unencryptedFolders) throw "unencryptedFolders is undefined or null"; var encryptedFolders = []; for (var i = 0; i < unencryptedFolders.length; i++) { encryptedFolders.push(_service.encryptFolder(unencryptedFolders[i], key)); } return encryptedFolders; }; _service.encryptFolder = function (unencryptedFolder, key) { if (!unencryptedFolder) throw "unencryptedFolder is undefined or null"; return { id: unencryptedFolder.id, name: cryptoService.encrypt(unencryptedFolder.name, key) }; }; _service.encryptCollections = function (unencryptedCollections, orgId) { if (!unencryptedCollections) throw "unencryptedCollections is undefined or null"; var encryptedCollections = []; for (var i = 0; i < unencryptedCollections.length; i++) { encryptedCollections.push(_service.encryptCollection(unencryptedCollections[i], orgId)); } return encryptedCollections; }; _service.encryptCollection = function (unencryptedCollection, orgId) { if (!unencryptedCollection) throw "unencryptedCollection is undefined or null"; return { id: unencryptedCollection.id, name: cryptoService.encrypt(unencryptedCollection.name, cryptoService.getOrgKey(orgId)) }; }; function encryptProperty(property, key) { return !property || property === '' ? null : cryptoService.encrypt(property, key); } return _service; }]); angular .module('bit.services') .factory('cryptoService', ["$sessionStorage", "constants", "$q", "$window", function ($sessionStorage, constants, $q, $window) { var _service = {}, _key, _encKey, _legacyEtmKey, _orgKeys, _privateKey, _publicKey, _crypto = typeof $window.crypto != 'undefined' ? $window.crypto : null, _subtle = (!!_crypto && typeof $window.crypto.subtle != 'undefined') ? $window.crypto.subtle : null; _service.setKey = function (key) { _key = key; $sessionStorage.key = _key.keyB64; }; _service.setEncKey = function (encKey, key, alreadyDecrypted) { if (alreadyDecrypted) { _encKey = encKey; $sessionStorage.encKey = _encKey.keyB64; return; } try { var encKeyBytes = _service.decrypt(encKey, key, 'raw'); $sessionStorage.encKey = forge.util.encode64(encKeyBytes); _encKey = new SymmetricCryptoKey(encKeyBytes); } catch (e) { console.log('Cannot set enc key. Decryption failed.'); } }; _service.setPrivateKey = function (privateKeyCt, key) { try { var privateKeyBytes = _service.decrypt(privateKeyCt, key, 'raw'); $sessionStorage.privateKey = forge.util.encode64(privateKeyBytes); _privateKey = forge.pki.privateKeyFromAsn1(forge.asn1.fromDer(privateKeyBytes)); } catch (e) { console.log('Cannot set private key. Decryption failed.'); } }; _service.setOrgKeys = function (orgKeysCt, privateKey) { if (!orgKeysCt || Object.keys(orgKeysCt).length === 0) { return; } _service.clearOrgKeys(); var orgKeysb64 = {}, _orgKeys = {}, setKey = false; for (var orgId in orgKeysCt) { if (orgKeysCt.hasOwnProperty(orgId)) { try { var decBytes = _service.rsaDecrypt(orgKeysCt[orgId].key, privateKey); var decKey = new SymmetricCryptoKey(decBytes); _orgKeys[orgId] = decKey; orgKeysb64[orgId] = decKey.keyB64; setKey = true; } catch (e) { console.log('Cannot set org key for ' + orgId + '. Decryption failed.'); } } } if (setKey) { $sessionStorage.orgKeys = orgKeysb64; } else { _orgKeys = null; } }; _service.addOrgKey = function (orgId, encOrgKey, privateKey) { _orgKeys = _service.getOrgKeys(); if (!_orgKeys) { _orgKeys = {}; } var orgKeysb64 = $sessionStorage.orgKeys; if (!orgKeysb64) { orgKeysb64 = {}; } try { var decBytes = _service.rsaDecrypt(encOrgKey, privateKey); var decKey = new SymmetricCryptoKey(decBytes); _orgKeys[orgId] = decKey; orgKeysb64[orgId] = decKey.keyB64; } catch (e) { _orgKeys = null; console.log('Cannot set org key. Decryption failed.'); } $sessionStorage.orgKeys = orgKeysb64; }; _service.getKey = function () { if (!_key && $sessionStorage.key) { _key = new SymmetricCryptoKey($sessionStorage.key, true); } if (!_key) { throw 'key unavailable'; } return _key; }; _service.getEncKey = function () { if (!_encKey && $sessionStorage.encKey) { _encKey = new SymmetricCryptoKey($sessionStorage.encKey, true); } return _encKey; }; _service.getPrivateKey = function (outputEncoding) { outputEncoding = outputEncoding || 'native'; if (_privateKey) { if (outputEncoding === 'raw') { var privateKeyAsn1 = forge.pki.privateKeyToAsn1(_privateKey); var privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1); return forge.asn1.toDer(privateKeyPkcs8).getBytes(); } return _privateKey; } if ($sessionStorage.privateKey) { var privateKeyBytes = forge.util.decode64($sessionStorage.privateKey); _privateKey = forge.pki.privateKeyFromAsn1(forge.asn1.fromDer(privateKeyBytes)); if (outputEncoding === 'raw') { return privateKeyBytes; } } return _privateKey; }; _service.getPublicKey = function () { if (_publicKey) { return _publicKey; } var privateKey = _service.getPrivateKey(); if (!privateKey) { return null; } _publicKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e); return _publicKey; }; _service.getOrgKeys = function () { if (_orgKeys) { return _orgKeys; } if ($sessionStorage.orgKeys) { var orgKeys = {}, setKey = false; for (var orgId in $sessionStorage.orgKeys) { if ($sessionStorage.orgKeys.hasOwnProperty(orgId)) { orgKeys[orgId] = new SymmetricCryptoKey($sessionStorage.orgKeys[orgId], true); setKey = true; } } if (setKey) { _orgKeys = orgKeys; } } return _orgKeys; }; _service.getOrgKey = function (orgId) { var orgKeys = _service.getOrgKeys(); if (!orgKeys || !(orgId in orgKeys)) { return null; } return orgKeys[orgId]; }; _service.clearKey = function () { _key = null; _legacyEtmKey = null; delete $sessionStorage.key; }; _service.clearEncKey = function () { _encKey = null; delete $sessionStorage.encKey; }; _service.clearKeyPair = function () { _privateKey = null; _publicKey = null; delete $sessionStorage.privateKey; }; _service.clearOrgKeys = function () { _orgKeys = null; delete $sessionStorage.orgKeys; }; _service.clearOrgKey = function (orgId) { if (_orgKeys.hasOwnProperty(orgId)) { delete _orgKeys[orgId]; } if ($sessionStorage.orgKeys.hasOwnProperty(orgId)) { delete $sessionStorage.orgKeys[orgId]; } }; _service.clearKeys = function () { _service.clearKey(); _service.clearEncKey(); _service.clearKeyPair(); _service.clearOrgKeys(); }; _service.makeKey = function (password, salt) { if (_subtle != null && !$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) { return pbkdf2WC(password, salt, 5000, 256).then(function (keyBuf) { return new SymmetricCryptoKey(bufToB64(keyBuf), true); }); } else { var deferred = $q.defer(); var keyBytes = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt), 5000, 256 / 8, 'sha256'); deferred.resolve(new SymmetricCryptoKey(keyBytes)); return deferred.promise; } }; _service.makeEncKey = function (key) { var encKey = forge.random.getBytesSync(512 / 8); var encKeyEnc = _service.encrypt(encKey, key, 'raw'); return { encKey: new SymmetricCryptoKey(encKey), encKeyEnc: encKeyEnc }; }; _service.makeKeyPair = function (key) { var deferred = $q.defer(); forge.pki.rsa.generateKeyPair({ bits: 2048, workers: 2, workerScript: '/lib/forge/prime.worker.min.js' }, function (error, keypair) { if (error) { deferred.reject(error); return; } var privateKeyAsn1 = forge.pki.privateKeyToAsn1(keypair.privateKey); var privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1); var privateKeyBytes = forge.asn1.toDer(privateKeyPkcs8).getBytes(); var privateKeyEncCt = _service.encrypt(privateKeyBytes, key, 'raw'); var publicKeyAsn1 = forge.pki.publicKeyToAsn1(keypair.publicKey); var publicKeyBytes = forge.asn1.toDer(publicKeyAsn1).getBytes(); deferred.resolve({ publicKey: forge.util.encode64(publicKeyBytes), privateKeyEnc: privateKeyEncCt }); }); return deferred.promise; }; _service.makeShareKey = function () { var key = forge.random.getBytesSync(512 / 8); return { key: new SymmetricCryptoKey(key), ct: _service.rsaEncryptMe(key) }; }; _service.hashPassword = function (password, key) { if (!key) { key = _service.getKey(); } if (!password || !key) { throw 'Invalid parameters.'; } if (_subtle != null && !$window.cryptoShimmed && $window.navigator.userAgent.indexOf('Edge') === -1) { var keyBuf = key.getBuffers(); return pbkdf2WC(new Uint8Array(keyBuf.key), password, 1, 256).then(function (hashBuf) { return bufToB64(hashBuf); }); } else { var deferred = $q.defer(); var hashBits = forge.pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256'); deferred.resolve(forge.util.encode64(hashBits)); return deferred.promise; } }; function pbkdf2WC(password, salt, iterations, size) { password = typeof (password) === 'string' ? utf8ToArray(password) : password; salt = typeof (salt) === 'string' ? utf8ToArray(salt) : salt; return _subtle.importKey('raw', password.buffer, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits']) .then(function (importedKey) { return _subtle.deriveKey( { name: 'PBKDF2', salt: salt.buffer, iterations: iterations, hash: { name: 'SHA-256' } }, importedKey, { name: 'AES-CBC', length: size }, true, ['encrypt', 'decrypt']); }).then(function (derivedKey) { return _subtle.exportKey('raw', derivedKey); }); } _service.makeKeyAndHash = function (email, password) { email = email.toLowerCase(); var key; return _service.makeKey(password, email).then(function (theKey) { key = theKey; return _service.hashPassword(password, theKey); }).then(function (theHash) { return { key: key, hash: theHash }; }); }; _service.encrypt = function (plainValue, key, plainValueEncoding) { var encValue = aesEncrypt(plainValue, key, plainValueEncoding); var iv = forge.util.encode64(encValue.iv); var ct = forge.util.encode64(encValue.ct); var cipherString = iv + '|' + ct; if (encValue.mac) { var mac = forge.util.encode64(encValue.mac); cipherString = cipherString + '|' + mac; } return encValue.key.encType + '.' + cipherString; }; _service.encryptToBytes = function (plainValue, key) { return aesEncryptWC(plainValue, key).then(function (encValue) { var macLen = 0; if (encValue.mac) { macLen = encValue.mac.length; } var encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length); encBytes.set([encValue.key.encType]); encBytes.set(encValue.iv, 1); if (encValue.mac) { encBytes.set(encValue.mac, 1 + encValue.iv.length); } encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen); return encBytes.buffer; }); }; function aesEncrypt(plainValue, key, plainValueEncoding) { key = key || _service.getEncKey() || _service.getKey(); if (!key) { throw 'Encryption key unavailable.'; } plainValueEncoding = plainValueEncoding || 'utf8'; var buffer = forge.util.createBuffer(plainValue, plainValueEncoding); var ivBytes = forge.random.getBytesSync(16); var cipher = forge.cipher.createCipher('AES-CBC', key.encKey); cipher.start({ iv: ivBytes }); cipher.update(buffer); cipher.finish(); var ctBytes = cipher.output.getBytes(); var macBytes = null; if (key.macKey) { macBytes = computeMac(ivBytes + ctBytes, key.macKey, false); } return { iv: ivBytes, ct: ctBytes, mac: macBytes, key: key, plainValueEncoding: plainValueEncoding }; } function aesEncryptWC(plainValue, key) { key = key || _service.getEncKey() || _service.getKey(); if (!key) { throw 'Encryption key unavailable.'; } var obj = { iv: new Uint8Array(16), ct: null, mac: null, key: key }; var keyBuf = key.getBuffers(); _crypto.getRandomValues(obj.iv); return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['encrypt']) .then(function (encKey) { return _subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue); }).then(function (encValue) { obj.ct = new Uint8Array(encValue); if (!keyBuf.macKey) { return null; } var data = new Uint8Array(obj.iv.length + obj.ct.length); data.set(obj.iv, 0); data.set(obj.ct, obj.iv.length); return computeMacWC(data.buffer, keyBuf.macKey); }).then(function (mac) { if (mac) { obj.mac = new Uint8Array(mac); } return obj; }); } _service.rsaEncrypt = function (plainValue, publicKey, key) { publicKey = publicKey || _service.getPublicKey(); if (!publicKey) { throw 'Public key unavailable.'; } if (typeof publicKey === 'string') { var publicKeyBytes = forge.util.decode64(publicKey); publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(publicKeyBytes)); } var encryptedBytes = publicKey.encrypt(plainValue, 'RSA-OAEP', { md: forge.md.sha1.create() }); var cipherString = forge.util.encode64(encryptedBytes); if (key && key.macKey) { var mac = computeMac(encryptedBytes, key.macKey, true); return constants.encType.Rsa2048_OaepSha1_HmacSha256_B64 + '.' + cipherString + '|' + mac; } else { return constants.encType.Rsa2048_OaepSha1_B64 + '.' + cipherString; } }; _service.rsaEncryptMe = function (plainValue) { return _service.rsaEncrypt(plainValue, _service.getPublicKey(), _service.getEncKey()); }; _service.decrypt = function (encValue, key, outputEncoding) { try { key = key || _service.getEncKey() || _service.getKey(); var headerPieces = encValue.split('.'), encType, encPieces; if (headerPieces.length === 2) { try { encType = parseInt(headerPieces[0]); encPieces = headerPieces[1].split('|'); } catch (e) { console.error('Cannot parse headerPieces.'); return null; } } else { encPieces = encValue.split('|'); encType = encPieces.length === 3 ? constants.encType.AesCbc128_HmacSha256_B64 : constants.encType.AesCbc256_B64; } if (encType === constants.encType.AesCbc128_HmacSha256_B64 && key.encType === constants.encType.AesCbc256_B64) { // Old encrypt-then-mac scheme, swap out the key _legacyEtmKey = _legacyEtmKey || new SymmetricCryptoKey(key.key, false, constants.encType.AesCbc128_HmacSha256_B64); key = _legacyEtmKey; } if (encType !== key.encType) { throw 'encType unavailable.'; } switch (encType) { case constants.encType.AesCbc128_HmacSha256_B64: case constants.encType.AesCbc256_HmacSha256_B64: if (encPieces.length !== 3) { console.error('Enc type (' + encType + ') not valid.'); return null; } break; case constants.encType.AesCbc256_B64: if (encPieces.length !== 2) { console.error('Enc type (' + encType + ') not valid.'); return null; } break; default: console.error('Enc type (' + encType + ') not supported.'); return null; } var ivBytes = forge.util.decode64(encPieces[0]); var ctBytes = forge.util.decode64(encPieces[1]); if (key.macKey && encPieces.length > 2) { var macBytes = forge.util.decode64(encPieces[2]); var computedMacBytes = computeMac(ivBytes + ctBytes, key.macKey, false); if (!macsEqual(macBytes, computedMacBytes)) { console.error('MAC failed.'); return null; } } var ctBuffer = forge.util.createBuffer(ctBytes); var decipher = forge.cipher.createDecipher('AES-CBC', key.encKey); decipher.start({ iv: ivBytes }); decipher.update(ctBuffer); decipher.finish(); outputEncoding = outputEncoding || 'utf8'; if (outputEncoding === 'utf8') { return decipher.output.toString('utf8'); } else { return decipher.output.getBytes(); } } catch (e) { console.error('Caught unhandled error in decrypt: ' + e); throw e; } }; _service.decryptFromBytes = function (encBuf, key) { try { if (!encBuf) { throw 'no encBuf.'; } var encBytes = new Uint8Array(encBuf), encType = encBytes[0], ctBytes = null, ivBytes = null, macBytes = null; switch (encType) { case constants.encType.AesCbc128_HmacSha256_B64: case constants.encType.AesCbc256_HmacSha256_B64: if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength console.error('Enc type (' + encType + ') not valid.'); return null; } ivBytes = slice(encBytes, 1, 17); macBytes = slice(encBytes, 17, 49); ctBytes = slice(encBytes, 49); break; case constants.encType.AesCbc256_B64: if (encBytes.length <= 17) { // 1 + 16 + ctLength console.error('Enc type (' + encType + ') not valid.'); return null; } ivBytes = slice(encBytes, 1, 17); ctBytes = slice(encBytes, 17); break; default: console.error('Enc type (' + encType + ') not supported.'); return null; } return aesDecryptWC( encType, ctBytes.buffer, ivBytes.buffer, macBytes ? macBytes.buffer : null, key); } catch (e) { console.error('Caught unhandled error in decryptFromBytes: ' + e); throw e; } }; function aesDecryptWC(encType, ctBuf, ivBuf, macBuf, key) { key = key || _service.getEncKey() || _service.getKey(); if (!key) { throw 'Encryption key unavailable.'; } if (key.macKey && !macBuf) { throw 'macBuf required for this type of key.'; } if (encType !== key.encType) { throw 'encType unavailable.'; } var keyBuf = key.getBuffers(), encKey = null; return _subtle.importKey('raw', keyBuf.encKey, { name: 'AES-CBC' }, false, ['decrypt']) .then(function (theEncKey) { encKey = theEncKey; if (!key.macKey || !macBuf) { return null; } var data = new Uint8Array(ivBuf.byteLength + ctBuf.byteLength); data.set(new Uint8Array(ivBuf), 0); data.set(new Uint8Array(ctBuf), ivBuf.byteLength); return computeMacWC(data.buffer, keyBuf.macKey); }).then(function (computedMacBuf) { if (computedMacBuf === null) { return null; } return macsEqualWC(macBuf, computedMacBuf); }).then(function (macsMatch) { if (macsMatch === false) { console.error('MAC failed.'); return null; } return _subtle.decrypt({ name: 'AES-CBC', iv: ivBuf }, encKey, ctBuf); }); } _service.rsaDecrypt = function (encValue, privateKey, key) { privateKey = privateKey || _service.getPrivateKey(); key = key || _service.getEncKey(); if (!privateKey) { throw 'Private key unavailable.'; } var headerPieces = encValue.split('.'), encType, encPieces; if (headerPieces.length === 1) { encType = constants.encType.Rsa2048_OaepSha256_B64; encPieces = [headerPieces[0]]; } else if (headerPieces.length === 2) { try { encType = parseInt(headerPieces[0]); encPieces = headerPieces[1].split('|'); } catch (e) { return null; } } switch (encType) { case constants.encType.Rsa2048_OaepSha256_B64: case constants.encType.Rsa2048_OaepSha1_B64: if (encPieces.length !== 1) { return null; } break; case constants.encType.Rsa2048_OaepSha256_HmacSha256_B64: case constants.encType.Rsa2048_OaepSha1_HmacSha256_B64: if (encPieces.length !== 2) { return null; } break; default: return null; } var ctBytes = forge.util.decode64(encPieces[0]); if (key && key.macKey && encPieces.length > 1) { var macBytes = forge.util.decode64(encPieces[1]); var computedMacBytes = computeMac(ctBytes, key.macKey, false); if (!macsEqual(macBytes, computedMacBytes)) { console.error('MAC failed.'); return null; } } var md; if (encType === constants.encType.Rsa2048_OaepSha256_B64 || encType === constants.encType.Rsa2048_OaepSha256_HmacSha256_B64) { md = forge.md.sha256.create(); } else if (encType === constants.encType.Rsa2048_OaepSha1_B64 || encType === constants.encType.Rsa2048_OaepSha1_HmacSha256_B64) { md = forge.md.sha1.create(); } else { throw 'encType unavailable.'; } var decBytes = privateKey.decrypt(ctBytes, 'RSA-OAEP', { md: md }); return decBytes; }; function computeMac(dataBytes, macKey, b64Output) { var hmac = forge.hmac.create(); hmac.start('sha256', macKey); hmac.update(dataBytes); var mac = hmac.digest(); return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes(); } function computeMacWC(dataBuf, macKeyBuf) { return _subtle.importKey('raw', macKeyBuf, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']) .then(function (key) { return _subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, key, dataBuf); }); } // Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification). // ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy function macsEqual(mac1, mac2) { var hmac = forge.hmac.create(); hmac.start('sha256', getRandomBytes(32)); hmac.update(mac1); mac1 = hmac.digest().getBytes(); hmac.start(null, null); hmac.update(mac2); mac2 = hmac.digest().getBytes(); return mac1 === mac2; } function macsEqualWC(mac1Buf, mac2Buf) { var mac1, macKey; var compareKey = new Uint8Array(32); _crypto.getRandomValues(compareKey); return window.crypto.subtle.importKey('raw', compareKey.buffer, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']) .then(function (key) { macKey = key; return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac1Buf); }).then(function (mac) { mac1 = mac; return window.crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, macKey, mac2Buf); }).then(function (mac2) { if (mac1.byteLength !== mac2.byteLength) { return false; } var arr1 = new Uint8Array(mac1); var arr2 = new Uint8Array(mac2); for (var i = 0; i < arr2.length; i++) { if (arr1[i] !== arr2[i]) { return false; } } return true; }); } function SymmetricCryptoKey(keyBytes, b64KeyBytes, encType) { if (b64KeyBytes) { keyBytes = forge.util.decode64(keyBytes); } if (!keyBytes) { throw 'Must provide keyBytes'; } var buffer = forge.util.createBuffer(keyBytes); if (!buffer || buffer.length() === 0) { throw 'Couldn\'t make buffer'; } var bufferLength = buffer.length(); if (encType === null || encType === undefined) { if (bufferLength === 32) { encType = constants.encType.AesCbc256_B64; } else if (bufferLength === 64) { encType = constants.encType.AesCbc256_HmacSha256_B64; } else { throw 'Unable to determine encType.'; } } this.key = keyBytes; this.keyB64 = forge.util.encode64(keyBytes); this.encType = encType; if (encType === constants.encType.AesCbc256_B64 && bufferLength === 32) { this.encKey = keyBytes; this.macKey = null; } else if (encType === constants.encType.AesCbc128_HmacSha256_B64 && bufferLength === 32) { this.encKey = buffer.getBytes(16); // first half this.macKey = buffer.getBytes(16); // second half } else if (encType === constants.encType.AesCbc256_HmacSha256_B64 && bufferLength === 64) { this.encKey = buffer.getBytes(32); // first half this.macKey = buffer.getBytes(32); // second half } else { throw 'Unsupported encType/key length.'; } } SymmetricCryptoKey.prototype.getBuffers = function () { if (this.keyBuf) { return this.keyBuf; } var key = b64ToArray(this.keyB64); var keys = { key: key.buffer }; if (this.macKey) { keys.encKey = slice(key, 0, key.length / 2).buffer; keys.macKey = slice(key, key.length / 2).buffer; } else { keys.encKey = key.buffer; keys.macKey = null; } this.keyBuf = keys; return this.keyBuf; }; function b64ToArray(b64Str) { var binaryString = $window.atob(b64Str); var arr = new Uint8Array(binaryString.length); for (var i = 0; i < binaryString.length; i++) { arr[i] = binaryString.charCodeAt(i); } return arr; } function bufToB64(buf) { var binary = ''; var bytes = new Uint8Array(buf); for (var i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return $window.btoa(binary); } function utf8ToArray(str) { var utf8Str = unescape(encodeURIComponent(str)); var arr = new Uint8Array(utf8Str.length); for (var i = 0; i < utf8Str.length; i++) { arr[i] = utf8Str.charCodeAt(i); } return arr; } function slice(arr, begin, end) { if (arr.slice) { return arr.slice(begin, end); } // shim for IE // ref: https://stackoverflow.com/a/21440217 arr = arr.buffer; if (begin === void 0) { begin = 0; } if (end === void 0) { end = arr.byteLength; } begin = Math.floor(begin); end = Math.floor(end); if (begin < 0) { begin += arr.byteLength; } if (end < 0) { end += arr.byteLength; } begin = Math.min(Math.max(0, begin), arr.byteLength); end = Math.min(Math.max(0, end), arr.byteLength); if (end - begin <= 0) { return new ArrayBuffer(0); } var result = new ArrayBuffer(end - begin); var resultBytes = new Uint8Array(result); var sourceBytes = new Uint8Array(arr, begin, end - begin); resultBytes.set(sourceBytes); return new Uint8Array(result); } function getRandomBytes(byteLength) { var bytes = new Uint32Array(byteLength / 4); _crypto.getRandomValues(bytes); var buffer = forge.util.createBuffer(); for (var i = 0; i < bytes.length; i++) { buffer.putInt32(bytes[i]); } return buffer.getBytes(); } return _service; }]); angular .module('bit.services') .factory('eventService', ["constants", "$filter", function (constants, $filter) { var _service = {}; _service.getDefaultDateFilters = function () { var d = new Date(); var filterEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59); d.setDate(d.getDate() - 30); var filterStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0); return { start: filterStart, end: filterEnd }; }; _service.formatDateFilters = function (filterStart, filterEnd) { var result = { start: null, end: null, error: null }; try { var format = 'yyyy-MM-ddTHH:mm'; result.start = $filter('date')(filterStart, format + 'Z', 'UTC'); result.end = $filter('date')(filterEnd, format + ':59.999Z', 'UTC'); } catch (e) { } if (!result.start || !result.end || result.end < result.start) { result.error = 'Invalid date range.'; } return result; }; _service.getEventInfo = function (ev, options) { options = options || { cipherInfo: true }; var appInfo = getAppInfo(ev); return { message: getEventMessage(ev, options), appIcon: appInfo.icon, appName: appInfo.name }; }; function getEventMessage(ev, options) { var msg = ''; switch (ev.Type) { // User case constants.eventType.User_LoggedIn: msg = 'Logged in.'; break; case constants.eventType.User_ChangedPassword: msg = 'Changed account password.'; break; case constants.eventType.User_Enabled2fa: msg = 'Enabled two-step login.'; break; case constants.eventType.User_Disabled2fa: msg = 'Disabled two-step login.'; break; case constants.eventType.User_Recovered2fa: msg = 'Recovered account from two-step login.'; break; case constants.eventType.User_FailedLogIn: msg = 'Login attempt failed with incorrect password.'; break; case constants.eventType.User_FailedLogIn2fa: msg = 'Login attempt failed with incorrect two-step login.'; break; // Cipher case constants.eventType.Cipher_Created: msg = options.cipherInfo ? 'Created item ' + formatCipherId(ev) + '.' : 'Created.'; break; case constants.eventType.Cipher_Updated: msg = options.cipherInfo ? 'Edited item ' + formatCipherId(ev) + '.' : 'Edited.'; break; case constants.eventType.Cipher_Deleted: msg = options.cipherInfo ? 'Deleted item ' + formatCipherId(ev) + '.' : 'Deleted'; break; case constants.eventType.Cipher_AttachmentCreated: msg = options.cipherInfo ? 'Created attachment for item ' + formatCipherId(ev) + '.' : 'Created attachment.'; break; case constants.eventType.Cipher_AttachmentDeleted: msg = options.cipherInfo ? 'Deleted attachment for item ' + formatCipherId(ev) + '.' : 'Deleted attachment.'; break; case constants.eventType.Cipher_Shared: msg = options.cipherInfo ? 'Shared item ' + formatCipherId(ev) + '.' : 'Shared.'; break; case constants.eventType.Cipher_UpdatedCollections: msg = options.cipherInfo ? 'Update collections for item ' + formatCipherId(ev) + '.' : 'Updated collections.'; break; // Collection case constants.eventType.Collection_Created: msg = 'Created collection ' + formatCollectionId(ev) + '.'; break; case constants.eventType.Collection_Updated: msg = 'Edited collection ' + formatCollectionId(ev) + '.'; break; case constants.eventType.Collection_Deleted: msg = 'Deleted collection ' + formatCollectionId(ev) + '.'; break; // Group case constants.eventType.Group_Created: msg = 'Created group ' + formatGroupId(ev) + '.'; break; case constants.eventType.Group_Updated: msg = 'Edited group ' + formatGroupId(ev) + '.'; break; case constants.eventType.Group_Deleted: msg = 'Deleted group ' + formatGroupId(ev) + '.'; break; // Org user case constants.eventType.OrganizationUser_Invited: msg = 'Invited user ' + formatOrgUserId(ev) + '.'; break; case constants.eventType.OrganizationUser_Confirmed: msg = 'Confirmed user ' + formatOrgUserId(ev) + '.'; break; case constants.eventType.OrganizationUser_Updated: msg = 'Edited user ' + formatOrgUserId(ev) + '.'; break; case constants.eventType.OrganizationUser_Removed: msg = 'Removed user ' + formatOrgUserId(ev) + '.'; break; case constants.eventType.OrganizationUser_UpdatedGroups: msg = 'Edited groups for user ' + formatOrgUserId(ev) + '.'; break; // Org case constants.eventType.Organization_Updated: msg = 'Edited organization settings.'; break; default: break; } return msg === '' ? null : msg; } function getAppInfo(ev) { var appInfo = { icon: 'fa-globe', name: 'Unknown' }; switch (ev.DeviceType) { case constants.deviceType.android: appInfo.icon = 'fa-android'; appInfo.name = 'Mobile App - Android'; break; case constants.deviceType.ios: appInfo.icon = 'fa-apple'; appInfo.name = 'Mobile App - iOS'; break; case constants.deviceType.uwp: appInfo.icon = 'fa-windows'; appInfo.name = 'Mobile App - Windows'; break; case constants.deviceType.chromeExt: appInfo.icon = 'fa-chrome'; appInfo.name = 'Extension - Chrome'; break; case constants.deviceType.firefoxExt: appInfo.icon = 'fa-firefox'; appInfo.name = 'Extension - Firefox'; break; case constants.deviceType.operaExt: appInfo.icon = 'fa-opera'; appInfo.name = 'Extension - Opera'; break; case constants.deviceType.edgeExt: appInfo.icon = 'fa-edge'; appInfo.name = 'Extension - Edge'; break; case constants.deviceType.vivaldiExt: appInfo.icon = 'fa-puzzle-piece'; appInfo.name = 'Extension - Vivaldi'; break; case constants.deviceType.windowsDesktop: appInfo.icon = 'fa-windows'; appInfo.name = 'Desktop - Windows'; break; case constants.deviceType.macOsDesktop: appInfo.icon = 'fa-apple'; appInfo.name = 'Desktop - macOS'; break; case constants.deviceType.linuxDesktop: appInfo.icon = 'fa-linux'; appInfo.name = 'Desktop - Linux'; break; case constants.deviceType.chrome: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Chrome'; break; case constants.deviceType.firefox: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Firefox'; break; case constants.deviceType.opera: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Opera'; break; case constants.deviceType.safari: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Safari'; break; case constants.deviceType.vivaldi: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Vivaldi'; break; case constants.deviceType.edge: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Edge'; break; case constants.deviceType.ie: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - IE'; break; case constants.deviceType.unknown: appInfo.icon = 'fa-globe'; appInfo.name = 'Web Vault - Unknown'; break; default: break; } return appInfo; } function formatCipherId(ev) { var shortId = ev.CipherId.substring(0, 8); if (!ev.OrganizationId) { return '' + shortId + ''; } return '' + '' + shortId + ''; } function formatGroupId(ev) { var shortId = ev.GroupId.substring(0, 8); return '' + '' + shortId + ''; } function formatCollectionId(ev) { var shortId = ev.CollectionId.substring(0, 8); return '' + '' + shortId + ''; } function formatOrgUserId(ev) { var shortId = ev.OrganizationUserId.substring(0, 8); return '' + '' + shortId + ''; } return _service; }]); angular .module('bit.services') .factory('importService', ["constants", function (constants) { var _service = {}; _service.import = function (source, file, success, error) { if (!file) { error(); return; } switch (source) { case 'bitwardencsv': importBitwardenCsv(file, success, error); break; case 'lastpass': importLastPass(file, success, error, false); break; case 'safeincloudxml': importSafeInCloudXml(file, success, error); break; case 'keepass2xml': importKeePass2Xml(file, success, error); break; case 'keepassxcsv': importKeePassXCsv(file, success, error); break; case 'padlockcsv': importPadlockCsv(file, success, error); break; case '1password1pif': import1Password1Pif(file, success, error); break; case '1password6wincsv': import1Password6WinCsv(file, success, error); break; case 'chromecsv': case 'vivaldicsv': case 'operacsv': importChromeCsv(file, success, error); break; case 'firefoxpasswordexportercsv': importFirefoxPasswordExporterCsv(file, success, error); break; case 'upmcsv': importUpmCsv(file, success, error); break; case 'keepercsv': importKeeperCsv(file, success, error); break; case 'passworddragonxml': importPasswordDragonXml(file, success, error); break; case 'enpasscsv': importEnpassCsv(file, success, error); break; case 'pwsafexml': importPasswordSafeXml(file, success, error); break; case 'dashlanecsv': importDashlaneCsv(file, success, error); break; case 'stickypasswordxml': importStickyPasswordXml(file, success, error); break; case 'msecurecsv': importmSecureCsv(file, success, error); break; case 'truekeycsv': importTrueKeyCsv(file, success, error); break; case 'clipperzhtml': importClipperzHtml(file, success, error); break; case 'avirajson': importAviraJson(file, success, error); break; case 'roboformhtml': importRoboFormHtml(file, success, error); break; case 'saferpasscsv': importSaferPassCsv(file, success, error); break; case 'ascendocsv': importAscendoCsv(file, success, error); break; case 'passwordbossjson': importPasswordBossJson(file, success, error); break; case 'zohovaultcsv': importZohoVaultCsv(file, success, error); break; case 'splashidcsv': importSplashIdCsv(file, success, error); break; case 'meldiumcsv': importMeldiumCsv(file, success, error); break; case 'passkeepcsv': importPassKeepCsv(file, success, error); break; case 'gnomejson': importGnomeJson(file, success, error); break; default: error(); break; } }; _service.importOrg = function (source, file, success, error) { if (!file) { error(); return; } switch (source) { case 'bitwardencsv': importBitwardenOrgCsv(file, success, error); break; case 'lastpass': importLastPass(file, success, error, true); break; default: error(); break; } }; // helpers var _passwordFieldNames = [ 'password', 'pass word', 'passphrase', 'pass phrase', 'pass', 'code', 'code word', 'codeword', 'secret', 'secret word', 'personpwd', 'key', 'keyword', 'key word', 'keyphrase', 'key phrase', 'form_pw', 'wppassword', 'pin', 'pwd', 'pw', 'pword', 'passwd', 'p', 'serial', 'serial#', 'license key', 'reg #', // Non-English names 'passwort' ]; var _usernameFieldNames = [ 'user', 'name', 'user name', 'username', 'login name', 'email', 'e-mail', 'id', 'userid', 'user id', 'login', 'form_loginname', 'wpname', 'mail', 'loginid', 'login id', 'log', 'personlogin', 'first name', 'last name', 'card#', 'account #', 'member', 'member #', // Non-English names 'nom', 'benutzername' ]; var _notesFieldNames = [ "note", "notes", "comment", "comments", "memo", "description", "free form", "freeform", "free text", "freetext", "free", // Non-English names "kommentar" ]; var _uriFieldNames = [ 'url', 'hyper link', 'hyperlink', 'link', 'host', 'hostname', 'host name', 'server', 'address', 'hyper ref', 'href', 'web', 'website', 'web site', 'site', 'web-site', 'uri', // Non-English names 'ort', 'adresse' ]; function loginNameFromUrl(url) { var a = document.createElement('a'); a.href = url; return a.hostname.startsWith('www.') ? a.hostname.replace('www.', '') : a.hostname; } function isField(fieldText, refFieldValues) { if (!fieldText || fieldText === '') { return false; } fieldText = fieldText.trim().toLowerCase(); for (var i = 0; i < refFieldValues.length; i++) { if (fieldText === refFieldValues[i]) { return true; } } return false; } function fixUri(uri) { uri = uri.toLowerCase().trim(); if (uri.indexOf('://') === -1 && uri.indexOf('.') >= 0) { uri = 'http://' + uri; } if (uri.length > 1000) { return uri.substring(0, 1000); } return uri; } function makeUriArray(uri) { if (!uri) { return null; } if (typeof uri === 'string') { return [{ uri: fixUri(uri), match: null }]; } if (uri.length) { var returnArr = []; for (var i = 0; i < uri.length; i++) { returnArr.push({ uri: fixUri(uri[i]), match: null }); } return returnArr; } return null; } function parseSingleRowCsv(rowData) { if (!rowData || rowData === '') { return null; } var parsedRow = Papa.parse(rowData); if (parsedRow && parsedRow.data && parsedRow.data.length && parsedRow.data[0].length) { return parsedRow.data[0]; } return null; } function parseCsvErrors(results) { if (results.errors && results.errors.length) { for (var i = 0; i < results.errors.length; i++) { console.warn('Error parsing row ' + results.errors[i].row + ': ' + results.errors[i].message); } } } function getFileContents(file, contentsCallback, errorCallback) { if (typeof file === 'string') { contentsCallback(file); } else { var reader = new FileReader(); reader.readAsText(file, 'utf-8'); reader.onload = function (evt) { contentsCallback(evt.target.result); }; reader.onerror = function (evt) { errorCallback(); }; } } function getXmlFileContents(file, xmlCallback, errorCallback) { getFileContents(file, function (fileContents) { xmlCallback($.parseXML(fileContents)); }, errorCallback); } // ref https://stackoverflow.com/a/5911300 function getCardType(number) { if (!number) { return null; } // Visa var re = new RegExp('^4'); if (number.match(re) != null) { return 'Visa'; } // Mastercard // Updated for Mastercard 2017 BINs expansion if (/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(number)) { return 'Mastercard'; } // AMEX re = new RegExp('^3[47]'); if (number.match(re) != null) { return 'Amex'; } // Discover re = new RegExp('^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)'); if (number.match(re) != null) { return 'Discover'; } // Diners re = new RegExp('^36'); if (number.match(re) != null) { return 'Diners Club'; } // Diners - Carte Blanche re = new RegExp('^30[0-5]'); if (number.match(re) != null) { return 'Diners Club'; } // JCB re = new RegExp('^35(2[89]|[3-8][0-9])'); if (number.match(re) != null) { return 'JCB'; } // Visa Electron re = new RegExp('^(4026|417500|4508|4844|491(3|7))'); if (number.match(re) != null) { return 'Visa'; } return null; } // importers function importBitwardenCsv(file, success, error) { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = [], i = 0; angular.forEach(results.data, function (value, key) { var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = value.folder && value.folder !== '', addFolder = hasFolder; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === value.folder) { addFolder = false; folderIndex = i; break; } } } var cipher = { favorite: value.favorite && value.favorite !== '' && value.favorite !== '0' ? true : false, notes: value.notes && value.notes !== '' ? value.notes : null, name: value.name && value.name !== '' ? value.name : '--', type: constants.cipherType.login }; if (value.fields && value.fields !== '') { var fields = value.fields.split(/(?:\r\n|\r|\n)/); for (i = 0; i < fields.length; i++) { if (!fields[i] || fields[i] === '') { continue; } var delimPosition = fields[i].lastIndexOf(': '); if (delimPosition === -1) { continue; } if (!cipher.fields) { cipher.fields = []; } var field = { name: fields[i].substr(0, delimPosition), value: null, type: constants.fieldType.text }; if (fields[i].length > (delimPosition + 2)) { field.value = fields[i].substr(delimPosition + 2); } cipher.fields.push(field); } } var valueType = value.type ? value.type.toLowerCase() : null; switch (valueType) { case 'login': case null: case undefined: cipher.type = constants.cipherType.login; var totp = value.login_totp || value.totp; var uris = parseSingleRowCsv(value.login_uri || value.uri); var username = value.login_username || value.username; var password = value.login_password || value.password; cipher.login = { totp: totp && totp !== '' ? totp : null, uris: makeUriArray(uris), username: username && username !== '' ? username : null, password: password && password !== '' ? password : null }; break; case 'note': cipher.type = constants.cipherType.secureNote; cipher.secureNote = { type: 0 // generic note }; break; default: break; } ciphers.push(cipher); if (addFolder) { folders.push({ name: value.folder }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } }); success(folders, ciphers, folderRelationships); } }); } function importBitwardenOrgCsv(file, success, error) { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var collections = [], ciphers = [], collectionRelationships = [], i; angular.forEach(results.data, function (value, key) { var cipherIndex = ciphers.length; if (value.collections && value.collections !== '') { var cipherCollections = value.collections.split(','); for (i = 0; i < cipherCollections.length; i++) { var addCollection = true; var collectionIndex = collections.length; for (var j = 0; j < collections.length; j++) { if (collections[j].name === cipherCollections[i]) { addCollection = false; collectionIndex = j; break; } } if (addCollection) { collections.push({ name: cipherCollections[i] }); } collectionRelationships.push({ key: cipherIndex, value: collectionIndex }); } } var cipher = { favorite: false, notes: value.notes && value.notes !== '' ? value.notes : null, name: value.name && value.name !== '' ? value.name : '--', type: constants.cipherType.login }; if (value.fields && value.fields !== '') { var fields = value.fields.split(/(?:\r\n|\r|\n)/); for (i = 0; i < fields.length; i++) { if (!fields[i] || fields[i] === '') { continue; } var delimPosition = fields[i].lastIndexOf(': '); if (delimPosition === -1) { continue; } if (!cipher.fields) { cipher.fields = []; } var field = { name: fields[i].substr(0, delimPosition), value: null, type: constants.fieldType.text }; if (fields[i].length > (delimPosition + 2)) { field.value = fields[i].substr(delimPosition + 2); } cipher.fields.push(field); } } var valueType = value.type ? value.type.toLowerCase() : null; switch (valueType) { case 'login': case null: case undefined: cipher.type = constants.cipherType.login; var totp = value.login_totp || value.totp; var uris = parseSingleRowCsv(value.login_uri || value.uri); var username = value.login_username || value.username; var password = value.login_password || value.password; cipher.login = { totp: totp && totp !== '' ? totp : null, uris: makeUriArray(uris), username: username && username !== '' ? username : null, password: password && password !== '' ? password : null }; break; case 'note': cipher.type = constants.cipherType.secureNote; cipher.secureNote = { type: 0 // generic note }; break; default: break; } ciphers.push(cipher); }); success(collections, ciphers, collectionRelationships); } }); } function importLastPass(file, success, error, org) { if (typeof file !== 'string' && file.type && file.type === 'text/html') { var reader = new FileReader(); reader.readAsText(file, 'utf-8'); reader.onload = function (evt) { var doc = $(evt.target.result); var pre = doc.find('pre'); var csv, results; if (pre.length === 1) { csv = pre.text().trim(); results = Papa.parse(csv, { header: true, encoding: 'UTF-8' }); parseData(results.data); } else { var foundPre = false; for (var i = 0; i < doc.length; i++) { if (doc[i].tagName.toLowerCase() === 'pre') { foundPre = true; csv = doc[i].outerText.trim(); results = Papa.parse(csv, { header: true, encoding: 'UTF-8' }); parseData(results.data); break; } } if (!foundPre) { error(); } } }; reader.onerror = function (evt) { error(); }; } else { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); parseData(results.data); }, beforeFirstChunk: function (chunk) { return chunk.replace(/^\s+/, ''); } }); } function parseSecureNoteMapping(extraParts, map, skip) { var obj = { dataObj: {}, notes: null }; for (var i = 0; i < extraParts.length; i++) { var fieldParts = extraParts[i].split(':'); if (fieldParts.length < 1 || fieldParts[0] === 'NoteType' || skip.indexOf(fieldParts[0]) > -1 || !fieldParts[1] || fieldParts[1] === '') { continue; } if (fieldParts[0] === 'Notes') { if (obj.notes) { obj.notes += ('\n' + fieldParts[1]); } else { obj.notes = fieldParts[1]; } } else if (map.hasOwnProperty(fieldParts[0])) { obj.dataObj[map[fieldParts[0]]] = fieldParts[1]; } else { if (obj.notes) { obj.notes += '\n'; } else { obj.notes = ''; } obj.notes += (fieldParts[0] + ': ' + fieldParts[1]); } } return obj; } function parseCard(value) { var cardData = { cardholderName: value.ccname && value.ccname !== '' ? value.ccname : null, number: value.ccnum && value.ccnum !== '' ? value.ccnum : null, brand: value.ccnum && value.ccnum !== '' ? getCardType(value.ccnum) : null, code: value.cccsc && value.cccsc !== '' ? value.cccsc : null }; if (value.ccexp && value.ccexp !== '' && value.ccexp.indexOf('-') > -1) { var ccexpParts = value.ccexp.split('-'); if (ccexpParts.length > 1) { cardData.expYear = ccexpParts[0]; cardData.expMonth = ccexpParts[1]; if (cardData.expMonth.length === 2 && cardData.expMonth[0] === '0') { cardData.expMonth = cardData.expMonth[1]; } } } return cardData; } function parseData(data) { var folders = [], ciphers = [], cipherRelationships = [], i = 0; angular.forEach(data, function (value, key) { var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = value.grouping && value.grouping !== '' && value.grouping !== '(none)', addFolder = hasFolder; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === value.grouping) { addFolder = false; folderIndex = i; break; } } } var cipher; if (value.hasOwnProperty('profilename') && value.hasOwnProperty('profilelanguage')) { // form fill cipher = { favorite: false, name: value.profilename && value.profilename !== '' ? value.profilename : '--', type: constants.cipherType.card }; if (value.title !== '' || value.firstname !== '' || value.lastname !== '' || value.address1 !== '' || value.phone !== '' || value.username !== '' || value.email !== '') { cipher.type = constants.cipherType.identity; } } else { // site or secure note cipher = { favorite: org ? false : value.fav === '1', name: value.name && value.name !== '' ? value.name : '--', type: value.url === 'http://sn' ? constants.cipherType.secureNote : constants.cipherType.login }; } if (cipher.type === constants.cipherType.login) { cipher.login = { uris: makeUriArray(value.url), username: value.username && value.username !== '' ? value.username : null, password: value.password && value.password !== '' ? value.password : null }; cipher.notes = value.extra && value.extra !== '' ? value.extra : null; } else if (cipher.type === constants.cipherType.secureNote) { var extraParts = value.extra.split(/(?:\r\n|\r|\n)/), processedNote = false; if (extraParts.length) { var typeParts = extraParts[0].split(':'); if (typeParts.length > 1 && typeParts[0] === 'NoteType' && (typeParts[1] === 'Credit Card' || typeParts[1] === 'Address')) { var mappedData = null; if (typeParts[1] === 'Credit Card') { mappedData = parseSecureNoteMapping(extraParts, { 'Number': 'number', 'Name on Card': 'cardholderName', 'Security Code': 'code' }, []); cipher.type = constants.cipherType.card; cipher.card = mappedData.dataObj; } else if (typeParts[1] === 'Address') { mappedData = parseSecureNoteMapping(extraParts, { 'Title': 'title', 'First Name': 'firstName', 'Last Name': 'lastName', 'Middle Name': 'middleName', 'Company': 'company', 'Address 1': 'address1', 'Address 2': 'address2', 'Address 3': 'address3', 'City / Town': 'city', 'State': 'state', 'Zip / Postal Code': 'postalCode', 'Country': 'country', 'Email Address': 'email', 'Username': 'username' }, []); cipher.type = constants.cipherType.identity; cipher.identity = mappedData.dataObj; } processedNote = true; cipher.notes = mappedData.notes; } } if (!processedNote) { cipher.secureNote = { type: 0 }; cipher.notes = value.extra && value.extra !== '' ? value.extra : null; } } else if (cipher.type === constants.cipherType.card) { cipher.card = parseCard(value); cipher.notes = value.notes && value.notes !== '' ? value.notes : null; } else if (cipher.type === constants.cipherType.identity) { cipher.identity = { title: value.title && value.title !== '' ? value.title : null, firstName: value.firstname && value.firstname !== '' ? value.firstname : null, middleName: value.middlename && value.middlename !== '' ? value.middlename : null, lastName: value.lastname && value.lastname !== '' ? value.lastname : null, username: value.username && value.username !== '' ? value.username : null, company: value.company && value.company !== '' ? value.company : null, ssn: value.ssn && value.ssn !== '' ? value.ssn : null, address1: value.address1 && value.address1 !== '' ? value.address1 : null, address2: value.address2 && value.address2 !== '' ? value.address2 : null, address3: value.address3 && value.address3 !== '' ? value.address3 : null, city: value.city && value.city !== '' ? value.city : null, state: value.state && value.state !== '' ? value.state : null, postalCode: value.zip && value.zip !== '' ? value.zip : null, country: value.country && value.country !== '' ? value.country : null, email: value.email && value.email !== '' ? value.email : null, phone: value.phone && value.phone !== '' ? value.phone : null }; cipher.notes = value.notes && value.notes !== '' ? value.notes : null; if (cipher.identity.title) { cipher.identity.title = cipher.identity.title.charAt(0).toUpperCase() + cipher.identity.title.slice(1); } if (value.ccnum && value.ccnum !== '') { // there is a card on this identity too var cardCipher = JSON.parse(JSON.stringify(cipher)); // cloned cardCipher.identity = null; cardCipher.type = constants.cipherType.card; cardCipher.card = parseCard(value); ciphers.push(cardCipher); } } ciphers.push(cipher); if (addFolder) { folders.push({ name: value.grouping }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; cipherRelationships.push(relationship); } }); success(folders, ciphers, cipherRelationships); } } function importSafeInCloudXml(file, success, error) { var folders = [], ciphers = [], cipherRelationships = [], foldersIndex = [], i = 0, j = 0; getXmlFileContents(file, parse, error); function parse(xmlDoc) { var xml = $(xmlDoc); var db = xml.find('database'); if (db.length) { var labels = db.find('> label'); if (labels.length) { for (i = 0; i < labels.length; i++) { var label = $(labels[i]); foldersIndex[label.attr('id')] = folders.length; folders.push({ name: label.attr('name') }); } } var cards = db.find('> card'); if (cards.length) { for (i = 0; i < cards.length; i++) { var card = $(cards[i]); if (card.attr('template') === 'true') { continue; } var cipher = { favorite: false, notes: '', name: card.attr('title'), fields: null }; if (!cipher.name || cipher.name === '') { cipher.name = '--'; } if (card.attr('type') === 'note') { cipher.type = constants.cipherType.secureNote; cipher.secureNote = { type: 0 // generic note }; } else { cipher.type = constants.cipherType.login; cipher.login = {}; var fields = card.find('> field'); for (j = 0; j < fields.length; j++) { var field = $(fields[j]); var text = field.text(); var type = field.attr('type'); var name = field.attr('name'); if (text && text !== '') { if (type === 'login') { cipher.login.username = text; } else if (type === 'password') { cipher.login.password = text; } else if (type === 'notes') { cipher.notes += (text + '\n'); } else if (type === 'weblogin' || type === 'website') { cipher.login.uris = makeUriArray(text); } else if (text.length > 200) { cipher.notes += (name + ': ' + text + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: name, value: text, type: constants.fieldType.text }); } } } } var notes = card.find('> notes'); for (j = 0; j < notes.length; j++) { cipher.notes += ($(notes[j]).text() + '\n'); } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); labels = card.find('> label_id'); if (labels.length) { var labelId = $(labels[0]).text(); var folderIndex = foldersIndex[labelId]; if (labelId !== null && labelId !== '' && folderIndex !== null) { cipherRelationships.push({ key: ciphers.length - 1, value: folderIndex }); } } } } success(folders, ciphers, cipherRelationships); } else { error(); } } } function importPadlockCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; var customFieldHeaders = []; // CSV index ref: 0 = name, 1 = category, 2 = username, 3 = password, 4+ = custom fields var i = 0, j = 0; for (i = 0; i < results.data.length; i++) { var value = results.data[i]; if (i === 0) { // header row for (j = 4; j < value.length; j++) { customFieldHeaders.push(value[j]); } continue; } var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = value[1] && value[1] !== '', addFolder = hasFolder; if (hasFolder) { for (j = 0; j < folders.length; j++) { if (folders[j].name === value[1]) { addFolder = false; folderIndex = j; break; } } } var cipher = { favorite: false, type: constants.cipherType.login, notes: null, name: value[0] && value[0] !== '' ? value[0] : '--', login: { uris: null, username: value[2] && value[2] !== '' ? value[2] : null, password: value[3] && value[3] !== '' ? value[3] : null }, fields: null }; if (customFieldHeaders.length) { for (j = 4; j < value.length; j++) { var cf = value[j]; if (!cf || cf === '') { continue; } var cfHeader = customFieldHeaders[j - 4]; if (cfHeader.toLowerCase() === 'url' || cfHeader.toLowerCase() === 'uri') { cipher.login.uris = makeUriArray(cf); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: cfHeader, value: cf, type: constants.fieldType.text }); } } } ciphers.push(cipher); if (addFolder) { folders.push({ name: value[1] }); } if (hasFolder) { folderRelationships.push({ key: cipherIndex, value: folderIndex }); } } success(folders, ciphers, folderRelationships); } }); } function importKeePass2Xml(file, success, error) { var folders = [], ciphers = [], folderRelationships = []; getXmlFileContents(file, parse, error); function parse(xmlDoc) { var xml = $(xmlDoc); var root = xml.find('Root'); if (root.length) { var group = root.find('> Group'); if (group.length) { traverse($(group[0]), true, ''); success(folders, ciphers, folderRelationships); } } else { error(); } } function traverse(node, isRootNode, groupNamePrefix) { var nodeEntries = []; var folderIndex = folders.length; var groupName = groupNamePrefix; if (!isRootNode) { if (groupName !== '') { groupName += ' > '; } groupName += node.find('> Name').text(); folders.push({ name: groupName }); } var entries = node.find('> Entry'); if (entries.length) { for (var i = 0; i < entries.length; i++) { var entry = $(entries[i]); var cipherIndex = ciphers.length; var cipher = { favorite: false, notes: null, name: null, type: constants.cipherType.login, login: { uris: null, username: null, password: null }, fields: null }; var entryStrings = entry.find('> String'); for (var j = 0; j < entryStrings.length; j++) { var entryString = $(entryStrings[j]); var key = entryString.find('> Key').text(); var value = entryString.find('> Value').text(); if (value === '') { continue; } switch (key) { case 'URL': cipher.login.uris = makeUriArray(value); break; case 'UserName': cipher.login.username = value; break; case 'Password': cipher.login.password = value; break; case 'Title': cipher.name = value; break; case 'Notes': cipher.notes = cipher.notes === null ? value + '\n' : cipher.notes + value + '\n'; break; default: if (value.length > 200 || value.indexOf('\n') > -1) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (key + ': ' + value + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } // other custom fields cipher.fields.push({ name: key, value: value, type: constants.fieldType.text }); } break; } } if (cipher.name === null) { cipher.name = '--'; } ciphers.push(cipher); if (!isRootNode) { folderRelationships.push({ key: cipherIndex, value: folderIndex }); } } } var groups = node.find('> Group'); if (groups.length) { for (var k = 0; k < groups.length; k++) { traverse($(groups[k]), false, groupName); } } } } function importKeePassXCsv(file, success, error) { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; angular.forEach(results.data, function (value, key) { value.Group = value.Group.startsWith('Root/') ? value.Group.replace('Root/', '') : value.Group; var groupName = value.Group && value.Group !== '' ? value.Group.split('/').join(' > ') : null; var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = groupName !== null, addFolder = hasFolder, i = 0; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === groupName) { addFolder = false; folderIndex = i; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: value.Notes && value.Notes !== '' ? value.Notes : null, name: value.Title && value.Title !== '' ? value.Title : '--', login: { uris: makeUriArray(value.URL), username: value.Username && value.Username !== '' ? value.Username : null, password: value.Password && value.Password !== '' ? value.Password : null } }; if (value.Title) { ciphers.push(cipher); } if (addFolder) { folders.push({ name: groupName }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } }); success(folders, ciphers, folderRelationships); } }); } function import1Password1Pif(file, success, error) { var folders = [], ciphers = [], i = 0; function parseFields(fields, cipher, designationKey, valueKey, nameKey) { for (var j = 0; j < fields.length; j++) { var field = fields[j]; if (!field[valueKey] || field[valueKey] === '') { continue; } var fieldValue = field[valueKey].toString(); if (cipher.type == constants.cipherType.login && !cipher.login.username && field[designationKey] && field[designationKey] === 'username') { cipher.login.username = fieldValue; } else if (cipher.type == constants.cipherType.login && !cipher.login.password && field[designationKey] && field[designationKey] === 'password') { cipher.login.password = fieldValue; } else if (cipher.type == constants.cipherType.login && !cipher.login.totp && field[designationKey] && field[designationKey].startsWith("TOTP_")) { cipher.login.totp = fieldValue; } else if (fieldValue) { var fieldName = (field[nameKey] || 'no_name'); if (fieldValue.indexOf('\\n') > -1 || fieldValue.length > 200) { if (cipher.notes === null) { cipher.notes = ''; } else { cipher.notes += '\n'; } cipher.notes += (fieldName + ': ' + fieldValue.split('\\r\\n').join('\n').split('\\n').join('\n')); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: fieldName, value: fieldValue, type: constants.fieldType.text }); } } } } getFileContents(file, parse, error); function parse(fileContent) { var fileLines = fileContent.split(/(?:\r\n|\r|\n)/); for (i = 0; i < fileLines.length; i++) { var line = fileLines[i]; if (!line.length || line[0] !== '{') { continue; } var item = JSON.parse(line); var cipher = { type: constants.cipherType.login, favorite: item.openContents && item.openContents.faveIndex ? true : false, notes: null, name: item.title && item.title !== '' ? item.title : '--', fields: null }; if (item.typeName === 'securenotes.SecureNote') { cipher.type = constants.cipherType.secureNote; cipher.secureNote = { type: 0 // generic note }; } else { cipher.type = constants.cipherType.login; cipher.login = { uris: makeUriArray(item.location), username: null, password: null, totp: null }; } if (item.secureContents) { if (item.secureContents.notesPlain && item.secureContents.notesPlain !== '') { cipher.notes = item.secureContents.notesPlain .split('\\r\\n').join('\n').split('\\n').join('\n'); } if (item.secureContents.fields) { parseFields(item.secureContents.fields, cipher, 'designation', 'value', 'name'); } if (item.secureContents.sections) { for (var j = 0; j < item.secureContents.sections.length; j++) { if (item.secureContents.sections[j].fields) { parseFields(item.secureContents.sections[j].fields, cipher, 'n', 'v', 't'); } } } } ciphers.push(cipher); } success(folders, ciphers, []); } } function import1Password6WinCsv(file, success, error) { var folders = [], ciphers = []; Papa.parse(file, { encoding: 'UTF-8', header: true, complete: function (results) { parseCsvErrors(results); for (var i = 0; i < results.data.length; i++) { var value = results.data[i]; if (!value.title) { continue; } var cipher = { type: constants.cipherType.login, favorite: false, notes: value.notesPlain && value.notesPlain !== '' ? value.notesPlain : '', name: value.title && value.title !== '' ? value.title : '--', login: { uris: null, username: null, password: null } }; for (var property in value) { if (value.hasOwnProperty(property)) { if (value[property] === null || value[property] === '') { continue; } if (!cipher.login.password && property === 'password') { cipher.login.password = value[property]; } else if (!cipher.login.username && property === 'username') { cipher.login.username = value[property]; } else if (!cipher.login.uris && property === 'urls') { var urls = value[property].split(/(?:\r\n|\r|\n)/); cipher.login.uris = makeUriArray(urls); } else if (property !== 'ainfo' && property !== 'autosubmit' && property !== 'notesPlain' && property !== 'ps' && property !== 'scope' && property !== 'tags' && property !== 'title' && property !== 'uuid' && !property.startsWith('section:')) { if (cipher.notes !== '') { cipher.notes += '\n'; } cipher.notes += (property + ': ' + value[property]); } } } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); } success(folders, ciphers, []); } }); } function importChromeCsv(file, success, error) { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; angular.forEach(results.data, function (value, key) { ciphers.push({ type: constants.cipherType.login, favorite: false, notes: null, name: value.name && value.name !== '' ? value.name : '--', login: { uris: makeUriArray(value.url), username: value.username && value.username !== '' ? value.username : null, password: value.password && value.password !== '' ? value.password : null } }); }); success(folders, ciphers, []); } }); } function importFirefoxPasswordExporterCsv(file, success, error) { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; angular.forEach(results.data, function (value, key) { ciphers.push({ type: constants.cipherType.login, favorite: false, notes: null, name: value.hostname && value.hostname !== '' ? loginNameFromUrl(value.hostname) : '--', login: { uris: makeUriArray(value.hostname), username: value.username && value.username !== '' ? value.username : null, password: value.password && value.password !== '' ? value.password : null } }); }); success(folders, ciphers, []); } }); } function importUpmCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; angular.forEach(results.data, function (value, key) { if (value.length === 5) { ciphers.push({ type: constants.cipherType.login, favorite: false, notes: value[4] && value[4] !== '' ? value[4] : null, name: value[0] && value[0] !== '' ? value[0] : '--', login: { uris: makeUriArray(value[3]), username: value[1] && value[1] !== '' ? value[1] : null, password: value[2] && value[2] !== '' ? value[2] : null } }); } }); success(folders, ciphers, []); } }); } function importKeeperCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; angular.forEach(results.data, function (value, key) { if (value.length >= 6) { var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = value[0] && value[0] !== '', addFolder = hasFolder, i = 0; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === value[0]) { addFolder = false; folderIndex = i; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: value[5] && value[5] !== '' ? value[5] : null, name: value[1] && value[1] !== '' ? value[1] : '--', login: { uris: makeUriArray(value[4]), username: value[2] && value[2] !== '' ? value[2] : null, password: value[3] && value[3] !== '' ? value[3] : null }, fields: null }; if (value.length > 6) { // we have some custom fields. for (i = 6; i < value.length; i = i + 2) { if (value[i + 1] && value[i + 1].length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (value[i] + ': ' + value[i + 1] + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: value[i], value: value[i + 1], type: constants.fieldType.text }); } } } ciphers.push(cipher); if (addFolder) { folders.push({ name: value[0] }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } } }); success(folders, ciphers, folderRelationships); } }); } function importPasswordDragonXml(file, success, error) { var folders = [], ciphers = [], folderRelationships = [], foldersIndex = [], j = 0; getXmlFileContents(file, parseXml, error); function parseXml(xmlDoc) { var xml = $(xmlDoc); var pwManager = xml.find('PasswordManager'); if (pwManager.length) { var records = pwManager.find('> record'); if (records.length) { for (var i = 0; i < records.length; i++) { var record = $(records[i]); var accountNameNode = record.find('> Account-Name'), accountName = accountNameNode.length ? $(accountNameNode) : null, userIdNode = record.find('> User-Id'), userId = userIdNode.length ? $(userIdNode) : null, passwordNode = record.find('> Password'), password = passwordNode.length ? $(passwordNode) : null, urlNode = record.find('> URL'), url = urlNode.length ? $(urlNode) : null, notesNode = record.find('> Notes'), notes = notesNode.length ? $(notesNode) : null, categoryNode = record.find('> Category'), category = categoryNode.length ? $(categoryNode) : null, categoryText = category ? category.text() : null; var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = categoryText && categoryText !== '' && categoryText !== 'Unfiled', addFolder = hasFolder; if (hasFolder) { for (j = 0; j < folders.length; j++) { if (folders[j].name === categoryText) { addFolder = false; folderIndex = j; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: notes && notes.text() !== '' ? notes.text() : null, name: accountName && accountName.text() !== '' ? accountName.text() : '--', login: { uris: url ? makeUriArray(url.text()) : null, username: userId && userId.text() !== '' ? userId.text() : null, password: password && password.text() !== '' ? password.text() : null }, fields: null }; var attributesSelector = ''; for (j = 1; j <= 10; j++) { attributesSelector += '> Attribute-' + j; if (j < 10) { attributesSelector += ', '; } } var attributes = record.find(attributesSelector); if (attributes.length) { // we have some attributes. add them as fields for (j = 0; j < attributes.length; j++) { var attr = $(attributes[j]), attrName = attr.prop('tagName'), attrValue = attr.text(); if (!attrValue || attrValue === '' || attrValue === 'null') { continue; } if (attrValue.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (attrName + ': ' + attrValue + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: attrName, value: attrValue, type: constants.fieldType.text }); } } } ciphers.push(cipher); if (addFolder) { folders.push({ name: categoryText }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } } } success(folders, ciphers, folderRelationships); } else { error(); } } } function importEnpassCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; for (var j = 0; j < results.data.length; j++) { var row = results.data[j]; if (row.length < 2) { continue; } if (j === 0 && row[0] === 'Title') { continue; } var note = row[row.length - 1]; var cipher = { type: constants.cipherType.login, name: row[0], favorite: false, notes: note && note !== '' ? note : null, fields: null, login: { uris: null, password: null, username: null, totp: null } }; if (row.length > 2 && (row.length % 2) === 0) { for (var i = 0; i < row.length - 2; i += 2) { var value = row[i + 2]; if (!value || value === '') { continue; } var field = row[i + 1]; var fieldLower = field.toLowerCase(); if (fieldLower === 'url' && !cipher.login.uris) { cipher.login.uris = makeUriArray(value); } else if ((fieldLower === 'username' || fieldLower === 'email') && !cipher.login.username) { cipher.login.username = value; } else if (fieldLower === 'password' && !cipher.login.password) { cipher.login.password = value; } else if (fieldLower === 'totp' && !cipher.login.totp) { cipher.login.totp = value; } else if (value.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (field + ': ' + value + '\n'); } else { // other fields if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: field, value: value, type: constants.fieldType.text }); } } } ciphers.push(cipher); } success(folders, ciphers, []); } }); } function importPasswordSafeXml(file, success, error) { var folders = [], ciphers = [], folderRelationships = [], foldersIndex = [], j = 0; getXmlFileContents(file, parseXml, error); function parseXml(xmlDoc) { var xml = $(xmlDoc); var pwsafe = xml.find('passwordsafe'); if (pwsafe.length) { var notesDelimiter = pwsafe.attr('delimiter'); var entries = pwsafe.find('> entry'); if (entries.length) { for (var i = 0; i < entries.length; i++) { var entry = $(entries[i]); var titleNode = entry.find('> title'), title = titleNode.length ? $(titleNode) : null, usernameNode = entry.find('> username'), username = usernameNode.length ? $(usernameNode) : null, emailNode = entry.find('> email'), email = emailNode.length ? $(emailNode) : null, emailText = email ? email.text() : null, passwordNode = entry.find('> password'), password = passwordNode.length ? $(passwordNode) : null, urlNode = entry.find('> url'), url = urlNode.length ? $(urlNode) : null, notesNode = entry.find('> notes'), notes = notesNode.length ? $(notesNode) : null, notesText = notes ? notes.text().split(notesDelimiter).join('\n') : null, groupNode = entry.find('> group'), group = groupNode.length ? $(groupNode) : null, groupText = group ? group.text().split('.').join(' > ') : null; var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = groupText && groupText !== '', addFolder = hasFolder; if (hasFolder) { for (j = 0; j < folders.length; j++) { if (folders[j].name === groupText) { addFolder = false; folderIndex = j; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: notes && notesText !== '' ? notesText : null, name: title && title.text() !== '' ? title.text() : '--', login: { uris: url ? makeUriArray(url.text()) : null, username: username && username.text() !== '' ? username.text() : null, password: password && password.text() !== '' ? password.text() : null } }; if (!cipher.login.username && emailText && emailText !== '') { cipher.login.username = emailText; } else if (emailText && emailText !== '') { cipher.notes = cipher.notes === null ? 'Email: ' + emailText : cipher.notes + '\n' + 'Email: ' + emailText; } ciphers.push(cipher); if (addFolder) { folders.push({ name: groupText }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } } } success(folders, ciphers, folderRelationships); } else { error(); } } } function importDashlaneCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; for (var j = 0; j < results.data.length; j++) { var skip = false; var row = results.data[j]; if (!row.length || row.length === 1) { continue; } var cipher = { type: constants.cipherType.login, name: row[0] && row[0] !== '' ? row[0] : '--', favorite: false, notes: null, login: { uris: null, password: null, username: null } }; if (row.length === 2) { cipher.login.uris = makeUriArray(row[1]); } else if (row.length === 3) { cipher.login.uris = makeUriArray(row[1]); cipher.login.username = row[2]; } else if (row.length === 4) { if (row[2] === '' && row[3] === '') { cipher.login.username = row[1]; cipher.notes = row[2] + '\n' + row[3]; } else { cipher.login.username = row[2]; cipher.notes = row[1] + '\n' + row[3]; } } else if (row.length === 5) { cipher.login.uris = makeUriArray(row[1]); cipher.login.username = row[2]; cipher.login.password = row[3]; cipher.notes = row[4]; } else if (row.length === 6) { if (row[2] === '') { cipher.login.username = row[3]; cipher.login.password = row[4]; cipher.notes = row[5]; } else { cipher.login.username = row[2]; cipher.login.password = row[3]; cipher.notes = row[4] + '\n' + row[5]; } cipher.login.uris = makeUriArray(row[1]); } else if (row.length === 7) { if (row[2] === '') { cipher.login.username = row[3]; cipher.notes = row[4] + '\n' + row[6]; } else { cipher.login.username = row[2]; cipher.notes = row[3] + '\n' + row[4] + '\n' + row[6]; } cipher.login.uris = makeUriArray(row[1]); cipher.login.password = row[5]; } else { cipher.notes = ''; for (var i = 1; i < row.length; i++) { cipher.notes = cipher.notes + row[i] + '\n'; if (row[i] === 'NO_TYPE') { skip = true; break; } } } if (skip) { continue; } if (cipher.login.username === '') { cipher.login.username = null; } if (cipher.login.password === '') { cipher.login.password = null; } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); } success(folders, ciphers, []); } }); } function importStickyPasswordXml(file, success, error) { var folders = [], ciphers = [], folderRelationships = [], foldersIndex = [], j = 0; function buildGroupText(database, groupId, groupText) { var group = database.find('> Groups > Group[ID="' + groupId + '"]'); if (group.length) { if (groupText && groupText !== '') { groupText = ' > ' + groupText; } groupText = group.attr('Name') + groupText; var parentGroupId = group.attr('ParentID'); return buildGroupText(database, parentGroupId, groupText); } return groupText; } getXmlFileContents(file, parseXml, error); function parseXml(xmlDoc) { var xml = $(xmlDoc); var database = xml.find('root > Database'); if (database.length) { var loginNodes = database.find('> Logins > Login'); if (loginNodes.length) { for (var i = 0; i < loginNodes.length; i++) { var loginNode = $(loginNodes[i]); var usernameText = loginNode.attr('Name'), passwordText = loginNode.attr('Password'), accountId = loginNode.attr('ID'), titleText = null, linkText = null, notesText = null, groupId = null, groupText = null; if (accountId && accountId !== '') { var accountLogin = database.find('> Accounts > Account > LoginLinks > Login[SourceLoginID="' + accountId + '"]'); if (accountLogin.length) { var account = accountLogin.parent().parent(); if (account.length) { titleText = account.attr('Name'); linkText = account.attr('Link'); groupId = account.attr('ParentID'); notesText = account.attr('Comments'); if (notesText) { notesText = notesText.split('/n').join('\n'); } } } } if (groupId && groupId !== '') { groupText = buildGroupText(database, groupId, ''); } var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = groupText && groupText !== '', addFolder = hasFolder; if (hasFolder) { for (j = 0; j < folders.length; j++) { if (folders[j].name === groupText) { addFolder = false; folderIndex = j; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: notesText && notesText !== '' ? notesText : null, name: titleText && titleText !== '' ? titleText : '--', login: { uris: makeUriArray(linkText), username: usernameText && usernameText !== '' ? usernameText : null, password: passwordText && passwordText !== '' ? passwordText : null } }; ciphers.push(cipher); if (addFolder) { folders.push({ name: groupText }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } } } success(folders, ciphers, folderRelationships); } else { error(); } } } function importmSecureCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; angular.forEach(results.data, function (value, key) { if (value.length >= 3) { var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = value[0] && value[0] !== '' && value[0] !== 'Unassigned', addFolder = hasFolder, i = 0; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === value[0]) { addFolder = false; folderIndex = i; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: '', name: value[2] && value[2] !== '' ? value[2] : null, login: { uris: null, username: null, password: null } }; if (value[1] === 'Web Logins') { cipher.login.uris = makeUriArray(value[4]); cipher.login.username = value[5] && value[5] !== '' ? value[5] : null; cipher.login.password = value[6] && value[6] !== '' ? value[6] : null; cipher.notes = value[3] && value[3] !== '' ? value[3].split('\\n').join('\n') : null; } else if (value.length > 3) { for (var j = 3; j < value.length; j++) { if (value[j] && value[j] !== '') { if (cipher.notes !== '') { cipher.notes = cipher.notes + '\n'; } cipher.notes = cipher.notes + value[j]; } } } if (value[1] && value[1] !== '' && value[1] !== 'Web Logins') { cipher.name = value[1] + ': ' + cipher.name; } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); if (addFolder) { folders.push({ name: value[0] }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } } }); success(folders, ciphers, folderRelationships); } }); } function importTrueKeyCsv(file, success, error) { var folders = [], ciphers = [], propsToIgnore = [ 'kind', 'autologin', 'favorite', 'hexcolor', 'protectedwithpassword', 'subdomainonly', 'type', 'tk_export_version', 'note', 'title', 'document_content' ]; Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); angular.forEach(results.data, function (value, key) { var cipher = { type: constants.cipherType.login, favorite: value.favorite && value.favorite.toLowerCase() === 'true' ? true : false, notes: value.memo && value.memo !== '' ? value.memo : null, name: value.name && value.name !== '' ? value.name : '--', login: { uris: makeUriArray(value.url), username: value.login && value.login !== '' ? value.login : null, password: value.password && value.password !== '' ? value.password : null }, fields: null }; if (value.kind !== 'login') { cipher.name = value.title && value.title !== '' ? value.title : '--'; cipher.notes = value.note && value.note !== '' ? value.note : null; if (!cipher.notes) { cipher.notes = value.document_content && value.document_content !== '' ? value.document_content : null; } for (var property in value) { if (value.hasOwnProperty(property) && propsToIgnore.indexOf(property.toLowerCase()) < 0 && value[property] && value[property] !== '') { if (value[property].length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (property + ': ' + value[property] + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } // other custom fields cipher.fields.push({ name: property, value: value[property], type: constants.fieldType.text }); } } } } ciphers.push(cipher); }); success(folders, ciphers, []); } }); } function importClipperzHtml(file, success, error) { var folders = [], ciphers = []; getFileContents(file, parse, error); function parse(fileContents) { var doc = $(fileContents); var textarea = doc.find('textarea'); var json = textarea && textarea.length ? textarea.val() : null; var entries = json ? JSON.parse(json) : null; if (entries && entries.length) { for (var i = 0; i < entries.length; i++) { var entry = entries[i]; var cipher = { type: constants.cipherType.login, favorite: false, notes: '', name: entry.label && entry.label !== '' ? entry.label.split(' ')[0] : '--', login: { uris: null, username: null, password: null }, fields: null }; if (entry.data && entry.data.notes && entry.data.notes !== '') { cipher.notes = entry.data.notes.split('\\n').join('\n'); } if (entry.currentVersion && entry.currentVersion.fields) { for (var property in entry.currentVersion.fields) { if (entry.currentVersion.fields.hasOwnProperty(property)) { var field = entry.currentVersion.fields[property]; var actionType = field.actionType.toLowerCase(); switch (actionType) { case 'password': cipher.login.password = field.value; break; case 'email': case 'username': case 'user': case 'name': cipher.login.username = field.value; break; case 'url': cipher.login.uris = makeUriArray(field.value); break; default: if (!cipher.login.username && isField(field.label, _usernameFieldNames)) { cipher.login.username = field.value; } else if (!cipher.login.password && isField(field.label, _passwordFieldNames)) { cipher.login.password = field.value; } else if (field.value.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (field.label + ': ' + field.value + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: field.label, value: field.value, type: constants.fieldType.text }); } break; } } } } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); } } success(folders, ciphers, []); } } function importAviraJson(file, success, error) { var folders = [], ciphers = [], i = 0; getFileContents(file, parseJson, error); function parseJson(fileContent) { var fileJson = JSON.parse(fileContent); if (fileJson) { if (fileJson.accounts) { for (i = 0; i < fileJson.accounts.length; i++) { var account = fileJson.accounts[i]; var cipher = { type: constants.cipherType.login, favorite: account.is_favorite && account.is_favorite === true, notes: null, name: account.label && account.label !== '' ? account.label : account.domain, login: { uris: makeUriArray(account.domain), username: account.username && account.username !== '' ? account.username : null, password: account.password && account.password !== '' ? account.password : null } }; if (account.email && account.email !== '') { if (!cipher.login.username || cipher.login.username === '') { cipher.login.username = account.email; } else { cipher.notes = account.email; } } if (!cipher.name || cipher.name === '') { cipher.name = '--'; } ciphers.push(cipher); } } } success(folders, ciphers, []); } } function importRoboFormHtml(file, success, error) { var folders = [], ciphers = []; getFileContents(file, parse, error); function parse(fileContents) { var doc = $(fileContents.split('­').join('').split('').join('')); var outterTables = doc.find('table.nobr'); if (outterTables.length) { for (var i = 0; i < outterTables.length; i++) { var outterTable = $(outterTables[i]); var cipher = { type: constants.cipherType.login, favorite: false, notes: '', name: outterTable.find('span.caption').text(), login: { uris: null, username: null, password: null }, fields: null }; var url = outterTable.find('.subcaption').text(); if (url && url !== '') { cipher.login.uris = makeUriArray(url); } var fields = []; /* jshint ignore:start */ $.each(outterTable.find('table td:not(.subcaption)'), function (indexInArray, valueOfElement) { $(valueOfElement).find('br').replaceWith('\n'); var t = $(valueOfElement).text(); if (t !== '') { fields.push(t.split('\\n').join('\n')); } }); /* jshint ignore:end */ if (fields.length && (fields.length % 2 === 0)) for (var j = 0; j < fields.length; j += 2) { var field = fields[j]; var fieldValue = fields[j + 1]; if (!cipher.login.password && isField(field.replace(':', ''), _passwordFieldNames)) { cipher.login.password = fieldValue; } else if (!cipher.login.username && isField(field.replace(':', ''), _usernameFieldNames)) { cipher.login.username = fieldValue; } else if (fieldValue.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (field + ': ' + fieldValue + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: field, value: fieldValue, type: constants.fieldType.text }); } } if (!cipher.notes || cipher.notes === '') { cipher.notes = null; } if (!cipher.name || cipher.name === '') { cipher.name = '--'; } ciphers.push(cipher); } } success(folders, ciphers, []); } } function importSaferPassCsv(file, success, error) { var folders = [], ciphers = []; Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); angular.forEach(results.data, function (value, key) { ciphers.push({ type: constants.cipherType.login, favorite: false, notes: value.notes && value.notes !== '' ? value.notes : null, name: value.url && value.url !== '' ? loginNameFromUrl(value.url) : '--', login: { uris: makeUriArray(value.url), username: value.username && value.username !== '' ? value.username : null, password: value.password && value.password !== '' ? value.password : null } }); }); success(folders, ciphers, []); } }); } function importAscendoCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; for (var j = 0; j < results.data.length; j++) { var row = results.data[j]; if (row.length < 2) { continue; } var note = row[row.length - 1]; var cipher = { type: constants.cipherType.login, name: row[0], favorite: false, notes: note && note !== '' ? note : null, login: { uris: null, password: null, username: null }, fields: null }; if (row.length > 2 && (row.length % 2) === 0) { for (var i = 0; i < row.length - 2; i += 2) { var value = row[i + 2]; var field = row[i + 1]; if (!field || field === '' || !value || value === '') { continue; } var fieldLower = field.toLowerCase(); if (!cipher.login.uris && isField(field, _uriFieldNames)) { cipher.login.uris = makeUriArray(value); } else if (!cipher.login.username && isField(field, _usernameFieldNames)) { cipher.login.username = value; } else if (!cipher.login.password && isField(field, _passwordFieldNames)) { cipher.login.password = value; } else if (value.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (field + ': ' + value + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } // other custom fields cipher.fields.push({ name: field, value: value, type: constants.fieldType.text }); } } } ciphers.push(cipher); } success(folders, ciphers, []); } }); } function importPasswordBossJson(file, success, error) { var folders = [], ciphers = [], i = 0; getFileContents(file, parseJson, error); function parseJson(fileContent) { var fileJson = JSON.parse(fileContent); if (fileJson && fileJson.length) { for (i = 0; i < fileJson.length; i++) { var item = fileJson[i]; var cipher = { type: constants.cipherType.login, favorite: false, notes: '', name: item.name && item.name !== '' ? item.name : '--', login: { uris: makeUriArray(item.login_url), username: null, password: null }, fields: null }; if (!item.identifiers) { continue; } if (item.identifiers.notes && item.identifiers.notes !== '') { cipher.notes = item.identifiers.notes.split('\\r\\n').join('\n').split('\\n').join('\n'); } for (var property in item.identifiers) { if (item.identifiers.hasOwnProperty(property)) { var value = item.identifiers[property]; if (property === 'notes' || value === '' || value === null) { continue; } if (property === 'username') { cipher.login.username = value; } else if (property === 'password') { cipher.login.password = value; } else if (value.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (property + ': ' + value + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: property, value: value, type: constants.fieldType.text }); } } } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); } } success(folders, ciphers, []); } } function importZohoVaultCsv(file, success, error) { function parseData(data, cipher) { if (!data || data === '') { return; } var dataLines = data.split(/(?:\r\n|\r|\n)/); for (var i = 0; i < dataLines.length; i++) { var line = dataLines[i]; var delimPosition = line.indexOf(':'); if (delimPosition < 0) { continue; } var field = line.substring(0, delimPosition); var value = line.length > delimPosition ? line.substring(delimPosition + 1) : null; if (!field || field === '' || !value || value === '' || field === 'SecretType') { continue; } var fieldLower = field.toLowerCase(); if (fieldLower === 'user name') { cipher.login.username = value; } else if (fieldLower === 'password') { cipher.login.password = value; } else if (value.length > 200) { if (!cipher.notes) { cipher.notes = ''; } cipher.notes += (field + ': ' + value + '\n'); } else { if (!cipher.fields) { cipher.fields = []; } cipher.fields.push({ name: field, value: value, type: constants.fieldType.text }); } } } Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; angular.forEach(results.data, function (value, key) { var chamber = value.ChamberName; var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = chamber && chamber !== '', addFolder = hasFolder, i = 0; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === chamber) { addFolder = false; folderIndex = i; break; } } } var cipher = { type: constants.cipherType.login, favorite: value.Favorite && value.Favorite === '1' ? true : false, notes: value.Notes && value.Notes !== '' ? value.Notes : '', name: value['Secret Name'] && value['Secret Name'] !== '' ? value['Secret Name'] : '--', login: { uris: makeUriArray(value['Secret URL']), username: null, password: null }, fields: null }; parseData(value.SecretData, cipher); parseData(value.CustomData, cipher); if (cipher.notes === '') { cipher.notes = null; } if (value['Secret Name']) { ciphers.push(cipher); } if (addFolder) { folders.push({ name: chamber }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } }); success(folders, ciphers, folderRelationships); } }); } function importSplashIdCsv(file, success, error) { Papa.parse(file, { encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; function parseFieldsToNotes(startIndex, row, cipher) { // last 3 rows do not get parsed for (var k = startIndex; k < row.length - 3; k++) { if (!row[k] || row[k] === '') { continue; } if (!cipher.notes) { cipher.notes = ''; } else if (cipher.notes !== '') { cipher.notes += '\n'; } cipher.notes += row[k]; } } // skip 1st row since its not data for (var i = 1; i < results.data.length; i++) { if (results.data[i].length < 3) { continue; } var value = results.data[i], category = value[results.data.length - 1], notes = value[results.data.length - 2], type = value[0]; var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = category && category !== '' && category !== 'Unfiled', addFolder = hasFolder, j = 0; if (hasFolder) { for (j = 0; j < folders.length; j++) { if (folders[j].name === category) { addFolder = false; folderIndex = j; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: notes, name: value[1] && value[1] !== '' ? value[1] : '--', fields: null, login: { uris: null, username: null, password: null } }; if (type === 'Web Logins' || type === 'Servers' || type === 'Email Accounts') { cipher.login.uris = makeUriArray(value[4]); cipher.login.username = value[2] && value[2] !== '' ? value[2] : null; cipher.login.password = value[3] && value[3] !== '' ? value[3] : null; parseFieldsToNotes(5, value, cipher); } else if (value.length > 2) { parseFieldsToNotes(2, value, cipher); } if (cipher.name && cipher.name !== '--' && type !== 'Web Logins' && type !== 'Servers' && type !== 'Email Accounts') { cipher.name = type + ': ' + cipher.name; } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); if (addFolder) { folders.push({ name: category }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } } success(folders, ciphers, folderRelationships); } }); } function importMeldiumCsv(file, success, error) { Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = []; for (var j = 0; j < results.data.length; j++) { var row = results.data[j]; var cipher = { type: constants.cipherType.login, name: row.DisplayName && row.DisplayName !== '' ? row.DisplayName : '--', favorite: false, notes: row.Notes && row.Notes !== '' ? row.Notes : null, login: { uris: makeUriArray(row.Url), password: row.Password && row.Password !== '' ? row.Password : null, username: row.UserName && row.UserName !== '' ? row.UserName : null } }; ciphers.push(cipher); } success(folders, ciphers, []); } }); } function importPassKeepCsv(file, success, error) { function getValue(key, obj) { var val = obj[key] || obj[(' ' + key)]; if (val && val !== '') { return val; } return null; } Papa.parse(file, { header: true, encoding: 'UTF-8', complete: function (results) { parseCsvErrors(results); var folders = [], ciphers = [], folderRelationships = []; angular.forEach(results.data, function (value, key) { var folderIndex = folders.length, cipherIndex = ciphers.length, hasFolder = !!getValue('category', value), addFolder = hasFolder, i = 0; if (hasFolder) { for (i = 0; i < folders.length; i++) { if (folders[i].name === getValue('category', value)) { addFolder = false; folderIndex = i; break; } } } var cipher = { type: constants.cipherType.login, favorite: false, notes: !!getValue('description', value) ? getValue('description', value) : null, name: !!getValue('title', value) ? getValue('title', value) : '--', login: { uris: !!getValue('site', value) ? makeUriArray(getValue('site', value)) : null, username: !!getValue('username', value) ? getValue('username', value) : null, password: !!getValue('password', value) ? getValue('password', value) : null } }; if (!!getValue('password2', value)) { if (!cipher.notes) { cipher.notes = ''; } else { cipher.notes += '\n'; } cipher.notes += ('Password 2: ' + getValue('password2', value)); } ciphers.push(cipher); if (addFolder) { folders.push({ name: getValue('category', value) }); } if (hasFolder) { var relationship = { key: cipherIndex, value: folderIndex }; folderRelationships.push(relationship); } }); success(folders, ciphers, folderRelationships); } }); } function importGnomeJson(file, success, error) { var folders = [], ciphers = [], folderRelationships = [], i = 0; getFileContents(file, parseJson, error); function parseJson(fileContent) { var fileJson = JSON.parse(fileContent); var folderIndex = 0; var cipherIndex = 0; if (fileJson && Object.keys(fileJson).length) { for (var keyRing in fileJson) { if (fileJson.hasOwnProperty(keyRing) && fileJson[keyRing].length) { folderIndex = folders.length; folders.push({ name: keyRing }); for (i = 0; i < fileJson[keyRing].length; i++) { var item = fileJson[keyRing][i]; if (!item.display_name || item.display_name.indexOf('http') !== 0) { continue; } cipherIndex = ciphers.length; var cipher = { type: constants.cipherType.login, favorite: false, notes: '', name: item.display_name.replace('http://', '').replace('https://', ''), login: { uris: makeUriArray(item.display_name), username: item.attributes.username_value && item.attributes.username_value !== '' ? item.attributes.username_value : null, password: item.secret && item.secret !== '' ? item.secret : null } }; if (cipher.name > 30) { cipher.name = cipher.name.substring(0, 30); } for (var attr in item.attributes) { if (item.attributes.hasOwnProperty(attr) && attr !== 'username_value' && attr !== 'xdg:schema') { if (cipher.notes !== '') { cipher.notes += '\n'; } cipher.notes += (attr + ': ' + item.attributes[attr]); } } if (cipher.notes === '') { cipher.notes = null; } ciphers.push(cipher); folderRelationships.push({ key: cipherIndex, value: folderIndex }); } } } } success(folders, ciphers, folderRelationships); } } return _service; }]); angular .module('bit.services') .factory('passwordService', function () { var _service = {}; _service.generatePassword = function (options) { var defaults = { length: 10, ambiguous: false, number: true, minNumber: 1, uppercase: true, minUppercase: 1, lowercase: true, minLowercase: 1, special: false, minSpecial: 1 }; // overload defaults with given options var o = angular.extend({}, defaults, options); // sanitize if (o.uppercase && o.minUppercase < 0) o.minUppercase = 1; if (o.lowercase && o.minLowercase < 0) o.minLowercase = 1; if (o.number && o.minNumber < 0) o.minNumber = 1; if (o.special && o.minSpecial < 0) o.minSpecial = 1; if (!o.length || o.length < 1) o.length = 10; var minLength = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; if (o.length < minLength) o.length = minLength; var positions = []; if (o.lowercase && o.minLowercase > 0) { for (var i = 0; i < o.minLowercase; i++) { positions.push('l'); } } if (o.uppercase && o.minUppercase > 0) { for (var j = 0; j < o.minUppercase; j++) { positions.push('u'); } } if (o.number && o.minNumber > 0) { for (var k = 0; k < o.minNumber; k++) { positions.push('n'); } } if (o.special && o.minSpecial > 0) { for (var l = 0; l < o.minSpecial; l++) { positions.push('s'); } } while (positions.length < o.length) { positions.push('a'); } // shuffle positions.sort(function () { return randomInt(0, 1) * 2 - 1; }); // build out the char sets var allCharSet = ''; var lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; if (o.ambiguous) lowercaseCharSet += 'l'; if (o.lowercase) allCharSet += lowercaseCharSet; var uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; if (o.ambiguous) uppercaseCharSet += 'O'; if (o.uppercase) allCharSet += uppercaseCharSet; var numberCharSet = '23456789'; if (o.ambiguous) numberCharSet += '01'; if (o.number) allCharSet += numberCharSet; var specialCharSet = '!@#$%^&*'; if (o.special) allCharSet += specialCharSet; var password = ''; for (var m = 0; m < o.length; m++) { var positionChars; switch (positions[m]) { case 'l': positionChars = lowercaseCharSet; break; case 'u': positionChars = uppercaseCharSet; break; case 'n': positionChars = numberCharSet; break; case 's': positionChars = specialCharSet; break; case 'a': positionChars = allCharSet; break; } var randomCharIndex = randomInt(0, positionChars.length - 1); password += positionChars.charAt(randomCharIndex); } return password; }; // EFForg/OpenWireless // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js function randomInt(min, max) { var rval = 0; var range = max - min; var bits_needed = Math.ceil(Math.log2(range)); if (bits_needed > 53) { throw new Exception("We cannot generate numbers larger than 53 bits."); } var bytes_needed = Math.ceil(bits_needed / 8); var mask = Math.pow(2, bits_needed) - 1; // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 // Create byte array and fill with N random numbers var byteArray = new Uint8Array(bytes_needed); window.crypto.getRandomValues(byteArray); var p = (bytes_needed - 1) * 8; for (var i = 0; i < bytes_needed; i++) { rval += byteArray[i] * Math.pow(2, p); p -= 8; } // Use & to apply the mask and reduce the number of recursive lookups rval = rval & mask; if (rval >= range) { // Integer out of acceptable range return randomInt(min, max); } // Return an integer that falls within the range return min + rval; } return _service; }); angular .module('bit.services') .factory('tokenService', ["$sessionStorage", "$localStorage", "jwtHelper", function ($sessionStorage, $localStorage, jwtHelper) { var _service = {}, _token = null, _refreshToken = null; _service.setToken = function (token) { $sessionStorage.accessToken = token; _token = token; }; _service.getToken = function () { if (!_token) { _token = $sessionStorage.accessToken; } return _token ? _token : null; }; _service.clearToken = function () { _token = null; delete $sessionStorage.accessToken; }; _service.setRefreshToken = function (token) { $sessionStorage.refreshToken = token; _refreshToken = token; }; _service.getRefreshToken = function () { if (!_refreshToken) { _refreshToken = $sessionStorage.refreshToken; } return _refreshToken ? _refreshToken : null; }; _service.clearRefreshToken = function () { _refreshToken = null; delete $sessionStorage.refreshToken; }; _service.setTwoFactorToken = function (token, email) { if (!$localStorage.twoFactor) { $localStorage.twoFactor = {}; } $localStorage.twoFactor[email] = token; }; _service.getTwoFactorToken = function (email) { return $localStorage.twoFactor ? $localStorage.twoFactor[email] : null; }; _service.clearTwoFactorToken = function (email) { if (email) { if ($localStorage.twoFactor && $localStorage.twoFactor[email]) { delete $localStorage.twoFactor[email]; } } else { delete $localStorage.twoFactor; } }; _service.clearTokens = function () { _service.clearToken(); _service.clearRefreshToken(); }; _service.tokenSecondsRemaining = function (token, offsetSeconds) { var d = jwtHelper.getTokenExpirationDate(token); offsetSeconds = offsetSeconds || 0; if (d === null) { return 0; } var msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); return Math.round(msRemaining / 1000); }; _service.tokenNeedsRefresh = function (token, minutes) { minutes = minutes || 5; // default 5 minutes var sRemaining = _service.tokenSecondsRemaining(token); return sRemaining < (60 * minutes); }; return _service; }]); angular .module('bit.services') .factory('utilsService', ["constants", function (constants) { var _service = {}; var _browserCache; _service.getDeviceType = function (token) { if (_browserCache) { return _browserCache; } if (navigator.userAgent.indexOf(' Vivaldi/') >= 0) { _browserCache = constants.deviceType.vivaldi; } else if (!!window.chrome && !!window.chrome.webstore) { _browserCache = constants.deviceType.chrome; } else if (typeof InstallTrigger !== 'undefined') { _browserCache = constants.deviceType.firefox; } else if ((!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) { _browserCache = constants.deviceType.firefox; } else if (/constructor/i.test(window.HTMLElement) || safariCheck(!window.safari || (typeof safari !== 'undefined' && safari.pushNotification))) { _browserCache = constants.deviceType.opera; } else if (!!document.documentMode) { _browserCache = constants.deviceType.ie; } else if (!!window.StyleMedia) { _browserCache = constants.deviceType.edge; } else { _browserCache = constants.deviceType.unknown; } return _browserCache; }; function safariCheck(p) { return p.toString() === '[object SafariRemoteNotification]'; } return _service; }]); angular .module('bit.services') .factory('validationService', function () { var _service = {}; _service.addErrors = function (form, reason) { var data = reason.data; var defaultErrorMessage = 'An unexpected error has occurred.'; form.$errors = []; if (!data || !angular.isObject(data)) { form.$errors.push(defaultErrorMessage); return; } if (data && data.ErrorModel) { data = data.ErrorModel; } if (!data.ValidationErrors) { if (data.Message) { form.$errors.push(data.Message); } else { form.$errors.push(defaultErrorMessage); } return; } for (var key in data.ValidationErrors) { if (!data.ValidationErrors.hasOwnProperty(key)) { continue; } for (var i = 0; i < data.ValidationErrors[key].length; i++) { _service.addError(form, key, data.ValidationErrors[key][i]); } } }; _service.addError = function (form, key, errorMessage, clearExistingErrors) { if (clearExistingErrors || !form.$errors) { form.$errors = []; } var pushError = true; for (var i = 0; i < form.$errors.length; i++) { if (form.$errors[i] === errorMessage) { pushError = false; break; } } if (pushError) { form.$errors.push(errorMessage); } if (key && key !== '' && form[key] && form[key].$registerApiError) { form[key].$registerApiError(); } }; _service.parseErrors = function (reason) { var data = reason.data; var defaultErrorMessage = 'An unexpected error has occurred.'; var errors = []; if (!data || !angular.isObject(data)) { errors.push(defaultErrorMessage); return errors; } if (data && data.ErrorModel) { data = data.ErrorModel; } if (!data.ValidationErrors) { if (data.Message) { errors.push(data.Message); } else { errors.push(defaultErrorMessage); } } for (var key in data.ValidationErrors) { if (!data.ValidationErrors.hasOwnProperty(key)) { continue; } for (var i = 0; i < data.ValidationErrors[key].length; i++) { errors.push(data.ValidationErrors[key][i]); } } return errors; }; return _service; }); angular .module('bit.tools') .controller('reportsBreachController', ["$scope", "apiService", "toastr", "authService", function ($scope, apiService, toastr, authService) { $scope.loading = true; $scope.error = false; $scope.breachAccounts = []; $scope.email = null; $scope.$on('$viewContentLoaded', function () { authService.getUserProfile().then(function (userProfile) { $scope.email = userProfile.email; return apiService.hibp.get({ email: $scope.email }).$promise; }).then(function (response) { var breachAccounts = []; for (var i = 0; i < response.length; i++) { var breach = { id: response[i].Name, title: response[i].Title, domain: response[i].Domain, date: new Date(response[i].BreachDate), reportedDate: new Date(response[i].AddedDate), modifiedDate: new Date(response[i].ModifiedDate), count: response[i].PwnCount, description: response[i].Description, classes: response[i].DataClasses, image: 'https://haveibeenpwned.com/Content/Images/PwnedLogos/' + response[i].Name + '.' + response[i].LogoType }; breachAccounts.push(breach); } $scope.breachAccounts = breachAccounts; $scope.loading = false; }, function (response) { $scope.error = response.status !== 404; $scope.loading = false; }); }); }]); angular .module('bit.vault') .controller('settingsAddEditEquivalentDomainController', ["$scope", "$uibModalInstance", "$analytics", "domainIndex", "domains", function ($scope, $uibModalInstance, $analytics, domainIndex, domains) { $analytics.eventTrack('settingsAddEditEquivalentDomainController', { category: 'Modal' }); $scope.domains = domains; $scope.index = domainIndex; $scope.submit = function (form) { $analytics.eventTrack((domainIndex ? 'Edited' : 'Added') + ' Equivalent Domain'); $uibModalInstance.close({ domains: $scope.domains, index: domainIndex }); }; $scope.close = function () { $uibModalInstance.dismiss('close'); }; }]); angular .module('bit.settings') .controller('settingsBillingAdjustStorageController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "add", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, add) { $analytics.eventTrack('settingsBillingAdjustStorageController', { category: 'Modal' }); $scope.add = add; $scope.storageAdjustment = 0; $scope.submit = function () { var request = { storageGbAdjustment: $scope.storageAdjustment }; if (!add) { request.storageGbAdjustment *= -1; } $scope.submitPromise = apiService.accounts.putStorage(null, request) .$promise.then(function (response) { if (add) { $analytics.eventTrack('Added Storage'); toastr.success('You have added ' + $scope.storageAdjustment + ' GB.'); } else { $analytics.eventTrack('Removed Storage'); toastr.success('You have removed ' + $scope.storageAdjustment + ' GB.'); } $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.organization') .controller('settingsBillingChangePaymentController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "existingPaymentMethod", "appSettings", "$timeout", "stripe", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, existingPaymentMethod, appSettings, $timeout /* jshint ignore:start */ , stripe /* jshint ignore:end */ ) { $analytics.eventTrack('settingsBillingChangePaymentController', { category: 'Modal' }); $scope.existingPaymentMethod = existingPaymentMethod; $scope.paymentMethod = 'card'; $scope.dropinLoaded = false; $scope.showPaymentOptions = false; $scope.hideBank = true; $scope.card = {}; var btInstance = null; $scope.changePaymentMethod = function (val) { $scope.paymentMethod = val; if ($scope.paymentMethod !== 'paypal') { return; } braintree.dropin.create({ authorization: appSettings.braintreeKey, container: '#bt-dropin-container', paymentOptionPriority: ['paypal'], paypal: { flow: 'vault', buttonStyle: { label: 'pay', size: 'medium', shape: 'pill', color: 'blue' } } }, function (createErr, instance) { if (createErr) { console.error(createErr); return; } btInstance = instance; $timeout(function () { $scope.dropinLoaded = true; }); }); }; $scope.submit = function () { $scope.submitPromise = getPaymentToken($scope.card).then(function (token) { if (!token) { throw 'No payment token.'; } var request = { paymentToken: token }; return apiService.accounts.putPayment(null, request).$promise; }, function (err) { throw err; }).then(function (response) { $scope.card = null; if (existingPaymentMethod) { $analytics.eventTrack('Changed Payment Method'); toastr.success('You have changed your payment method.'); } else { $analytics.eventTrack('Added Payment Method'); toastr.success('You have added a payment method.'); } $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; function getPaymentToken(card) { if ($scope.paymentMethod === 'paypal') { return btInstance.requestPaymentMethod().then(function (payload) { return payload.nonce; }).catch(function (err) { throw err.message; }); } else { return stripe.card.createToken(card).then(function (response) { return response.id; }).catch(function (err) { throw err.message; }); } } }]); angular .module('bit.settings') .controller('settingsBillingController', ["$scope", "apiService", "authService", "$state", "$uibModal", "toastr", "$analytics", "appSettings", function ($scope, apiService, authService, $state, $uibModal, toastr, $analytics, appSettings) { $scope.selfHosted = appSettings.selfHosted; $scope.charges = []; $scope.paymentSource = null; $scope.subscription = null; $scope.loading = true; var license = null; $scope.expiration = null; $scope.$on('$viewContentLoaded', function () { load(); }); $scope.changePayment = function () { if ($scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingChangePayment.html', controller: 'settingsBillingChangePaymentController', resolve: { existingPaymentMethod: function () { return $scope.paymentSource ? $scope.paymentSource.description : null; } } }); modal.result.then(function () { load(); }); }; $scope.adjustStorage = function (add) { if ($scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingAdjustStorage.html', controller: 'settingsBillingAdjustStorageController', resolve: { add: function () { return add; } } }); modal.result.then(function () { load(); }); }; $scope.cancel = function () { if ($scope.selfHosted) { return; } if (!confirm('Are you sure you want to cancel? You will lose access to all premium features at the end ' + 'of this billing cycle.')) { return; } apiService.accounts.putCancelPremium({}, {}) .$promise.then(function (response) { $analytics.eventTrack('Canceled Premium'); toastr.success('Premium subscription has been canceled.'); load(); }); }; $scope.reinstate = function () { if ($scope.selfHosted) { return; } if (!confirm('Are you sure you want to remove the cancellation request and reinstate your premium membership?')) { return; } apiService.accounts.putReinstatePremium({}, {}) .$promise.then(function (response) { $analytics.eventTrack('Reinstated Premium'); toastr.success('Premium cancellation request has been removed.'); load(); }); }; $scope.updateLicense = function () { if (!$scope.selfHosted) { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsBillingUpdateLicense.html', controller: 'settingsBillingUpdateLicenseController' }); modal.result.then(function () { load(); }); }; $scope.license = function () { if ($scope.selfHosted) { return; } var licenseString = JSON.stringify(license, null, 2); var licenseBlob = new Blob([licenseString]); // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(licenseBlob, 'bitwarden_premium_license.json'); } else { var a = window.document.createElement('a'); a.href = window.URL.createObjectURL(licenseBlob, { type: 'text/plain' }); a.download = 'bitwarden_premium_license.json'; document.body.appendChild(a); // IE: "Access is denied". // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access a.click(); document.body.removeChild(a); } }; function load() { authService.getUserProfile().then(function (profile) { $scope.premium = profile.premium; if (!profile.premium) { return null; } return apiService.accounts.getBilling({}).$promise; }).then(function (billing) { if (!billing) { return $state.go('backend.user.settingsPremium'); } var i = 0; $scope.expiration = billing.Expiration; license = billing.License; $scope.storage = null; if (billing && billing.MaxStorageGb) { $scope.storage = { currentGb: billing.StorageGb || 0, maxGb: billing.MaxStorageGb, currentName: billing.StorageName || '0 GB' }; $scope.storage.percentage = +(100 * ($scope.storage.currentGb / $scope.storage.maxGb)).toFixed(2); } $scope.subscription = null; if (billing && billing.Subscription) { $scope.subscription = { trialEndDate: billing.Subscription.TrialEndDate, cancelledDate: billing.Subscription.CancelledDate, status: billing.Subscription.Status, cancelled: billing.Subscription.Cancelled, markedForCancel: !billing.Subscription.Cancelled && billing.Subscription.CancelAtEndDate }; } $scope.nextInvoice = null; if (billing && billing.UpcomingInvoice) { $scope.nextInvoice = { date: billing.UpcomingInvoice.Date, amount: billing.UpcomingInvoice.Amount }; } if (billing && billing.Subscription && billing.Subscription.Items) { $scope.subscription.items = []; for (i = 0; i < billing.Subscription.Items.length; i++) { $scope.subscription.items.push({ amount: billing.Subscription.Items[i].Amount, name: billing.Subscription.Items[i].Name, interval: billing.Subscription.Items[i].Interval, qty: billing.Subscription.Items[i].Quantity }); } } $scope.paymentSource = null; if (billing && billing.PaymentSource) { $scope.paymentSource = { type: billing.PaymentSource.Type, description: billing.PaymentSource.Description, cardBrand: billing.PaymentSource.CardBrand }; } var charges = []; if (billing && billing.Charges) { for (i = 0; i < billing.Charges.length; i++) { charges.push({ date: billing.Charges[i].CreatedDate, paymentSource: billing.Charges[i].PaymentSource ? billing.Charges[i].PaymentSource.Description : '-', amount: billing.Charges[i].Amount, status: billing.Charges[i].Status, failureMessage: billing.Charges[i].FailureMessage, refunded: billing.Charges[i].Refunded, partiallyRefunded: billing.Charges[i].PartiallyRefunded, refundedAmount: billing.Charges[i].RefundedAmount, invoiceId: billing.Charges[i].InvoiceId }); } } $scope.charges = charges; $scope.loading = false; }); } }]); angular .module('bit.settings') .controller('settingsBillingUpdateLicenseController', ["$scope", "$state", "$uibModalInstance", "apiService", "$analytics", "toastr", "validationService", function ($scope, $state, $uibModalInstance, apiService, $analytics, toastr, validationService) { $analytics.eventTrack('settingsBillingUpdateLicenseController', { category: 'Modal' }); $scope.submit = function (form) { var fileEl = document.getElementById('file'); var files = fileEl.files; if (!files || !files.length) { validationService.addError(form, 'file', 'Select a license file.', true); return; } var fd = new FormData(); fd.append('license', files[0]); $scope.submitPromise = apiService.accounts.putLicense(fd) .$promise.then(function (response) { $analytics.eventTrack('Updated License'); toastr.success('You have updated your license.'); $uibModalInstance.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.settings') .controller('settingsChangeEmailController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", "validationService", function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics, validationService) { $analytics.eventTrack('settingsChangeEmailController', { category: 'Modal' }); var _masterPasswordHash, _masterPassword, _newEmail; $scope.token = function (model, form) { var encKey = cryptoService.getEncKey(); if (!encKey) { validationService.addError(form, null, 'You cannot change your email until you update your encryption key.', true); return; } _masterPassword = model.masterPassword; _newEmail = model.newEmail.toLowerCase(); $scope.tokenPromise = cryptoService.hashPassword(_masterPassword).then(function (hash) { _masterPasswordHash = hash; var request = { newEmail: _newEmail, masterPasswordHash: _masterPasswordHash }; return apiService.accounts.emailToken(request, function () { $scope.tokenSent = true; }).$promise; }); }; $scope.confirm = function (model) { $scope.confirmPromise = cryptoService.makeKeyAndHash(_newEmail, _masterPassword).then(function (result) { var encKey = cryptoService.getEncKey(); var newEncKey = cryptoService.encrypt(encKey.key, result.key, 'raw'); var request = { token: model.token, newEmail: _newEmail, masterPasswordHash: _masterPasswordHash, newMasterPasswordHash: result.hash, key: newEncKey }; return apiService.accounts.email(request).$promise; }).then(function () { $uibModalInstance.dismiss('cancel'); authService.logOut(); $analytics.eventTrack('Changed Email'); return $state.go('frontend.login.info'); }).then(function () { toastr.success('Please log back in.', 'Email Changed'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.settings') .controller('settingsChangePasswordController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "authService", "validationService", "toastr", "$analytics", function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, validationService, toastr, $analytics) { $analytics.eventTrack('settingsChangePasswordController', { category: 'Modal' }); $scope.save = function (model, form) { var error = false; var encKey = cryptoService.getEncKey(); if (!encKey) { validationService.addError(form, null, 'You cannot change your master password until you update your encryption key.', true); error = true; } if ($scope.model.newMasterPassword.length < 8) { validationService.addError(form, 'NewMasterPasswordHash', 'Master password must be at least 8 characters long.', true); error = true; } if ($scope.model.newMasterPassword !== $scope.model.confirmNewMasterPassword) { validationService.addError(form, 'ConfirmNewMasterPasswordHash', 'New master password confirmation does not match.', true); error = true; } if (error) { return; } var makeResult; $scope.savePromise = authService.getUserProfile().then(function (profile) { return cryptoService.makeKeyAndHash(profile.email, model.newMasterPassword); }).then(function (result) { makeResult = result; return cryptoService.hashPassword(model.masterPassword); }).then(function (hash) { var encKey = cryptoService.getEncKey(); var newEncKey = cryptoService.encrypt(encKey.key, makeResult.key, 'raw'); var request = { masterPasswordHash: hash, newMasterPasswordHash: makeResult.hash, key: newEncKey }; return apiService.accounts.putPassword(request).$promise; }).then(function () { $uibModalInstance.dismiss('cancel'); authService.logOut(); $analytics.eventTrack('Changed Password'); return $state.go('frontend.login.info'); }).then(function () { toastr.success('Please log back in.', 'Master Password Changed'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.settings') .controller('settingsController', ["$scope", "$state", "$uibModal", "apiService", "toastr", "authService", "$localStorage", "$rootScope", "cipherService", function ($scope, $state, $uibModal, apiService, toastr, authService, $localStorage, $rootScope, cipherService) { $scope.model = { profile: {}, email: null, disableWebsiteIcons: false }; $scope.$on('$viewContentLoaded', function () { apiService.accounts.getProfile({}, function (user) { $scope.model = { profile: { name: user.Name, masterPasswordHint: user.MasterPasswordHint, culture: user.Culture }, email: user.Email, disableWebsiteIcons: $localStorage.disableWebsiteIcons }; if (user.Organizations) { var orgs = []; for (var i = 0; i < user.Organizations.length; i++) { // Only confirmed if (user.Organizations[i].Status !== 2) { continue; } orgs.push({ id: user.Organizations[i].Id, name: user.Organizations[i].Name, status: user.Organizations[i].Status, type: user.Organizations[i].Type, enabled: user.Organizations[i].Enabled }); } $scope.model.organizations = orgs; } }); }); $scope.generalSave = function () { $scope.generalPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) { authService.setUserProfile(profile).then(function (updatedProfile) { toastr.success('Account has been updated.', 'Success!'); }); }).$promise; }; $scope.passwordHintSave = function () { $scope.passwordHintPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) { authService.setUserProfile(profile).then(function (updatedProfile) { toastr.success('Account has been updated.', 'Success!'); }); }).$promise; }; $scope.optionsSave = function () { $localStorage.disableWebsiteIcons = cipherService.disableWebsiteIcons = $scope.model.disableWebsiteIcons; $rootScope.vaultCiphers = null; toastr.success('Options have been updated.', 'Success!'); }; $scope.changePassword = function () { $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsChangePassword.html', controller: 'settingsChangePasswordController' }); }; $scope.changeEmail = function () { $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsChangeEmail.html', controller: 'settingsChangeEmailController' }); }; $scope.viewOrganization = function (org) { if (org.type === 2) { // 2 = User scrollToTop(); toastr.error('You cannot manage this organization.'); return; } $state.go('backend.org.dashboard', { orgId: org.id }); }; $scope.leaveOrganization = function (org) { if (!confirm('Are you sure you want to leave this organization (' + org.name + ')?')) { return; } apiService.organizations.postLeave({ id: org.id }, {}, function (response) { authService.refreshAccessToken().then(function () { var index = $scope.model.organizations.indexOf(org); if (index > -1) { $scope.model.organizations.splice(index, 1); } toastr.success('You have left the organization.'); scrollToTop(); }); }, function (error) { toastr.error('Unable to leave this organization.'); scrollToTop(); }); }; $scope.sessions = function () { $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsSessions.html', controller: 'settingsSessionsController' }); }; $scope.delete = function () { $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsDelete.html', controller: 'settingsDeleteController' }); }; $scope.purge = function () { $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsPurge.html', controller: 'settingsPurgeController' }); }; function scrollToTop() { $('html, body').animate({ scrollTop: 0 }, 200); } }]); angular .module('bit.settings') .controller('settingsCreateOrganizationController', ["$scope", "$state", "apiService", "cryptoService", "toastr", "$analytics", "authService", "constants", "appSettings", "validationService", "stripe", function ($scope, $state, apiService, cryptoService, toastr, $analytics, authService, constants, appSettings, validationService /* jshint ignore:start */ , stripe /* jshint ignore:end */ ) { $scope.plans = constants.plans; $scope.storageGb = constants.storageGb; $scope.paymentMethod = 'card'; $scope.selfHosted = appSettings.selfHosted; $scope.model = { plan: 'free', additionalSeats: 0, interval: 'year', ownedBusiness: false, additionalStorageGb: null }; $scope.totalPrice = function () { if ($scope.model.interval === 'month') { return (($scope.model.additionalSeats || 0) * ($scope.plans[$scope.model.plan].monthlySeatPrice || 0)) + (($scope.model.additionalStorageGb || 0) * $scope.storageGb.monthlyPrice) + ($scope.plans[$scope.model.plan].monthlyBasePrice || 0); } else { return (($scope.model.additionalSeats || 0) * ($scope.plans[$scope.model.plan].annualSeatPrice || 0)) + (($scope.model.additionalStorageGb || 0) * $scope.storageGb.yearlyPrice) + ($scope.plans[$scope.model.plan].annualBasePrice || 0); } }; $scope.changePaymentMethod = function (val) { $scope.paymentMethod = val; }; $scope.changedPlan = function () { if ($scope.plans[$scope.model.plan].hasOwnProperty('monthPlanType')) { $scope.model.interval = 'year'; } if ($scope.plans[$scope.model.plan].noAdditionalSeats) { $scope.model.additionalSeats = 0; } else if (!$scope.model.additionalSeats && !$scope.plans[$scope.model.plan].baseSeats && !$scope.plans[$scope.model.plan].noAdditionalSeats) { $scope.model.additionalSeats = 1; } }; $scope.changedBusiness = function () { if ($scope.model.ownedBusiness) { $scope.model.plan = 'teams'; } }; $scope.submit = function (model, form) { var shareKey = cryptoService.makeShareKey(); var defaultCollectionCt = cryptoService.encrypt('Default Collection', shareKey.key); if ($scope.selfHosted) { var fileEl = document.getElementById('file'); var files = fileEl.files; if (!files || !files.length) { validationService.addError(form, 'file', 'Select a license file.', true); return; } var fd = new FormData(); fd.append('license', files[0]); fd.append('key', shareKey.ct); fd.append('collectionName', defaultCollectionCt); $scope.submitPromise = apiService.organizations.postLicense(fd).$promise.then(finalizeCreate); } else { if (model.plan === 'free') { var freeRequest = { name: model.name, planType: model.plan, key: shareKey.ct, billingEmail: model.billingEmail, collectionName: defaultCollectionCt }; $scope.submitPromise = apiService.organizations.post(freeRequest).$promise.then(finalizeCreate); } else { var stripeReq = null; if ($scope.paymentMethod === 'card') { stripeReq = stripe.card.createToken(model.card); } else if ($scope.paymentMethod === 'bank') { model.bank.currency = 'USD'; model.bank.country = 'US'; stripeReq = stripe.bankAccount.createToken(model.bank); } else { return; } $scope.submitPromise = stripeReq.then(function (response) { var paidRequest = { name: model.name, planType: model.interval === 'month' ? $scope.plans[model.plan].monthPlanType : $scope.plans[model.plan].annualPlanType, key: shareKey.ct, paymentToken: response.id, additionalSeats: model.additionalSeats, additionalStorageGb: model.additionalStorageGb, billingEmail: model.billingEmail, businessName: model.ownedBusiness ? model.businessName : null, country: $scope.paymentMethod === 'card' ? model.card.address_country : null, collectionName: defaultCollectionCt }; return apiService.organizations.post(paidRequest).$promise; }, function (err) { throw err.message; }).then(finalizeCreate); } } function finalizeCreate(result) { $analytics.eventTrack('Created Organization'); authService.addProfileOrganizationOwner(result, shareKey.ct); authService.refreshAccessToken().then(function () { goToOrg(result.Id); }, function () { goToOrg(result.Id); }); } function goToOrg(id) { $state.go('backend.org.dashboard', { orgId: id }).then(function () { toastr.success('Your new organization is ready to go!', 'Organization Created'); }); } }; }]); angular .module('bit.settings') .controller('settingsDeleteController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", "tokenService", function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics, tokenService) { $analytics.eventTrack('settingsDeleteController', { category: 'Modal' }); $scope.submit = function (model) { var profile; $scope.submitPromise = authService.getUserProfile().then(function (theProfile) { profile = theProfile; return cryptoService.hashPassword(model.masterPassword); }).then(function (hash) { return apiService.accounts.postDelete({ masterPasswordHash: hash }).$promise; }).then(function () { $uibModalInstance.dismiss('cancel'); authService.logOut(); tokenService.clearTwoFactorToken(profile.email); $analytics.eventTrack('Deleted Account'); return $state.go('frontend.login.info'); }).then(function () { toastr.success('Your account has been closed and all associated data has been deleted.', 'Account Deleted'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.settings') .controller('settingsDomainsController', ["$scope", "$state", "apiService", "toastr", "$analytics", "$uibModal", function ($scope, $state, apiService, toastr, $analytics, $uibModal) { $scope.globalEquivalentDomains = []; $scope.equivalentDomains = []; apiService.settings.getDomains({}, function (response) { var i; if (response.EquivalentDomains) { for (i = 0; i < response.EquivalentDomains.length; i++) { $scope.equivalentDomains.push(response.EquivalentDomains[i].join(', ')); } } if (response.GlobalEquivalentDomains) { for (i = 0; i < response.GlobalEquivalentDomains.length; i++) { $scope.globalEquivalentDomains.push({ domains: response.GlobalEquivalentDomains[i].Domains.join(', '), excluded: response.GlobalEquivalentDomains[i].Excluded, key: response.GlobalEquivalentDomains[i].Type }); } } }); $scope.toggleExclude = function (globalDomain) { globalDomain.excluded = !globalDomain.excluded; }; $scope.customize = function (globalDomain) { globalDomain.excluded = true; $scope.equivalentDomains.push(globalDomain.domains); }; $scope.delete = function (i) { $scope.equivalentDomains.splice(i, 1); $scope.$emit('removeAppendedDropdownMenu'); }; $scope.addEdit = function (i) { var addEditModal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsAddEditEquivalentDomain.html', controller: 'settingsAddEditEquivalentDomainController', resolve: { domainIndex: function () { return i; }, domains: function () { return i !== null ? $scope.equivalentDomains[i] : null; } } }); addEditModal.result.then(function (returnObj) { if (returnObj.domains) { returnObj.domains = returnObj.domains.split(' ').join('').split(',').join(', '); } if (returnObj.index !== null) { $scope.equivalentDomains[returnObj.index] = returnObj.domains; } else { $scope.equivalentDomains.push(returnObj.domains); } }); }; $scope.saveGlobal = function () { $scope.globalPromise = save(); }; $scope.saveCustom = function () { $scope.customPromise = save(); }; var save = function () { var request = { ExcludedGlobalEquivalentDomains: [], EquivalentDomains: [] }; for (var i = 0; i < $scope.globalEquivalentDomains.length; i++) { if ($scope.globalEquivalentDomains[i].excluded) { request.ExcludedGlobalEquivalentDomains.push($scope.globalEquivalentDomains[i].key); } } for (i = 0; i < $scope.equivalentDomains.length; i++) { request.EquivalentDomains.push($scope.equivalentDomains[i].split(' ').join('').split(',')); } if (!request.EquivalentDomains.length) { request.EquivalentDomains = null; } if (!request.ExcludedGlobalEquivalentDomains.length) { request.ExcludedGlobalEquivalentDomains = null; } return apiService.settings.putDomains(request, function (domains) { $analytics.eventTrack('Saved Equivalent Domains'); toastr.success('Domains have been updated.', 'Success!'); }).$promise; }; }]); angular .module('bit.settings') .controller('settingsPremiumController', ["$scope", "$state", "apiService", "toastr", "$analytics", "authService", "constants", "$timeout", "appSettings", "validationService", "stripe", function ($scope, $state, apiService, toastr, $analytics, authService, constants, $timeout, appSettings, validationService /* jshint ignore:start */ , stripe /* jshint ignore:end */ ) { var profile = null; authService.getUserProfile().then(function (theProfile) { profile = theProfile; if (profile && profile.premium) { return $state.go('backend.user.settingsBilling'); } }); $scope.selfHosted = appSettings.selfHosted; var btInstance = null; $scope.storageGbPrice = constants.storageGb.yearlyPrice; $scope.premiumPrice = constants.premium.price; $scope.paymentMethod = 'card'; $scope.dropinLoaded = false; $scope.model = { additionalStorageGb: null }; $scope.changePaymentMethod = function (val) { $scope.paymentMethod = val; if ($scope.paymentMethod !== 'paypal') { return; } braintree.dropin.create({ authorization: appSettings.braintreeKey, container: '#bt-dropin-container', paymentOptionPriority: ['paypal'], paypal: { flow: 'vault', buttonStyle: { label: 'pay', size: 'medium', shape: 'pill', color: 'blue' } } }, function (createErr, instance) { if (createErr) { console.error(createErr); return; } btInstance = instance; $timeout(function () { $scope.dropinLoaded = true; }); }); }; $scope.totalPrice = function () { return $scope.premiumPrice + (($scope.model.additionalStorageGb || 0) * $scope.storageGbPrice); }; $scope.submit = function (model, form) { if ($scope.selfHosted) { if (profile && !profile.emailVerified) { validationService.addError(form, null, 'Your account\'s email address first must be verified.', true); return; } var fileEl = document.getElementById('file'); var files = fileEl.files; if (!files || !files.length) { validationService.addError(form, 'file', 'Select a license file.', true); return; } var fd = new FormData(); fd.append('license', files[0]); $scope.submitPromise = apiService.accounts.postPremium(fd).$promise.then(function (result) { return finalizePremium(); }); } else { $scope.submitPromise = getPaymentToken(model).then(function (token) { if (!token) { throw 'No payment token.'; } var fd = new FormData(); fd.append('paymentToken', token); fd.append('additionalStorageGb', model.additionalStorageGb || 0); return apiService.accounts.postPremium(fd).$promise; }, function (err) { throw err; }).then(function (result) { return finalizePremium(); }); } }; function finalizePremium() { return authService.updateProfilePremium(true).then(function () { $analytics.eventTrack('Signed Up Premium'); return authService.refreshAccessToken(); }).then(function () { return $state.go('backend.user.settingsBilling'); }).then(function () { toastr.success('Premium upgrade complete.', 'Success'); }); } function getPaymentToken(model) { if ($scope.paymentMethod === 'paypal') { return btInstance.requestPaymentMethod().then(function (payload) { return payload.nonce; }).catch(function (err) { throw err.message; }); } else { return stripe.card.createToken(model.card).then(function (response) { return response.id; }).catch(function (err) { throw err.message; }); } } }]); angular .module('bit.settings') .controller('settingsPurgeController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", "tokenService", function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics, tokenService) { $analytics.eventTrack('settingsPurgeController', { category: 'Modal' }); $scope.submit = function (model) { $scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { return apiService.ciphers.purge({ masterPasswordHash: hash }).$promise; }).then(function () { $uibModalInstance.dismiss('cancel'); $analytics.eventTrack('Purged Vault'); return $state.go('backend.user.vault', { refreshFromServer: true }); }).then(function () { toastr.success('All items in your vault have been deleted.', 'Vault Purged'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.settings') .controller('settingsSessionsController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "authService", "tokenService", "toastr", "$analytics", function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, tokenService, toastr, $analytics) { $analytics.eventTrack('settingsSessionsController', { category: 'Modal' }); $scope.submit = function (model) { var hash, profile; $scope.submitPromise = cryptoService.hashPassword(model.masterPassword).then(function (theHash) { hash = theHash; return authService.getUserProfile(); }).then(function (theProfile) { profile = theProfile; return apiService.accounts.putSecurityStamp({ masterPasswordHash: hash }).$promise; }).then(function () { $uibModalInstance.dismiss('cancel'); authService.logOut(); tokenService.clearTwoFactorToken(profile.email); $analytics.eventTrack('Deauthorized Sessions'); return $state.go('frontend.login.info'); }).then(function () { toastr.success('Please log back in.', 'All Sessions Deauthorized'); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.settings') .controller('settingsTwoStepAuthenticatorController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "authService", "$q", "toastr", "$analytics", "constants", "$timeout", function ($scope, apiService, $uibModalInstance, cryptoService, authService, $q, toastr, $analytics, constants, $timeout) { $analytics.eventTrack('settingsTwoStepAuthenticatorController', { category: 'Modal' }); var _issuer = 'Bitwarden', _profile = null, _masterPasswordHash, _key = null; $timeout(function () { $("#masterPassword").focus(); }); $scope.auth = function (model) { var response = null; $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; return apiService.twoFactor.getAuthenticator({}, { masterPasswordHash: _masterPasswordHash }).$promise; }).then(function (apiResponse) { response = apiResponse; return authService.getUserProfile(); }).then(function (profile) { _profile = profile; $scope.account = _profile.email; processResponse(response); }); }; function formatString(s) { if (!s) { return null; } return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase(); } function processResponse(response) { $scope.enabled = response.Enabled; _key = response.Key; $scope.model = { key: formatString(_key), qr: 'https://chart.googleapis.com/chart?chs=160x160&chld=L|0&cht=qr&chl=otpauth://totp/' + _issuer + ':' + encodeURIComponent(_profile.email) + '%3Fsecret=' + encodeURIComponent(_key) + '%26issuer=' + _issuer }; $scope.updateModel = { token: null }; } $scope.submit = function (model) { if (!model || !model.token) { disable(); return; } update(model); }; function disable() { if (!confirm('Are you sure you want to disable the authenticator app provider?')) { return; } $scope.submitPromise = apiService.twoFactor.disable({}, { masterPasswordHash: _masterPasswordHash, type: constants.twoFactorProvider.authenticator }, function (response) { $analytics.eventTrack('Disabled Two-step Authenticator'); toastr.success('Authenticator app has been disabled.'); $scope.enabled = response.Enabled; $scope.close(); }).$promise; } function update(model) { $scope.submitPromise = apiService.twoFactor.putAuthenticator({}, { token: model.token.replace(' ', ''), key: _key, masterPasswordHash: _masterPasswordHash }, function (response) { $analytics.eventTrack('Enabled Two-step Authenticator'); processResponse(response); model.token = null; }).$promise; } var closing = false; $scope.close = function () { closing = true; $uibModalInstance.close($scope.enabled); }; $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; } e.preventDefault(); $scope.close(); }); }]); angular .module('bit.settings') .controller('settingsTwoStepController', ["$scope", "apiService", "toastr", "$analytics", "constants", "$filter", "$uibModal", "authService", function ($scope, apiService, toastr, $analytics, constants, $filter, $uibModal, authService) { $scope.providers = $filter('filter')(constants.twoFactorProviderInfo, { organization: false }); $scope.premium = true; authService.getUserProfile().then(function (profile) { $scope.premium = profile.premium; return apiService.twoFactor.list({}).$promise; }).then(function (response) { if (response.Data) { for (var i = 0; i < response.Data.length; i++) { if (!response.Data[i].Enabled) { continue; } var provider = $filter('filter')($scope.providers, { type: response.Data[i].Type }); if (provider.length) { provider[0].enabled = true; } } } return; }); $scope.edit = function (provider) { if (!$scope.premium && !provider.free) { $uibModal.open({ animation: true, templateUrl: 'app/views/premiumRequired.html', controller: 'premiumRequiredController' }); return; } if (provider.type === constants.twoFactorProvider.authenticator) { typeName = 'Authenticator'; } else if (provider.type === constants.twoFactorProvider.email) { typeName = 'Email'; } else if (provider.type === constants.twoFactorProvider.yubikey) { typeName = 'Yubi'; } else if (provider.type === constants.twoFactorProvider.duo) { typeName = 'Duo'; } else if (provider.type === constants.twoFactorProvider.u2f) { typeName = 'U2f'; } else { return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html', controller: 'settingsTwoStep' + typeName + 'Controller', resolve: { enabled: function () { return provider.enabled; }, orgId: function () { return null; } } }); modal.result.then(function (enabled) { if (enabled || enabled === false) { // do not adjust when undefined or null provider.enabled = enabled; } }); }; $scope.viewRecover = function () { var modal = $uibModal.open({ animation: true, templateUrl: 'app/settings/views/settingsTwoStepRecover.html', controller: 'settingsTwoStepRecoverController' }); }; }]); angular .module('bit.settings') .controller('settingsTwoStepDuoController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "toastr", "$analytics", "constants", "$timeout", "orgId", function ($scope, apiService, $uibModalInstance, cryptoService, toastr, $analytics, constants, $timeout, orgId) { $analytics.eventTrack('settingsTwoStepDuoController', { category: 'Modal' }); var _masterPasswordHash; $scope.updateModel = { token: null, host: null, ikey: null, skey: null }; $timeout(function () { $("#masterPassword").focus(); }); $scope.auth = function (model) { $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; var requestModel = { masterPasswordHash: _masterPasswordHash }; if (orgId) { return apiService.twoFactor.getOrganizationDuo({ orgId: orgId }, requestModel).$promise; } else { return apiService.twoFactor.getDuo({}, requestModel).$promise; } }).then(function (apiResponse) { processResult(apiResponse); $scope.authed = true; }); }; $scope.submit = function (model) { if ($scope.enabled) { disable(); return; } update(model); }; function disable() { if (!confirm('Are you sure you want to disable the Duo provider?')) { return; } if (orgId) { $scope.submitPromise = apiService.twoFactor.disableOrganization({ orgId: orgId }, { masterPasswordHash: _masterPasswordHash, type: constants.twoFactorProvider.organizationDuo }, function (response) { $analytics.eventTrack('Disabled Two-step Organization Duo'); toastr.success('Duo has been disabled.'); $scope.enabled = response.Enabled; $scope.close(); }).$promise; } else { $scope.submitPromise = apiService.twoFactor.disable({}, { masterPasswordHash: _masterPasswordHash, type: constants.twoFactorProvider.duo }, function (response) { $analytics.eventTrack('Disabled Two-step Duo'); toastr.success('Duo has been disabled.'); $scope.enabled = response.Enabled; $scope.close(); }).$promise; } } function update(model) { var requestModel = { integrationKey: model.ikey, secretKey: model.skey, host: model.host, masterPasswordHash: _masterPasswordHash }; if (orgId) { $scope.submitPromise = apiService.twoFactor.putOrganizationDuo({ orgId: orgId }, requestModel, function (response) { $analytics.eventTrack('Enabled Two-step Organization Duo'); processResult(response); }).$promise; } else { $scope.submitPromise = apiService.twoFactor.putDuo({}, requestModel, function (response) { $analytics.eventTrack('Enabled Two-step Duo'); processResult(response); }).$promise; } } function processResult(response) { $scope.enabled = response.Enabled; $scope.updateModel = { ikey: response.IntegrationKey, skey: response.SecretKey, host: response.Host }; } var closing = false; $scope.close = function () { closing = true; $uibModalInstance.close($scope.enabled); }; $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; } e.preventDefault(); $scope.close(); }); }]); angular .module('bit.settings') .controller('settingsTwoStepEmailController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", "constants", "$timeout", function ($scope, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics, constants, $timeout) { $analytics.eventTrack('settingsTwoStepEmailController', { category: 'Modal' }); var _profile = null, _masterPasswordHash; $scope.updateModel = { token: null, email: null }; $timeout(function () { $("#masterPassword").focus(); }); $scope.auth = function (model) { var response = null; $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; return apiService.twoFactor.getEmail({}, { masterPasswordHash: _masterPasswordHash }).$promise; }).then(function (apiResponse) { response = apiResponse; return authService.getUserProfile(); }).then(function (profile) { _profile = profile; $scope.enabled = response.Enabled; $scope.updateModel.email = $scope.enabled ? response.Email : _profile.email; $scope.authed = true; }); }; $scope.sendEmail = function (model) { $scope.emailError = false; $scope.emailSuccess = false; if (!model || !model.email || model.email.indexOf('@') < 0) { $scope.emailError = true; $scope.emailSuccess = false; return; } $scope.emailLoading = true; apiService.twoFactor.sendEmail({}, { masterPasswordHash: _masterPasswordHash, email: model.email }, function (response) { $scope.emailError = false; $scope.emailSuccess = true; $scope.emailLoading = false; }, function (response) { $scope.emailError = true; $scope.emailSuccess = false; $scope.emailLoading = false; }); }; $scope.submit = function (model) { if (!model || !model.token) { disable(); return; } update(model); }; function disable() { if (!confirm('Are you sure you want to disable the email provider?')) { return; } $scope.submitPromise = apiService.twoFactor.disable({}, { masterPasswordHash: _masterPasswordHash, type: constants.twoFactorProvider.email }, function (response) { $analytics.eventTrack('Disabled Two-step Email'); toastr.success('Email has been disabled.'); $scope.enabled = response.Enabled; $scope.close(); }).$promise; } function update(model) { $scope.submitPromise = apiService.twoFactor.putEmail({}, { email: model.email.toLowerCase().trim(), token: model.token.replace(' ', ''), masterPasswordHash: _masterPasswordHash }, function (response) { $analytics.eventTrack('Enabled Two-step Email'); $scope.enabled = response.Enabled; model.email = response.Email; model.token = null; }).$promise; } var closing = false; $scope.close = function () { closing = true; $uibModalInstance.close($scope.enabled); }; $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; } e.preventDefault(); $scope.close(); }); }]); angular .module('bit.settings') .controller('settingsTwoStepRecoverController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "$analytics", "$timeout", function ($scope, apiService, $uibModalInstance, cryptoService, $analytics, $timeout) { $analytics.eventTrack('settingsTwoStepRecoverController', { category: 'Modal' }); $scope.code = null; $scope.auth = function (model) { $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { return apiService.twoFactor.getRecover({}, { masterPasswordHash: hash }).$promise; }).then(function (apiResponse) { $scope.code = formatString(apiResponse.Code); $scope.authed = true; }); }; $timeout(function () { $("#masterPassword").focus(); }); $scope.print = function () { if (!$scope.code) { return; } $analytics.eventTrack('Print Recovery Code'); var w = window.open(); w.document.write('

Bitwarden two-step login recovery code:

' + '' + $scope.code + '' + '

' + new Date() + '

'); w.print(); w.close(); }; function formatString(s) { if (!s) { return null; } return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase(); } $scope.close = function () { $uibModalInstance.close(); }; }]); angular .module('bit.settings') .controller('settingsTwoStepU2fController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", "constants", "$timeout", "$window", function ($scope, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics, constants, $timeout, $window) { $analytics.eventTrack('settingsTwoStepU2fController', { category: 'Modal' }); var _masterPasswordHash; var closed = false; $scope.deviceResponse = null; $scope.deviceListening = false; $scope.deviceError = false; $timeout(function () { $("#masterPassword").focus(); }); $scope.auth = function (model) { $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; return apiService.twoFactor.getU2f({}, { masterPasswordHash: _masterPasswordHash }).$promise; }).then(function (response) { $scope.enabled = response.Enabled; $scope.challenge = response.Challenge; $scope.authed = true; return $scope.readDevice(); }); }; $scope.readDevice = function () { if (closed || $scope.enabled) { return; } console.log('listening for key...'); $scope.deviceResponse = null; $scope.deviceError = false; $scope.deviceListening = true; $window.u2f.register($scope.challenge.AppId, [{ version: $scope.challenge.Version, challenge: $scope.challenge.Challenge }], [], function (data) { $scope.deviceListening = false; if (data.errorCode === 5) { $scope.readDevice(); return; } else if (data.errorCode) { $timeout(function () { $scope.deviceError = true; }); console.log('error: ' + data.errorCode); return; } $timeout(function () { $scope.deviceResponse = JSON.stringify(data); }); }, 10); }; $scope.submit = function () { if ($scope.enabled) { disable(); return; } update(); }; function disable() { if (!confirm('Are you sure you want to disable the U2F provider?')) { return; } $scope.submitPromise = apiService.twoFactor.disable({}, { masterPasswordHash: _masterPasswordHash, type: constants.twoFactorProvider.u2f }, function (response) { $analytics.eventTrack('Disabled Two-step U2F'); toastr.success('U2F has been disabled.'); $scope.enabled = response.Enabled; $scope.close(); }).$promise; } function update() { $scope.submitPromise = apiService.twoFactor.putU2f({}, { deviceResponse: $scope.deviceResponse, masterPasswordHash: _masterPasswordHash }, function (response) { $analytics.eventTrack('Enabled Two-step U2F'); $scope.enabled = response.Enabled; $scope.challenge = null; $scope.deviceResponse = null; $scope.deviceError = false; }).$promise; } $scope.close = function () { closed = true; $uibModalInstance.close($scope.enabled); }; $scope.$on('modal.closing', function (e, reason, isClosed) { if (closed) { return; } e.preventDefault(); $scope.close(); }); }]); angular .module('bit.settings') .controller('settingsTwoStepYubiController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "authService", "toastr", "$analytics", "constants", "$timeout", function ($scope, apiService, $uibModalInstance, cryptoService, authService, toastr, $analytics, constants, $timeout) { $analytics.eventTrack('settingsTwoStepYubiController', { category: 'Modal' }); var _profile = null, _masterPasswordHash; $timeout(function () { $("#masterPassword").focus(); }); $scope.auth = function (model) { var response = null; $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; return apiService.twoFactor.getYubi({}, { masterPasswordHash: _masterPasswordHash }).$promise; }).then(function (apiResponse) { response = apiResponse; return authService.getUserProfile(); }).then(function (profile) { _profile = profile; processResult(response); $scope.authed = true; }); }; $scope.remove = function (model) { model.key = null; model.existingKey = null; }; $scope.submit = function (model) { $scope.submitPromise = apiService.twoFactor.putYubi({}, { key1: model.key1.key, key2: model.key2.key, key3: model.key3.key, nfc: model.nfc, masterPasswordHash: _masterPasswordHash }, function (response) { $analytics.eventTrack('Saved Two-step YubiKey'); toastr.success('YubiKey saved.'); processResult(response); }).$promise; }; $scope.disable = function () { if (!confirm('Are you sure you want to disable the YubiKey provider?')) { return; } $scope.disableLoading = true; $scope.submitPromise = apiService.twoFactor.disable({}, { masterPasswordHash: _masterPasswordHash, type: constants.twoFactorProvider.yubikey }, function (response) { $scope.disableLoading = false; $analytics.eventTrack('Disabled Two-step YubiKey'); toastr.success('YubiKey has been disabled.'); $scope.enabled = response.Enabled; $scope.close(); }, function (response) { toastr.error('Failed to disable.'); $scope.disableLoading = false; }).$promise; }; function processResult(response) { $scope.enabled = response.Enabled; $scope.updateModel = { key1: { key: response.Key1, existingKey: padRight(response.Key1, '*', 44) }, key2: { key: response.Key2, existingKey: padRight(response.Key2, '*', 44) }, key3: { key: response.Key3, existingKey: padRight(response.Key3, '*', 44) }, nfc: response.Nfc === true || !response.Enabled }; } function padRight(str, character, size) { if (!str || !character || str.length >= size) { return str; } var max = (size - str.length) / character.length; for (var i = 0; i < max; i++) { str += character; } return str; } var closing = false; $scope.close = function () { closing = true; $uibModalInstance.close($scope.enabled); }; $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; } e.preventDefault(); $scope.close(); }); }]); angular .module('bit.settings') .controller('settingsUpdateKeyController', ["$scope", "$state", "apiService", "$uibModalInstance", "cipherService", "cryptoService", "authService", "validationService", "toastr", "$analytics", "$q", function ($scope, $state, apiService, $uibModalInstance, cipherService, cryptoService, authService, validationService, toastr, $analytics, $q) { $analytics.eventTrack('settingsUpdateKeyController', { category: 'Modal' }); $scope.save = function (form) { var encKey = cryptoService.getEncKey(); if (encKey) { validationService.addError(form, 'MasterPasswordHash', 'You do not need to update. You are already using the new encryption key.', true); return; } $scope.savePromise = cryptoService.hashPassword($scope.masterPassword).then(function (hash) { return updateKey(hash); }).then(function () { $uibModalInstance.dismiss('cancel'); authService.logOut(); $analytics.eventTrack('Key Updated'); return $state.go('frontend.login.info'); }, function (e) { throw e ? e : 'Error occurred.'; }).then(function () { toastr.success('Please log back in. If you are using other Bitwarden applications, ' + 'log out and back in to those as well.', 'Key Updated', { timeOut: 10000 }); }); }; function updateKey(masterPasswordHash) { var madeEncKey = cryptoService.makeEncKey(null); var reencryptedCiphers = []; var ciphersPromise = apiService.ciphers.list({}, function (encryptedCiphers) { var filteredEncryptedCiphers = []; for (var i = 0; i < encryptedCiphers.Data.length; i++) { if (encryptedCiphers.Data[i].OrganizationId) { continue; } filteredEncryptedCiphers.push(encryptedCiphers.Data[i]); } var unencryptedCiphers = cipherService.decryptCiphers(filteredEncryptedCiphers); reencryptedCiphers = cipherService.encryptCiphers(unencryptedCiphers, madeEncKey.encKey); }).$promise; var reencryptedFolders = []; var foldersPromise = apiService.folders.list({}, function (encryptedFolders) { var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data); reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, madeEncKey.encKey); }).$promise; var privateKey = cryptoService.getPrivateKey('raw'), reencryptedPrivateKey = null; if (privateKey) { reencryptedPrivateKey = cryptoService.encrypt(privateKey, madeEncKey.encKey, 'raw'); } return $q.all([ciphersPromise, foldersPromise]).then(function () { var request = { masterPasswordHash: masterPasswordHash, ciphers: reencryptedCiphers, folders: reencryptedFolders, privateKey: reencryptedPrivateKey, key: madeEncKey.encKeyEnc }; return apiService.accounts.putKey(request).$promise; }, function () { throw 'Error while encrypting data.'; }).then(function () { cryptoService.setEncKey(madeEncKey.encKey, null, true); }); } $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.vault') .controller('vaultAddCipherController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "passwordService", "selectedFolder", "$analytics", "checkedFavorite", "$rootScope", "authService", "$uibModal", "constants", "$filter", "selectedType", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal, constants, $filter, selectedType) { $analytics.eventTrack('vaultAddCipherController', { category: 'Modal' }); $scope.folders = $rootScope.vaultFolders; $scope.constants = constants; $scope.selectedType = selectedType ? selectedType.toString() : constants.cipherType.login.toString(); $scope.cipher = { folderId: selectedFolder ? selectedFolder.id : null, favorite: checkedFavorite === true, type: selectedType || constants.cipherType.login, login: { uris: [{ uri: null, match: null, matchValue: null }] }, identity: {}, card: {}, secureNote: { type: 0 } }; authService.getUserProfile().then(function (profile) { $scope.useTotp = profile.premium; }); $scope.typeChanged = function () { $scope.cipher.type = parseInt($scope.selectedType); }; $scope.savePromise = null; $scope.save = function () { if ($scope.cipher.type === constants.cipherType.login && $scope.cipher.login.uris.length === 1 && ($scope.cipher.login.uris[0].uri == null || $scope.cipher.login.uris[0].uri === '')) { $scope.cipher.login.uris = null; } var cipher = cipherService.encryptCipher($scope.cipher); $scope.savePromise = apiService.ciphers.post(cipher, function (cipherResponse) { $analytics.eventTrack('Created Cipher'); var decCipher = cipherService.decryptCipherPreview(cipherResponse); $uibModalInstance.close(decCipher); }).$promise; }; $scope.generatePassword = function () { if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) { $analytics.eventTrack('Generated Password From Add'); $scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true }); } }; $scope.addUri = function () { if (!$scope.cipher.login) { return; } if (!$scope.cipher.login.uris) { $scope.cipher.login.uris = []; } $scope.cipher.login.uris.push({ uri: null, match: null, matchValue: null }); }; $scope.removeUri = function (uri) { if (!$scope.cipher.login || !$scope.cipher.login.uris) { return; } var index = $scope.cipher.login.uris.indexOf(uri); if (index > -1) { $scope.cipher.login.uris.splice(index, 1); } }; $scope.uriMatchChanged = function (uri) { if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') { uri.match = null; } else { uri.match = parseInt(uri.matchValue); } }; $scope.addField = function () { if (!$scope.cipher.fields) { $scope.cipher.fields = []; } $scope.cipher.fields.push({ type: constants.fieldType.text.toString(), name: null, value: null }); }; $scope.removeField = function (field) { var index = $scope.cipher.fields.indexOf(field); if (index > -1) { $scope.cipher.fields.splice(index, 1); } }; $scope.toggleFavorite = function () { $scope.cipher.favorite = !$scope.cipher.favorite; }; $scope.clipboardSuccess = function (e) { e.clearSelection(); selectPassword(e); }; $scope.clipboardError = function (e, password) { if (password) { selectPassword(e); } alert('Your web browser does not support easy clipboard copying. Copy it manually instead.'); }; $scope.folderSort = function (item) { if (!item.id) { return ''; } return item.name.toLowerCase(); }; function selectPassword(e) { var target = $(e.trigger).parent().prev(); if (target.attr('type') === 'text') { target.select(); } } $scope.close = function () { $uibModalInstance.dismiss('close'); }; $scope.showUpgrade = function () { $uibModal.open({ animation: true, templateUrl: 'app/views/premiumRequired.html', controller: 'premiumRequiredController' }); }; }]); angular .module('bit.vault') .controller('vaultAddFolderController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "$analytics", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, $analytics) { $analytics.eventTrack('vaultAddFolderController', { category: 'Modal' }); $scope.savePromise = null; $scope.save = function (model) { var folder = cipherService.encryptFolder(model); $scope.savePromise = apiService.folders.post(folder, function (response) { $analytics.eventTrack('Created Folder'); var decFolder = cipherService.decryptFolder(response); $uibModalInstance.close(decFolder); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('close'); }; }]); angular .module('bit.vault') .controller('vaultAttachmentsController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "cipherId", "$analytics", "validationService", "toastr", "$timeout", "authService", "$uibModal", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, cipherId, $analytics, validationService, toastr, $timeout, authService, $uibModal) { $analytics.eventTrack('vaultAttachmentsController', { category: 'Modal' }); $scope.cipher = {}; $scope.readOnly = true; $scope.loading = true; $scope.isPremium = true; $scope.canUseAttachments = true; var closing = false; authService.getUserProfile().then(function (profile) { $scope.isPremium = profile.premium; return apiService.ciphers.get({ id: cipherId }).$promise; }).then(function (cipher) { $scope.cipher = cipherService.decryptCipher(cipher); $scope.readOnly = !$scope.cipher.edit; $scope.canUseAttachments = $scope.isPremium || $scope.cipher.organizationId; $scope.loading = false; }, function () { $scope.loading = false; }); $scope.save = function (form) { var fileEl = document.getElementById('file'); var files = fileEl.files; if (!files || !files.length) { validationService.addError(form, 'file', 'Select a file.', true); return; } $scope.savePromise = cipherService.encryptAttachmentFile(getKeyForCipher(), files[0]).then(function (encValue) { var fd = new FormData(); var blob = new Blob([encValue.data], { type: 'application/octet-stream' }); fd.append('data', blob, encValue.fileName); return apiService.ciphers.postAttachment({ id: cipherId }, fd).$promise; }).then(function (response) { $analytics.eventTrack('Added Attachment'); $scope.cipher = cipherService.decryptCipher(response); // reset file input // ref: https://stackoverflow.com/a/20552042 fileEl.type = ''; fileEl.type = 'file'; fileEl.value = ''; }, function (e) { var errors = validationService.parseErrors(e); toastr.error(errors.length ? errors[0] : 'An error occurred.'); }); }; $scope.download = function (attachment) { attachment.loading = true; if (!$scope.canUseAttachments) { attachment.loading = false; alert('Premium membership is required to use this feature.'); return; } cipherService.downloadAndDecryptAttachment(getKeyForCipher(), attachment, true).then(function (res) { $timeout(function () { attachment.loading = false; }); }, function () { $timeout(function () { attachment.loading = false; }); }); }; function getKeyForCipher() { if ($scope.cipher.organizationId) { return cryptoService.getOrgKey($scope.cipher.organizationId); } return null; } $scope.remove = function (attachment) { if (!confirm('Are you sure you want to delete this attachment (' + attachment.fileName + ')?')) { return; } attachment.loading = true; apiService.ciphers.delAttachment({ id: cipherId, attachmentId: attachment.id }).$promise.then(function () { attachment.loading = false; $analytics.eventTrack('Deleted Attachment'); var index = $scope.cipher.attachments.indexOf(attachment); if (index > -1) { $scope.cipher.attachments.splice(index, 1); } }, function () { toastr.error('Cannot delete attachment.'); attachment.loading = false; }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return; } e.preventDefault(); closing = true; $uibModalInstance.close(!!$scope.cipher.attachments && $scope.cipher.attachments.length > 0); }); $scope.showUpgrade = function () { $uibModal.open({ animation: true, templateUrl: 'app/views/premiumRequired.html', controller: 'premiumRequiredController' }); }; }]); angular .module('bit.vault') .controller('vaultCipherCollectionsController', ["$scope", "apiService", "$uibModalInstance", "cipherService", "cipherId", "$analytics", function ($scope, apiService, $uibModalInstance, cipherService, cipherId, $analytics) { $analytics.eventTrack('vaultCipherCollectionsController', { category: 'Modal' }); $scope.cipher = {}; $scope.readOnly = false; $scope.loadingCipher = true; $scope.loadingCollections = true; $scope.selectedCollections = {}; $scope.collections = []; var cipherAndCols = null; $uibModalInstance.opened.then(function () { apiService.ciphers.getDetails({ id: cipherId }).$promise.then(function (cipher) { $scope.loadingCipher = false; $scope.readOnly = !cipher.Edit; if (cipher.Edit && cipher.OrganizationId) { if (cipher.Type === 1) { $scope.cipher = cipherService.decryptCipherPreview(cipher); } var collections = {}; if (cipher.CollectionIds) { for (var i = 0; i < cipher.CollectionIds.length; i++) { collections[cipher.CollectionIds[i]] = null; } } return { cipher: cipher, cipherCollections: collections }; } return null; }).then(function (result) { if (!result) { $scope.loadingCollections = false; return false; } cipherAndCols = result; return apiService.collections.listMe({ writeOnly: true }).$promise; }).then(function (response) { if (response === false) { return; } var collections = []; var selectedCollections = {}; var writeableCollections = response.Data; for (var i = 0; i < writeableCollections.length; i++) { // clean out selectCollections that aren't from this organization if (writeableCollections[i].OrganizationId !== cipherAndCols.cipher.OrganizationId) { continue; } if (writeableCollections[i].Id in cipherAndCols.cipherCollections) { selectedCollections[writeableCollections[i].Id] = true; } var decCollection = cipherService.decryptCollection(writeableCollections[i]); collections.push(decCollection); } $scope.loadingCollections = false; $scope.collections = collections; $scope.selectedCollections = selectedCollections; }); }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { collections[$scope.collections[i].id] = true; } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = true; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { return Object.keys($scope.selectedCollections).length === $scope.collections.length; }; $scope.submit = function () { var request = { collectionIds: [] }; for (var id in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(id)) { request.collectionIds.push(id); } } $scope.submitPromise = apiService.ciphers.putCollections({ id: cipherId }, request) .$promise.then(function (response) { $analytics.eventTrack('Edited Cipher Collections'); $uibModalInstance.close({ action: 'collectionsEdit', collectionIds: request.collectionIds }); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.vault') .controller('vaultController', ["$scope", "$uibModal", "apiService", "$filter", "cryptoService", "authService", "toastr", "cipherService", "$q", "$localStorage", "$timeout", "$rootScope", "$state", "$analytics", "constants", "validationService", function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr, cipherService, $q, $localStorage, $timeout, $rootScope, $state, $analytics, constants, validationService) { $scope.loadingCiphers = true; $scope.loadingGroupings = true; $scope.ciphers = []; $scope.folders = []; $scope.collections = []; $scope.constants = constants; $scope.filter = undefined; $scope.selectedType = undefined; $scope.selectedFolder = undefined; $scope.selectedCollection = undefined; $scope.selectedFavorites = false; $scope.selectedAll = true; $scope.selectedTitle = 'All'; $scope.selectedIcon = 'fa-th'; if ($state.params.refreshFromServer) { $rootScope.vaultFolders = $rootScope.vaultCollections = $rootScope.vaultCiphers = null; } $scope.$on('$viewContentLoaded', function () { $timeout(function () { if ($('body').hasClass('control-sidebar-open')) { $("#search").focus(); } }, 500); if (($rootScope.vaultFolders || $rootScope.vaultCollections) && $rootScope.vaultCiphers) { $scope.loadingCiphers = $scope.loadingGroupings = false; loadCipherData($rootScope.vaultCiphers); return; } loadDataFromServer(); }); function loadDataFromServer() { var decFolders = [{ id: null, name: 'No Folder' }]; var decCollections = []; var collectionPromise = apiService.collections.listMe({ writeOnly: false }, function (collections) { for (var i = 0; i < collections.Data.length; i++) { var decCollection = cipherService.decryptCollection(collections.Data[i], null, true); decCollections.push(decCollection); } }).$promise; var folderPromise = apiService.folders.list({}, function (folders) { for (var i = 0; i < folders.Data.length; i++) { var decFolder = cipherService.decryptFolderPreview(folders.Data[i]); decFolders.push(decFolder); } }).$promise; var groupingPromise = $q.all([collectionPromise, folderPromise]).then(function () { $rootScope.vaultCollections = decCollections; $rootScope.vaultFolders = decFolders; $scope.loadingGroupings = false; }); apiService.ciphers.list({}, function (ciphers) { var decCiphers = []; for (var i = 0; i < ciphers.Data.length; i++) { var decCipher = cipherService.decryptCipherPreview(ciphers.Data[i]); decCiphers.push(decCipher); } groupingPromise.then(function () { loadCipherData(decCiphers); }); }); } function loadCipherData(decCiphers) { $rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')(decCiphers, ['sort', 'name', 'subTitle']); var chunks = chunk($rootScope.vaultCiphers, 200); if (chunks.length > 0) { $scope.ciphers = chunks[0]; var delay = 200; angular.forEach(chunks, function (value, index) { delay += 200; // skip the first chuck if (index > 0) { $timeout(function () { Array.prototype.push.apply($scope.ciphers, value); }, delay); } }); } $scope.loadingCiphers = false; } function sortScopedCipherData() { $rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')($rootScope.vaultCiphers, ['name', 'subTitle']); } function chunk(arr, len) { var chunks = [], i = 0, n = arr.length; while (i < n) { chunks.push(arr.slice(i, i += len)); } return chunks; } $scope.groupingSort = function (item) { if (!item.id) { return ''; } return item.name.toLowerCase(); }; $scope.clipboardError = function (e) { alert('Your web browser does not support easy clipboard copying. ' + 'Edit the item and copy it manually instead.'); }; $scope.editCipher = function (cipher) { var editModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultEditCipher.html', controller: 'vaultEditCipherController', resolve: { cipherId: function () { return cipher.id; } } }); editModel.result.then(function (returnVal) { if (returnVal.action === 'edit') { var index = $scope.ciphers.indexOf(cipher); if (index > -1) { // restore collection ids since those cannot change on edit here. returnVal.data.collectionIds = $rootScope.vaultCiphers[index].collectionIds; $rootScope.vaultCiphers[index] = returnVal.data; } sortScopedCipherData(); } else if (returnVal.action === 'partialEdit') { cipher.folderId = returnVal.data.folderId; cipher.favorite = returnVal.data.favorite; } else if (returnVal.action === 'delete') { removeCipherFromScopes(cipher); } }); }; $scope.$on('vaultAddCipher', function (event, args) { $scope.addCipher(); }); $scope.addCipher = function () { var addModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultAddCipher.html', controller: 'vaultAddCipherController', resolve: { selectedFolder: function () { return $scope.selectedFolder; }, selectedType: function () { return $scope.selectedType; }, checkedFavorite: function () { return $scope.selectedFavorites; } } }); addModel.result.then(function (addedCipher) { $rootScope.vaultCiphers.push(addedCipher); sortScopedCipherData(); }); }; $scope.deleteCipher = function (cipher) { if (!confirm('Are you sure you want to delete this item (' + cipher.name + ')?')) { return; } apiService.ciphers.del({ id: cipher.id }, function () { $analytics.eventTrack('Deleted Item'); removeCipherFromScopes(cipher); }); }; $scope.attachments = function (cipher) { authService.getUserProfile().then(function (profile) { return { isPremium: profile.premium, orgUseStorage: cipher.organizationId && !!profile.organizations[cipher.organizationId].maxStorageGb }; }).then(function (perms) { if (!cipher.hasAttachments) { if (cipher.organizationId && !perms.orgUseStorage) { $uibModal.open({ animation: true, templateUrl: 'app/views/paidOrgRequired.html', controller: 'paidOrgRequiredController', resolve: { orgId: function () { return cipher.organizationId; } } }); return; } if (!cipher.organizationId && !perms.isPremium) { $uibModal.open({ animation: true, templateUrl: 'app/views/premiumRequired.html', controller: 'premiumRequiredController' }); return; } } if (!cipher.organizationId && !cryptoService.getEncKey()) { toastr.error('You cannot use this feature until you update your encryption key.', 'Feature Unavailable'); return; } var attachmentModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultAttachments.html', controller: 'vaultAttachmentsController', resolve: { cipherId: function () { return cipher.id; } } }); attachmentModel.result.then(function (hasAttachments) { cipher.hasAttachments = hasAttachments; }); }); }; $scope.editFolder = function (folder) { if (!folder.id) { return; } var editModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultEditFolder.html', controller: 'vaultEditFolderController', size: 'sm', resolve: { folderId: function () { return folder.id; } } }); editModel.result.then(function (editedFolder) { folder.name = editedFolder.name; }); }; $scope.$on('vaultAddFolder', function (event, args) { $scope.addFolder(); }); $scope.addFolder = function () { var addModel = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultAddFolder.html', controller: 'vaultAddFolderController', size: 'sm' }); addModel.result.then(function (addedFolder) { $rootScope.vaultFolders.push(addedFolder); }); }; $scope.deleteFolder = function (folder) { if (!folder.id) { return; } if (!confirm('Are you sure you want to delete this folder (' + folder.name + ')? ' + 'Any items will be moved to "No Folder".')) { return; } apiService.folders.del({ id: folder.id }, function () { $analytics.eventTrack('Deleted Folder'); var index = $rootScope.vaultFolders.indexOf(folder); if (index > -1) { $rootScope.vaultFolders.splice(index, 1); $scope.filterAll(); } }); }; $scope.share = function (cipher) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultShareCipher.html', controller: 'vaultShareCipherController', resolve: { cipherId: function () { return cipher.id; } } }); modal.result.then(function (returned) { cipher.organizationId = returned.orgId; cipher.collectionIds = returned.collectionIds || []; }); }; $scope.editCollections = function (cipher) { var modal = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultCipherCollections.html', controller: 'vaultCipherCollectionsController', resolve: { cipherId: function () { return cipher.id; } } }); modal.result.then(function (response) { if (response.collectionIds && !response.collectionIds.length) { removeCipherFromScopes(cipher); } else if (response.collectionIds) { cipher.collectionIds = response.collectionIds; } }); }; $scope.filterCollection = function (col) { resetSelected(); $scope.selectedCollection = col; $scope.selectedIcon = 'fa-cube'; $scope.filter = function (c) { return c.collectionIds && c.collectionIds.indexOf(col.id) > -1; }; fixLayout(); }; $scope.filterFolder = function (f) { resetSelected(); $scope.selectedFolder = f; $scope.selectedIcon = 'fa-folder-open' + (!f.id ? '-o' : ''); $scope.filter = function (c) { return c.folderId === f.id; }; fixLayout(); }; $scope.filterType = function (t) { resetSelected(); $scope.selectedType = t; switch (t) { case constants.cipherType.login: $scope.selectedTitle = 'Login'; $scope.selectedIcon = 'fa-globe'; break; case constants.cipherType.card: $scope.selectedTitle = 'Card'; $scope.selectedIcon = 'fa-credit-card'; break; case constants.cipherType.identity: $scope.selectedTitle = 'Identity'; $scope.selectedIcon = 'fa-id-card-o'; break; case constants.cipherType.secureNote: $scope.selectedTitle = 'Secure Note'; $scope.selectedIcon = 'fa-sticky-note-o'; break; default: break; } $scope.filter = function (c) { return c.type === t; }; fixLayout(); }; $scope.filterFavorites = function () { resetSelected(); $scope.selectedFavorites = true; $scope.selectedTitle = 'Favorites'; $scope.selectedIcon = 'fa-star'; $scope.filter = function (c) { return !!c.favorite; }; fixLayout(); }; $scope.filterAll = function () { resetSelected(); $scope.selectedAll = true; $scope.selectedTitle = 'All'; $scope.selectedIcon = 'fa-th'; $scope.filter = null; fixLayout(); }; function resetSelected() { $scope.selectedFolder = undefined; $scope.selectedCollection = undefined; $scope.selectedType = undefined; $scope.selectedFavorites = false; $scope.selectedAll = false; } function fixLayout() { if ($.AdminLTE && $.AdminLTE.layout) { $timeout(function () { $.AdminLTE.layout.fix(); }, 0); } } $scope.cipherFilter = function () { return function (cipher) { return !$scope.filter || $scope.filter(cipher); }; }; $scope.unselectAll = function () { selectAll(false); }; $scope.selectAll = function () { selectAll(true); }; $scope.select = function ($event) { var checkbox = $($event.currentTarget).closest('tr').find('input[name="cipherSelection"]'); checkbox.prop('checked', !checkbox.prop('checked')); }; function distinct(value, index, self) { return self.indexOf(value) === index; } function getSelectedCiphers() { return $('input[name="cipherSelection"]:checked').map(function () { return $(this).val(); }).get().filter(distinct); } function selectAll(select) { $('input[name="cipherSelection"]').prop('checked', select); } $scope.bulkMove = function () { var ids = getSelectedCiphers(); if (ids.length === 0) { alert('You have not selected anything.'); return; } var modal = $uibModal.open({ animation: true, templateUrl: 'app/vault/views/vaultMoveCiphers.html', controller: 'vaultMoveCiphersController', size: 'sm', resolve: { ids: function () { return ids; } } }); modal.result.then(function (folderId) { for (var i = 0; i < ids.length; i++) { var cipher = $filter('filter')($rootScope.vaultCiphers, { id: ids[i] }); if (cipher.length) { cipher[0].folderId = folderId; } } selectAll(false); sortScopedCipherData(); toastr.success('Items have been moved!'); }); }; $scope.bulkDelete = function () { var ids = getSelectedCiphers(); if (ids.length === 0) { alert('You have not selected anything.'); return; } if (!confirm('Are you sure you want to delete the selected items (total: ' + ids.length + ')?')) { return; } $scope.actionLoading = true; apiService.ciphers.delMany({ ids: ids }, function () { $analytics.eventTrack('Bulk Deleted Items'); for (var i = 0; i < ids.length; i++) { var cipher = $filter('filter')($rootScope.vaultCiphers, { id: ids[i] }); if (cipher.length && cipher[0].edit) { removeCipherFromScopes(cipher[0]); } } selectAll(false); $scope.actionLoading = false; toastr.success('Items have been deleted!'); }, function (e) { var errors = validationService.parseErrors(e); toastr.error(errors.length ? errors[0] : 'An error occurred.'); $scope.actionLoading = false; }); }; function removeCipherFromScopes(cipher) { var index = $rootScope.vaultCiphers.indexOf(cipher); if (index > -1) { $rootScope.vaultCiphers.splice(index, 1); } index = $scope.ciphers.indexOf(cipher); if (index > -1) { $scope.ciphers.splice(index, 1); } } }]); angular .module('bit.vault') .controller('vaultEditCipherController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "passwordService", "cipherId", "$analytics", "$rootScope", "authService", "$uibModal", "constants", "$filter", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, cipherId, $analytics, $rootScope, authService, $uibModal, constants, $filter) { $analytics.eventTrack('vaultEditCipherController', { category: 'Modal' }); $scope.folders = $rootScope.vaultFolders; $scope.cipher = {}; $scope.readOnly = false; $scope.constants = constants; authService.getUserProfile().then(function (profile) { $scope.useTotp = profile.premium; return apiService.ciphers.get({ id: cipherId }).$promise; }).then(function (cipher) { $scope.cipher = cipherService.decryptCipher(cipher); $scope.readOnly = !$scope.cipher.edit; $scope.useTotp = $scope.useTotp || $scope.cipher.organizationUseTotp; setUriMatchValues(); }); $scope.save = function (model) { if ($scope.readOnly) { $scope.savePromise = apiService.ciphers.putPartial({ id: cipherId }, { folderId: model.folderId, favorite: model.favorite }, function (response) { $analytics.eventTrack('Partially Edited Cipher'); $uibModalInstance.close({ action: 'partialEdit', data: { id: cipherId, favorite: model.favorite, folderId: model.folderId && model.folderId !== '' ? model.folderId : null } }); }).$promise; } else { var cipher = cipherService.encryptCipher(model, $scope.cipher.type); $scope.savePromise = apiService.ciphers.put({ id: cipherId }, cipher, function (cipherResponse) { $analytics.eventTrack('Edited Cipher'); var decCipher = cipherService.decryptCipherPreview(cipherResponse); $uibModalInstance.close({ action: 'edit', data: decCipher }); }).$promise; } }; $scope.generatePassword = function () { if (!$scope.cipher.login.password || confirm('Are you sure you want to overwrite the current password?')) { $analytics.eventTrack('Generated Password From Edit'); $scope.cipher.login.password = passwordService.generatePassword({ length: 14, special: true }); } }; $scope.addUri = function () { if (!$scope.cipher.login) { return; } if (!$scope.cipher.login.uris) { $scope.cipher.login.uris = []; } $scope.cipher.login.uris.push({ uri: null, match: null, matchValue: null }); }; $scope.removeUri = function (uri) { if (!$scope.cipher.login || !$scope.cipher.login.uris) { return; } var index = $scope.cipher.login.uris.indexOf(uri); if (index > -1) { $scope.cipher.login.uris.splice(index, 1); } }; $scope.uriMatchChanged = function (uri) { if ((!uri.matchValue && uri.matchValue !== 0) || uri.matchValue === '') { uri.match = null; } else { uri.match = parseInt(uri.matchValue); } }; $scope.addField = function () { if (!$scope.cipher.fields) { $scope.cipher.fields = []; } $scope.cipher.fields.push({ type: constants.fieldType.text.toString(), name: null, value: null }); }; $scope.removeField = function (field) { var index = $scope.cipher.fields.indexOf(field); if (index > -1) { $scope.cipher.fields.splice(index, 1); } }; $scope.toggleFavorite = function () { $scope.cipher.favorite = !$scope.cipher.favorite; }; $scope.clipboardSuccess = function (e) { e.clearSelection(); selectPassword(e); }; $scope.clipboardError = function (e, password) { if (password) { selectPassword(e); } alert('Your web browser does not support easy clipboard copying. Copy it manually instead.'); }; $scope.folderSort = function (item) { if (!item.id) { return ''; } return item.name.toLowerCase(); }; function selectPassword(e) { var target = $(e.trigger).parent().prev(); if (target.attr('type') === 'text') { target.select(); } } $scope.delete = function () { if (!confirm('Are you sure you want to delete this item (' + $scope.cipher.name + ')?')) { return; } apiService.ciphers.del({ id: $scope.cipher.id }, function () { $analytics.eventTrack('Deleted Cipher From Edit'); $uibModalInstance.close({ action: 'delete', data: $scope.cipher.id }); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; $scope.showUpgrade = function () { $uibModal.open({ animation: true, templateUrl: 'app/views/premiumRequired.html', controller: 'premiumRequiredController' }); }; function setUriMatchValues() { if ($scope.cipher.login && $scope.cipher.login.uris) { for (var i = 0; i < $scope.cipher.login.uris.length; i++) { $scope.cipher.login.uris[i].matchValue = $scope.cipher.login.uris[i].match || $scope.cipher.login.uris[i].match === 0 ? $scope.cipher.login.uris[i].match.toString() : ''; } } } }]); angular .module('bit.vault') .controller('vaultEditFolderController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "folderId", "$analytics", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, folderId, $analytics) { $analytics.eventTrack('vaultEditFolderController', { category: 'Modal' }); $scope.folder = {}; apiService.folders.get({ id: folderId }, function (folder) { $scope.folder = cipherService.decryptFolder(folder); }); $scope.savePromise = null; $scope.save = function (model) { var folder = cipherService.encryptFolder(model); $scope.savePromise = apiService.folders.put({ id: folderId }, folder, function (response) { $analytics.eventTrack('Edited Folder'); var decFolder = cipherService.decryptFolder(response); $uibModalInstance.close(decFolder); }).$promise; }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.vault') .controller('vaultMoveCiphersController', ["$scope", "apiService", "$uibModalInstance", "ids", "$analytics", "$rootScope", "$filter", function ($scope, apiService, $uibModalInstance, ids, $analytics, $rootScope, $filter) { $analytics.eventTrack('vaultMoveCiphersController', { category: 'Modal' }); $scope.folders = $rootScope.vaultFolders; $scope.count = ids.length; $scope.save = function () { $scope.savePromise = apiService.ciphers.moveMany({ ids: ids, folderId: $scope.folderId }, function () { $analytics.eventTrack('Bulk Moved Ciphers'); $uibModalInstance.close($scope.folderId || null); }).$promise; }; $scope.folderSort = function (item) { if (!item.id) { return '!'; } return item.name.toLowerCase(); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]); angular .module('bit.vault') .controller('vaultShareCipherController', ["$scope", "apiService", "$uibModalInstance", "authService", "cipherService", "cipherId", "$analytics", "$state", "cryptoService", "$q", "toastr", function ($scope, apiService, $uibModalInstance, authService, cipherService, cipherId, $analytics, $state, cryptoService, $q, toastr) { $analytics.eventTrack('vaultShareCipherController', { category: 'Modal' }); $scope.model = {}; $scope.cipher = {}; $scope.collections = []; $scope.selectedCollections = {}; $scope.organizations = []; var organizationCollectionCounts = {}; $scope.loadingCollections = true; $scope.loading = true; $scope.readOnly = false; apiService.ciphers.get({ id: cipherId }).$promise.then(function (cipher) { $scope.readOnly = !cipher.Edit; if (cipher.Edit) { $scope.cipher = cipherService.decryptCipher(cipher); } return cipher.Edit; }).then(function (canEdit) { $scope.loading = false; if (!canEdit) { return; } return authService.getUserProfile(); }).then(function (profile) { if (profile && profile.organizations) { var orgs = [], setFirstOrg = false; for (var i in profile.organizations) { if (profile.organizations.hasOwnProperty(i) && profile.organizations[i].enabled) { orgs.push({ id: profile.organizations[i].id, name: profile.organizations[i].name }); organizationCollectionCounts[profile.organizations[i].id] = 0; if (!setFirstOrg) { setFirstOrg = true; $scope.model.organizationId = profile.organizations[i].id; } } } $scope.organizations = orgs; apiService.collections.listMe({ writeOnly: true }, function (response) { var collections = []; for (var i = 0; i < response.Data.length; i++) { var decCollection = cipherService.decryptCollection(response.Data[i]); decCollection.organizationId = response.Data[i].OrganizationId; collections.push(decCollection); organizationCollectionCounts[decCollection.organizationId]++; } $scope.collections = collections; $scope.loadingCollections = false; }); } }); $scope.toggleCollectionSelectionAll = function ($event) { var collections = {}; if ($event.target.checked) { for (var i = 0; i < $scope.collections.length; i++) { if ($scope.model.organizationId && $scope.collections[i].organizationId === $scope.model.organizationId) { collections[$scope.collections[i].id] = true; } } } $scope.selectedCollections = collections; }; $scope.toggleCollectionSelection = function (id) { if (id in $scope.selectedCollections) { delete $scope.selectedCollections[id]; } else { $scope.selectedCollections[id] = true; } }; $scope.collectionSelected = function (collection) { return collection.id in $scope.selectedCollections; }; $scope.allSelected = function () { if (!$scope.model.organizationId) { return false; } return Object.keys($scope.selectedCollections).length === organizationCollectionCounts[$scope.model.organizationId]; }; $scope.orgChanged = function () { $scope.selectedCollections = {}; }; $scope.submitPromise = null; $scope.submit = function (model) { var orgKey = cryptoService.getOrgKey(model.organizationId); var errorOnUpload = false; var attachmentSharePromises = []; if ($scope.cipher.attachments) { for (var i = 0; i < $scope.cipher.attachments.length; i++) { /* jshint ignore:start */ (function (attachment) { var promise = cipherService.downloadAndDecryptAttachment(null, attachment, false) .then(function (decData) { return cryptoService.encryptToBytes(decData.buffer, orgKey); }).then(function (encData) { if (errorOnUpload) { return; } var fd = new FormData(); var blob = new Blob([encData], { type: 'application/octet-stream' }); var encFilename = cryptoService.encrypt(attachment.fileName, orgKey); fd.append('data', blob, encFilename); return apiService.ciphers.postShareAttachment({ id: cipherId, attachmentId: attachment.id, orgId: model.organizationId }, fd).$promise; }, function (err) { errorOnUpload = true; }); attachmentSharePromises.push(promise); })($scope.cipher.attachments[i]); /* jshint ignore:end */ } } var returnedCollectionIds = null; $scope.submitPromise = $q.all(attachmentSharePromises).then(function () { if (errorOnUpload) { return; } $scope.cipher.organizationId = model.organizationId; var request = { collectionIds: [], cipher: cipherService.encryptCipher($scope.cipher, $scope.cipher.type, null, true) }; for (var id in $scope.selectedCollections) { if ($scope.selectedCollections.hasOwnProperty(id)) { request.collectionIds.push(id); } } returnedCollectionIds = request.collectionIds; return apiService.ciphers.putShare({ id: cipherId }, request).$promise; }).then(function (response) { $analytics.eventTrack('Shared Cipher'); toastr.success('Item has been shared.'); $uibModalInstance.close({ orgId: model.organizationId, collectionIds: returnedCollectionIds }); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; $scope.createOrg = function () { $state.go('backend.user.settingsCreateOrg').then(function () { $uibModalInstance.dismiss('cancel'); }); }; }]); angular .module('bit.tools') .controller('toolsController', ["$scope", "$uibModal", "apiService", "toastr", "authService", function ($scope, $uibModal, apiService, toastr, authService) { $scope.import = function () { $uibModal.open({ animation: true, templateUrl: 'app/tools/views/toolsImport.html', controller: 'toolsImportController' }); }; $scope.export = function () { $uibModal.open({ animation: true, templateUrl: 'app/tools/views/toolsExport.html', controller: 'toolsExportController' }); }; }]); angular .module('bit.tools') .controller('toolsExportController', ["$scope", "apiService", "$uibModalInstance", "cipherService", "$q", "toastr", "$analytics", "constants", function ($scope, apiService, $uibModalInstance, cipherService, $q, toastr, $analytics, constants) { $analytics.eventTrack('toolsExportController', { category: 'Modal' }); $scope.export = function (model) { $scope.startedExport = true; var decCiphers = [], decFolders = []; var folderPromise = apiService.folders.list({}, function (folders) { decFolders = cipherService.decryptFolders(folders.Data); }).$promise; var ciphersPromise = apiService.ciphers.list({}, function (ciphers) { decCiphers = cipherService.decryptCiphers(ciphers.Data); }).$promise; $q.all([folderPromise, ciphersPromise]).then(function () { if (!decCiphers.length) { toastr.error('Nothing to export.', 'Error!'); $scope.close(); return; } var foldersDict = {}; for (var i = 0; i < decFolders.length; i++) { foldersDict[decFolders[i].id] = decFolders[i]; } try { var exportCiphers = []; for (i = 0; i < decCiphers.length; i++) { // only export logins and secure notes if (decCiphers[i].type !== constants.cipherType.login && decCiphers[i].type !== constants.cipherType.secureNote) { continue; } var cipher = { folder: decCiphers[i].folderId && (decCiphers[i].folderId in foldersDict) ? foldersDict[decCiphers[i].folderId].name : null, favorite: decCiphers[i].favorite ? 1 : null, type: null, name: decCiphers[i].name, notes: decCiphers[i].notes, fields: null, // Login props login_uri: null, login_username: null, login_password: null, login_totp: null }; var j; if (decCiphers[i].fields) { for (j = 0; j < decCiphers[i].fields.length; j++) { if (!cipher.fields) { cipher.fields = ''; } else { cipher.fields += '\n'; } cipher.fields += ((decCiphers[i].fields[j].name || '') + ': ' + decCiphers[i].fields[j].value); } } switch (decCiphers[i].type) { case constants.cipherType.login: cipher.type = 'login'; cipher.login_username = decCiphers[i].login.username; cipher.login_password = decCiphers[i].login.password; cipher.login_totp = decCiphers[i].login.totp; if (decCiphers[i].login.uris && decCiphers[i].login.uris.length) { cipher.login_uri = []; for (j = 0; j < decCiphers[i].login.uris.length; j++) { cipher.login_uri.push(decCiphers[i].login.uris[j].uri); } } break; case constants.cipherType.secureNote: cipher.type = 'note'; break; default: continue; } exportCiphers.push(cipher); } var csvString = Papa.unparse(exportCiphers); var csvBlob = new Blob([csvString]); // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(csvBlob, makeFileName()); } else { var a = window.document.createElement('a'); a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); a.download = makeFileName(); document.body.appendChild(a); // IE: "Access is denied". // ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access a.click(); document.body.removeChild(a); } $analytics.eventTrack('Exported Data'); toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!'); $scope.close(); } catch (err) { toastr.error('Something went wrong. Please try again.', 'Error!'); $scope.close(); } }, function () { toastr.error('Something went wrong. Please try again.', 'Error!'); $scope.close(); }); }; $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; function makeFileName() { var now = new Date(); var dateString = now.getFullYear() + '' + padNumber(now.getMonth() + 1, 2) + '' + padNumber(now.getDate(), 2) + padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + padNumber(now.getSeconds(), 2); return 'bitwarden_export_' + dateString + '.csv'; } function padNumber(number, width, paddingCharacter) { paddingCharacter = paddingCharacter || '0'; number = number + ''; return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; } }]); angular .module('bit.tools') .controller('toolsImportController', ["$scope", "$state", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "toastr", "importService", "$analytics", "$sce", "validationService", function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics, $sce, validationService) { $analytics.eventTrack('toolsImportController', { category: 'Modal' }); $scope.model = { source: '' }; $scope.source = {}; $scope.splitFeatured = true; $scope.options = [ { id: 'bitwardencsv', name: 'Bitwarden (csv)', featured: true, sort: 1, instructions: $sce.trustAsHtml('Export using the web vault (vault.bitwarden.com). ' + 'Log into the web vault and navigate to "Tools" > "Export".') }, { id: 'lastpass', name: 'LastPass (csv)', featured: true, sort: 2, instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-lastpass/') }, { id: 'chromecsv', name: 'Chrome (csv)', featured: true, sort: 3, instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-chrome/') }, { id: 'firefoxpasswordexportercsv', name: 'Firefox Password Exporter (csv)', featured: true, sort: 4, instructions: $sce.trustAsHtml('Use the ' + '' + 'FF Password Exporter application to export your passwords to a CSV file.') }, { id: 'keepass2xml', name: 'KeePass 2 (xml)', featured: true, sort: 5, instructions: $sce.trustAsHtml('Using the KeePass 2 desktop application, navigate to "File" > "Export" and ' + 'select the KeePass XML (2.x) option.') }, { id: 'keepassxcsv', name: 'KeePassX (csv)', instructions: $sce.trustAsHtml('Using the KeePassX desktop application, navigate to "Database" > ' + '"Export to CSV file" and save the CSV file.') }, { id: 'dashlanecsv', name: 'Dashlane (csv)', featured: true, sort: 7, instructions: $sce.trustAsHtml('Using the Dashlane desktop application, navigate to "File" > "Export" > ' + '"Unsecured archive (readable) in CSV format" and save the CSV file.') }, { id: '1password1pif', name: '1Password (1pif)', featured: true, sort: 6, instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-1password/') }, { id: '1password6wincsv', name: '1Password 6 Windows (csv)', instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-1password/') }, { id: 'roboformhtml', name: 'RoboForm (html)', instructions: $sce.trustAsHtml('Using the RoboForm Editor desktop application, navigate to "RoboForm" ' + '(top left) > "Print List" > "Logins". When the following print dialog pops up click on the "Save" button ' + 'and save the HTML file.') }, { id: 'keepercsv', name: 'Keeper (csv)', instructions: $sce.trustAsHtml('Log into the Keeper web vault (keepersecurity.com/vault). Navigate to "Backup" ' + '(top right) and find the "Export to Text File" option. Click "Export Now" to save the TXT/CSV file.') }, { id: 'enpasscsv', name: 'Enpass (csv)', instructions: $sce.trustAsHtml('Using the Enpass desktop application, navigate to "File" > "Export" > ' + '"As CSV". Select "Yes" to the warning alert and save the CSV file. Note that the importer only fully ' + 'supports files exported while Enpass is set to the English language, so adjust your settings accordingly.') }, { id: 'safeincloudxml', name: 'SafeInCloud (xml)', instructions: $sce.trustAsHtml('Using the SaveInCloud desktop application, navigate to "File" > "Export" > ' + '"As XML" and save the XML file.') }, { id: 'pwsafexml', name: 'Password Safe (xml)', instructions: $sce.trustAsHtml('Using the Password Safe desktop application, navigate to "File" > ' + '"Export To" > "XML format..." and save the XML file.') }, { id: 'stickypasswordxml', name: 'Sticky Password (xml)', instructions: $sce.trustAsHtml('Using the Sticky Password desktop application, navigate to "Menu" ' + '(top right) > "Export" > "Export all". Select the unencrypted format XML option and then the ' + '"Save to file" button. Save the XML file.') }, { id: 'msecurecsv', name: 'mSecure (csv)', instructions: $sce.trustAsHtml('Using the mSecure desktop application, navigate to "File" > ' + '"Export" > "CSV File..." and save the CSV file.') }, { id: 'truekeycsv', name: 'True Key (csv)', instructions: $sce.trustAsHtml('Using the True Key desktop application, click the gear icon (top right) and ' + 'then navigate to "App Settings". Click the "Export" button, enter your password and save the CSV file.') }, { id: 'passwordbossjson', name: 'Password Boss (json)', instructions: $sce.trustAsHtml('Using the Password Boss desktop application, navigate to "File" > ' + '"Export data" > "Password Boss JSON - not encrypted" and save the JSON file.') }, { id: 'zohovaultcsv', name: 'Zoho Vault (csv)', instructions: $sce.trustAsHtml('Log into the Zoho web vault (vault.zoho.com). Navigate to "Tools" > ' + '"Export Secrets". Select "All Secrets" and click the "Zoho Vault Format CSV" button. Highlight ' + 'and copy the data from the textarea. Open a text editor like Notepad and paste the data. Save the ' + 'data from the text editor as zoho_export.csv.') }, { id: 'splashidcsv', name: 'SplashID (csv)', instructions: $sce.trustAsHtml('Using the SplashID Safe desktop application, click on the SplashID ' + 'blue lock logo in the top right corner. Navigate to "Export" > "Export as CSV" and save the CSV file.') }, { id: 'passworddragonxml', name: 'Password Dragon (xml)', instructions: $sce.trustAsHtml('Using the Password Dragon desktop application, navigate to "File" > ' + '"Export" > "To XML". In the dialog that pops up select "All Rows" and check all fields. Click ' + 'the "Export" button and save the XML file.') }, { id: 'padlockcsv', name: 'Padlock (csv)', instructions: $sce.trustAsHtml('Using the Padlock desktop application, click the hamburger icon ' + 'in the top left corner and navigate to "Settings". Click the "Export Data" option. Ensure that ' + 'the "CSV" option is selected from the dropdown. Highlight and copy the data from the textarea. ' + 'Open a text editor like Notepad and paste the data. Save the data from the text editor as ' + 'padlock_export.csv.') }, { id: 'clipperzhtml', name: 'Clipperz (html)', instructions: $sce.trustAsHtml('Log into the Clipperz web application (clipperz.is/app). Click the ' + 'hamburger menu icon in the top right to expand the navigation bar. Navigate to "Data" > ' + '"Export". Click the "download HTML+JSON" button to save the HTML file.') }, { id: 'avirajson', name: 'Avira (json)', instructions: $sce.trustAsHtml('Using the Avira browser extension, click your username in the top ' + 'right corner and navigate to "Settings". Locate the "Export Data" section and click "Export". ' + 'In the dialog that pops up, click the "Export Password Manager Data" button to save the ' + 'TXT/JSON file.') }, { id: 'saferpasscsv', name: 'SaferPass (csv)', instructions: $sce.trustAsHtml('Using the SaferPass browser extension, click the hamburger icon ' + 'in the top left corner and navigate to "Settings". Click the "Export accounts" button to ' + 'save the CSV file.') }, { id: 'upmcsv', name: 'Universal Password Manager (csv)', instructions: $sce.trustAsHtml('Using the Universal Password Manager desktop application, navigate ' + 'to "Database" > "Export" and save the CSV file.') }, { id: 'ascendocsv', name: 'Ascendo DataVault (csv)', instructions: $sce.trustAsHtml('Using the Ascendo DataVault desktop application, navigate ' + 'to "Tools" > "Export". In the dialog that pops up, select the "All Items (DVX, CSV)" ' + 'option. Click the "Ok" button to save the CSV file.') }, { id: 'meldiumcsv', name: 'Meldium (csv)', instructions: $sce.trustAsHtml('Using the Meldium web vault, navigate to "Settings". ' + 'Locate the "Export data" function and click "Show me my data" to save the CSV file.') }, { id: 'passkeepcsv', name: 'PassKeep (csv)', instructions: $sce.trustAsHtml('Using the PassKeep mobile app, navigate to "Backup/Restore". ' + 'Locate the "CSV Backup/Restore" section and click "Backup to CSV" to save the CSV file.') }, { id: 'operacsv', name: 'Opera (csv)', instructions: $sce.trustAsHtml('The process for importing from Opera is exactly the same as ' + 'importing from Google Chrome. See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-chrome/') }, { id: 'vivaldicsv', name: 'Vivaldi (csv)', instructions: $sce.trustAsHtml('The process for importing from Vivaldi is exactly the same as ' + 'importing from Google Chrome. See detailed instructions on our help site at ' + '' + 'https://help.bitwarden.com/article/import-from-chrome/') }, { id: 'gnomejson', name: 'GNOME Passwords and Keys/Seahorse (json)', instructions: $sce.trustAsHtml('Make sure you have python-keyring and python-gnomekeyring installed. ' + 'Save the GNOME Keyring Import/Export ' + 'python script by Luke Plant to your desktop as pw_helper.py. Open terminal and run ' + 'chmod +rx Desktop/pw_helper.py and then ' + 'python Desktop/pw_helper.py export Desktop/my_passwords.json. Then upload ' + 'the resulting my_passwords.json file here to Bitwarden.') } ]; $scope.setSource = function () { for (var i = 0; i < $scope.options.length; i++) { if ($scope.options[i].id === $scope.model.source) { $scope.source = $scope.options[i]; break; } } }; $scope.setSource(); $scope.import = function (model, form) { if (!model.source || model.source === '') { validationService.addError(form, 'source', 'Select the format of the import file.', true); return; } var file = document.getElementById('file').files[0]; if (!file && (!model.fileContents || model.fileContents === '')) { validationService.addError(form, 'file', 'Select the import file or copy/paste the import file contents.', true); return; } $scope.processing = true; importService.import(model.source, file || model.fileContents, importSuccess, importError); }; function importSuccess(folders, ciphers, folderRelationships) { if (!folders.length && !ciphers.length) { importError('Nothing was imported.'); return; } else if (ciphers.length) { var halfway = Math.floor(ciphers.length / 2); var last = ciphers.length - 1; if (cipherIsBadData(ciphers[0]) && cipherIsBadData(ciphers[halfway]) && cipherIsBadData(ciphers[last])) { importError('Data is not formatted correctly. Please check your import file and try again.'); return; } } apiService.ciphers.import({ folders: cipherService.encryptFolders(folders), ciphers: cipherService.encryptCiphers(ciphers), folderRelationships: folderRelationships }, function () { $uibModalInstance.dismiss('cancel'); $state.go('backend.user.vault', { refreshFromServer: true }).then(function () { $analytics.eventTrack('Imported Data', { label: $scope.model.source }); toastr.success('Data has been successfully imported into your vault.', 'Import Success'); }); }, importError); } function cipherIsBadData(cipher) { return (cipher.name === null || cipher.name === '--') && (cipher.login && (cipher.login.password === null || cipher.login.password === '')); } function importError(error) { $analytics.eventTrack('Import Data Failed', { label: $scope.model.source }); $uibModalInstance.dismiss('cancel'); if (error) { var data = error.data; if (data && data.ValidationErrors) { var message = ''; for (var key in data.ValidationErrors) { if (!data.ValidationErrors.hasOwnProperty(key)) { continue; } for (var i = 0; i < data.ValidationErrors[key].length; i++) { message += (key + ': ' + data.ValidationErrors[key][i] + ' '); } } if (message !== '') { toastr.error(message); return; } } else if (data && data.Message) { toastr.error(data.Message); return; } else { toastr.error(error); return; } } toastr.error('Something went wrong. Try again.', 'Oh No!'); } $scope.close = function () { $uibModalInstance.dismiss('cancel'); }; }]);