From 0c9cc1306ed4e2e3ccda3ebe1d1857064add1d06 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 28 Dec 2017 23:22:27 -0500 Subject: [PATCH] Update 2017-12-29T04:22:25.941Z --- index.html | 14 +- js/app.min.js | 14873 ++++++++++++++++++++++++++++++++++- js/bw.min.js | 1398 +++- js/fallback-scripts.min.js | 10 +- js/fallback-styles.min.js | 18 +- js/lib.min.js | 7798 +++++++++--------- js/u2f.min.js | 919 ++- u2f-connector.html | 2 +- 8 files changed, 21120 insertions(+), 3912 deletions(-) diff --git a/index.html b/index.html index afe12ca5..547e6dca 100644 --- a/index.html +++ b/index.html @@ -15,9 +15,9 @@ - + - + @@ -35,11 +35,11 @@ integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"> - - + + - - - + + + diff --git a/js/app.min.js b/js/app.min.js index 4618e381..bec383fd 100644 --- a/js/app.min.js +++ b/js/app.min.js @@ -1 +1,14872 @@ -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:!1,version:"1.22.0",environment:"Production"}),angular.module("bit.accounts",["ui.bootstrap","ngCookies"]),angular.module("bit.directives",[]),angular.module("bit.global",[]),angular.module("bit.filters",[]),angular.module("bit.reports",["toastr","ngSanitize"]),angular.module("bit.services",["ngResource","ngStorage","angular-jwt"]),angular.module("bit.organization",["ui.bootstrap"]),angular.module("bit.settings",["ui.bootstrap","toastr"]),angular.module("bit.tools",["ui.bootstrap","toastr"]),angular.module("bit.vault",["ui.bootstrap","ngclipboard"]),angular.module("bit").factory("apiInterceptor",["$injector","$q","toastr","appSettings","utilsService",function(e,t,n,o,r){return{request:function(e){return e.url.indexOf(o.apiUri+"/")>-1&&(e.headers["Device-Type"]=r.getDeviceType()),e},response:function(o){return 401!==o.status&&403!==o.status||(e.get("authService").logOut(),e.get("$state").go("frontend.login.info").then(function(){n.warning("Your login session has expired.","Logged out")})),o||t.when(o)},responseError:function(o){return 401!==o.status&&403!==o.status||(e.get("authService").logOut(),e.get("$state").go("frontend.login.info").then(function(){n.warning("Your login session has expired.","Logged out")})),t.reject(o)}}}]),angular.module("bit").config(["$stateProvider","$urlRouterProvider","$httpProvider","jwtInterceptorProvider","jwtOptionsProvider","$uibTooltipProvider","toastrConfig","$locationProvider","$qProvider","appSettings","stripeProvider",function(e,t,n,o,r,a,i,s,l,c,u){angular.extend(c,window.bitwardenAppSettings),l.errorOnUnhandledRejections(!1),s.hashPrefix("");var d;o.tokenGetter=["options","tokenService","authService",function(e,t,n){if(-1!==e.url.indexOf(c.apiUri+"/")){if(d)return d;var o=t.getToken();if(o){if(!t.tokenNeedsRefresh(o))return o;var r=n.refreshAccessToken();if(r)return d=r.then(function(e){return d=null,e||o})}}}],u.setPublishableKey(c.stripeKey),angular.extend(i,{closeButton:!0,progressBar:!0,showMethod:"slideDown",target:".toast-target"}),a.options({popupDelay:600,appendToBody:!0}),(-1!==navigator.userAgent.indexOf("MSIE")||navigator.appVersion.indexOf("Trident/")>0)&&(n.defaults.headers.get||(n.defaults.headers.get={}),n.defaults.headers.get["Cache-Control"]="no-cache",n.defaults.headers.get.Pragma="no-cache"),n.interceptors.push("apiInterceptor"),n.interceptors.push("jwtInterceptor"),t.otherwise("/"),e.state("backend",{templateUrl:"app/views/backendLayout.html",abstract:!0,data:{authorize:!0}}).state("backend.user",{templateUrl:"app/views/userLayout.html",abstract:!0}).state("backend.user.vault",{url:"^/vault",templateUrl:"app/vault/views/vault.html",controller:"vaultController",data:{pageTitle:"My Vault",controlSidebar:!0},params:{refreshFromServer:!1}}).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.user.apps",{url:"^/apps",templateUrl:"app/views/apps.html",controller:"appsController",data:{pageTitle:"Get the Apps"}}).state("backend.org",{templateUrl:"app/views/organizationLayout.html",abstract:!0}).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"}}).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"}}).state("frontend",{templateUrl:"app/views/frontendLayout.html",abstract:!0,data:{authorize:!1}}).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:!0}}).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:!0}}).state("frontend.verifyEmail",{url:"^/verify-email?userId&token",templateUrl:"app/accounts/views/accountsVerifyEmail.html",controller:"accountsVerifyEmailController",data:{pageTitle:"Verifying Email",bodyClass:"login-page",skipAuthorize:!0}})}]).run(["$rootScope","authService","$state",function(e,t,n){e.$on("$stateChangeSuccess",function(){$("html, body").animate({scrollTop:0},200)}),e.$on("$stateChangeStart",function(o,r,a){if(!r.data||!r.data.authorize){if(r.data&&r.data.skipAuthorize)return;if(!t.isAuthenticated())return;return o.preventDefault(),void n.go("backend.user.vault")}if(!t.isAuthenticated())return o.preventDefault(),t.logOut(),void n.go("frontend.login.info");r.name.indexOf("backend.org.")>-1&&a.orgId&&(e.vaultCiphers=e.vaultGroupings=null,t.getUserProfile().then(function(e){var t=e.organizations;t&&a.orgId in t&&2===t[a.orgId].status&&2!==t[a.orgId].type||(o.preventDefault(),n.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},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:1e3,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:!1,active:!0,free:!0,image:"authapp.png",displayOrder:0,priority:1,requiresUsb:!1},{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:!1,active:!0,image:"yubico.png",displayOrder:1,priority:3,requiresUsb:!0},{type:2,name:"Duo",description:"Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.",enabled:!1,active:!0,image:"duo.png",displayOrder:2,priority:2,requiresUsb:!1},{type:4,name:"FIDO U2F Security Key",description:"Use any FIDO U2F enabled security key to access your account.",enabled:!1,active:!0,image:"fido.png",displayOrder:3,priority:4,requiresUsb:!0},{type:1,name:"Email",description:"Verification codes will be emailed to you.",enabled:!1,active:!0,free:!0,image:"gmail.png",displayOrder:4,priority:0,requiresUsb:!1}],plans:{free:{basePrice:0,noAdditionalSeats:!0,noPayment:!0,upgradeSortOrder:-1},families:{basePrice:1,annualBasePrice:12,baseSeats:5,noAdditionalSeats:!0,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:.33,monthlyPrice:.5,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(e,t,n,o,r,a,i,s,l,c,u,d,p,m){e.state=i,e.twoFactorProviderConstants=s.twoFactorProvider,e.rememberTwoFactor={checked:!1};var g=!0;e.returnState=i.params.returnState,e.stateEmail=i.params.email,!e.returnState&&i.params.org?e.returnState={name:"backend.user.settingsCreateOrg",params:{plan:i.params.org}}:!e.returnState&&i.params.premium&&(e.returnState={name:"backend.user.settingsPremium"}),!(i.current.name.indexOf("twoFactor")>-1)||e.twoFactorProviders&&e.twoFactorProviders.length||i.go("frontend.login.info",{returnState:e.returnState});var f=n.get(s.rememberedEmailCookieName);f||e.stateEmail?(e.model={email:e.stateEmail||f,rememberEmail:null!==f},u(function(){$("#masterPassword").focus()})):u(function(){$("#email").focus()});var h,v;e.twoFactorProviders=null,e.twoFactorProvider=null,e.login=function(t){e.loginPromise=a.logIn(t.email,t.masterPassword).then(function(o){if(t.rememberEmail){var r=new Date;r.setFullYear(r.getFullYear()+10),n.put(s.rememberedEmailCookieName,t.email,{expires:r})}else n.remove(s.rememberedEmailCookieName);o&&Object.keys(o).length>0?(h=t.email,v=t.masterPassword,e.twoFactorProviders=function(e){if(function(){var e;return e=navigator.userAgent||navigator.vendor||window.opera,(/(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(e)||/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(e.substr(0,4)))&&(a1842=!0),!a1842&&!navigator.userAgent.match(/iPad/i)}())return e;for(var t=Object.keys(e),n=0;no){if(a[0].type===s.twoFactorProvider.u2f&&!u2f.isSupported)continue;n=a[0].type,o=a[0].priority}}if(null===n)return null;return parseInt(n)}(e.twoFactorProviders),l.eventTrack("Logged In To Two-step"),i.go("frontend.login.twoFactor",{returnState:e.returnState}).then(function(){u(function(){$("#code").focus(),b()})})):(l.eventTrack("Logged In"),y()),t.masterPassword=""})};e.twoFactor=function(t){e.twoFactorProvider!==s.twoFactorProvider.email&&e.twoFactorProvider!==s.twoFactorProvider.authenticator||(t=t.replace(" ","")),e.twoFactorPromise=a.logIn(h,v,t,e.twoFactorProvider,e.rememberTwoFactor.checked||!1),e.twoFactorPromise.then(function(){l.eventTrack("Logged In From Two-step"),y()},function(){e.twoFactorProvider===s.twoFactorProvider.u2f&&b()})},e.anotherMethod=function(){c.open({animation:!0,templateUrl:"app/accounts/views/accountsTwoFactorMethods.html",controller:"accountsTwoFactorMethodsController",resolve:{providers:function(){return e.twoFactorProviders}}}).result.then(function(t){e.twoFactorProvider=t,u(function(){$("#code").focus(),b()})})},e.sendEmail=function(t){if(e.twoFactorProvider===s.twoFactorProvider.email)return r.makeKeyAndHash(h,v).then(function(e){return o.twoFactor.sendEmailLogin({email:h,masterPasswordHash:e.hash}).$promise}).then(function(){t&&m.success("Verification email sent to "+e.twoFactorEmail+".")},function(){m.error("Could not send verification email.")})},e.$on("$destroy",function(){g=!0});function y(){e.returnState?i.go(e.returnState.name,e.returnState.params):i.go("backend.user.vault")}function b(){g=!0;var t;if(e.twoFactorProvider===s.twoFactorProvider.duo)t=e.twoFactorProviders[s.twoFactorProvider.duo],d.Duo.init({host:t.Host,sig_request:t.Signature,submit_callback:function(t){var n=$(t).find('input[name="sig_response"]').val();e.twoFactor(n)}});else if(e.twoFactorProvider===s.twoFactorProvider.u2f){g=!1,t=e.twoFactorProviders[s.twoFactorProvider.u2f];w(JSON.parse(t.Challenges))}else e.twoFactorProvider===s.twoFactorProvider.email&&(t=e.twoFactorProviders[s.twoFactorProvider.email],e.twoFactorEmail=t.Email,Object.keys(e.twoFactorProviders).length>1&&e.sendEmail(!1))}function w(t){g||t.length<1||e.twoFactorProvider!==s.twoFactorProvider.u2f||(console.log("listening for u2f key..."),d.u2f.sign(t[0].appId,t[0].challenge,[{version:t[0].version,keyHandle:t[0].keyHandle}],function(n){if(e.twoFactorProvider===s.twoFactorProvider.u2f)return n.errorCode?(console.log(n.errorCode),void u(function(){w(t)},5===n.errorCode?0:1e3)):void e.twoFactor(JSON.stringify(n))},10))}}]),angular.module("bit.accounts").controller("accountsLogoutController",["$scope","authService","$state","$analytics",function(e,t,n,o){t.logOut(),o.eventTrack("Logged Out"),n.go("frontend.login.info")}]),angular.module("bit.accounts").controller("accountsOrganizationAcceptController",["$scope","$state","apiService","authService","toastr","$analytics",function(e,t,n,o,r,a){e.state={name:t.current.name,params:t.params},t.params.organizationId&&t.params.organizationUserId&&t.params.token&&t.params.email&&t.params.organizationName?e.$on("$viewContentLoaded",function(){o.isAuthenticated()?(e.accepting=!0,n.organizationUsers.accept({orgId:t.params.organizationId,id:t.params.organizationUserId},{token:t.params.token},function(){a.eventTrack("Accepted Invitation"),t.go("backend.user.vault",null,{location:"replace"}).then(function(){r.success("You can access this organization once an administrator confirms your membership. We'll send an email when that happens.","Invite Accepted",{timeOut:1e4})})},function(){a.eventTrack("Failed To Accept Invitation"),t.go("backend.user.vault",null,{location:"replace"}).then(function(){r.error("Unable to accept invitation.","Error")})})):e.loading=!1}):t.go("frontend.login.info").then(function(){r.error("Invalid parameters.")})}]),angular.module("bit.accounts").controller("accountsPasswordHintController",["$scope","$rootScope","apiService","$analytics",function(e,t,n,o){e.success=!1,e.submit=function(t){e.submitPromise=n.accounts.postPasswordHint({email:t.email},function(){o.eventTrack("Requested Password Hint"),e.success=!0}).$promise}}]),angular.module("bit.accounts").controller("accountsRecoverController",["$scope","apiService","cryptoService","$analytics",function(e,t,n,o){e.success=!1,e.submit=function(r){var a=r.email.toLowerCase();e.submitPromise=n.makeKeyAndHash(r.email,r.masterPassword).then(function(e){return t.twoFactor.recover({email:a,masterPasswordHash:e.hash,recoveryCode:r.code.replace(/\s/g,"").toLowerCase()}).$promise}).then(function(){o.eventTrack("Recovered 2FA"),e.success=!0})}}]),angular.module("bit.accounts").controller("accountsRecoverDeleteController",["$scope","$rootScope","apiService","$analytics",function(e,t,n,o){e.success=!1,e.submit=function(t){e.submitPromise=n.accounts.postDeleteRecover({email:t.email},function(){o.eventTrack("Started Delete Recovery"),e.success=!0}).$promise}}]),angular.module("bit.accounts").controller("accountsRegisterController",["$scope","$location","apiService","cryptoService","validationService","$analytics","$state","$timeout",function(e,t,n,o,r,a,i,s){var l=t.search(),c=i.params;e.createOrg=c.org,!c.returnState&&c.org?e.returnState={name:"backend.user.settingsCreateOrg",params:{plan:i.params.org}}:!c.returnState&&c.premium?e.returnState={name:"backend.user.settingsPremium",params:{plan:i.params.org}}:e.returnState=c.returnState,e.success=!1,e.model={email:l.email?l.email:c.email},e.readOnlyEmail=null!==c.email,s(function(){e.model.email?$("#name").focus():$("#email").focus()}),e.registerPromise=null,e.register=function(t){var i=!1;if(e.model.masterPassword.length<8&&(r.addError(t,"MasterPassword","Master password must be at least 8 characters long.",!0),i=!0),e.model.masterPassword!==e.model.confirmMasterPassword&&(r.addError(t,"ConfirmMasterPassword","Master password confirmation does not match.",!0),i=!0),!i){var s,l,c=e.model.email.toLowerCase();e.registerPromise=o.makeKeyAndHash(c,e.model.masterPassword).then(function(e){return s=e,l=o.makeEncKey(e.key),o.makeKeyPair(l.encKey)}).then(function(t){var o={name:e.model.name,email:c,masterPasswordHash:s.hash,masterPasswordHint:e.model.masterPasswordHint,key:l.encKeyEnc,keys:{publicKey:t.publicKey,encryptedPrivateKey:t.privateKeyEnc}};return n.accounts.register(o).$promise},function(e){return r.addError(t,null,"Problem generating keys.",!0),!1}).then(function(t){!1!==t&&(e.success=!0,a.eventTrack("Registered"))})}}}]),angular.module("bit.accounts").controller("accountsTwoFactorMethodsController",["$scope","$uibModalInstance","$analytics","providers","constants",function(e,t,n,o,r){n.eventTrack("accountsTwoFactorMethodsController",{category:"Modal"}),e.providers=[],o.hasOwnProperty(r.twoFactorProvider.authenticator)&&a(r.twoFactorProvider.authenticator),o.hasOwnProperty(r.twoFactorProvider.yubikey)&&a(r.twoFactorProvider.yubikey),o.hasOwnProperty(r.twoFactorProvider.email)&&a(r.twoFactorProvider.email),o.hasOwnProperty(r.twoFactorProvider.duo)&&a(r.twoFactorProvider.duo),o.hasOwnProperty(r.twoFactorProvider.u2f)&&u2f.isSupported&&a(r.twoFactorProvider.u2f),e.choose=function(e){t.close(e.type)},e.close=function(){t.dismiss("close")};function a(t){for(var n=0;n1&&(n=function(e,t){var n=e.split(" ");if(n&&n.length>1){for(var o="",r=0;r').attr({y:"50%",x:"50%",dy:"0.35em","pointer-events":"auto",fill:i,"font-family":s}).text(a).css({"font-weight":l,"font-size":c+"px"})),g=o.bgColor?o.bgColor:function(e){var t=0,n=0;for(n=0;n>8*n&255).toString(16)).substr(-2);return o}(r),f=(u=o.width,d=o.height,p=g,angular.element("").attr({xmlns:"http://www.w3.org/2000/svg","pointer-events":"none",width:u,height:d}).css({"background-color":p,width:u+"px",height:d+"px"}));f.append(m);var h=angular.element("
").append(f).html(),v="data:image/svg+xml;base64,"+window.btoa(unescape(encodeURIComponent(h))),y=angular.element("").attr({src:v,title:e.data});"true"===o.round&&y.css("border-radius","50%"),"true"===o.border&&y.css("border",o.borderStyle),o.class&&y.addClass(o.class),"true"===o.dynamic?(t.empty(),t.append(y)):t.replaceWith(y)}}}}),angular.module("bit.directives").directive("masterPassword",["cryptoService","authService",function(e,t){return{require:"ngModel",restrict:"A",link:function(n,o,r,a){t.getUserProfile().then(function(t){a.$parsers.unshift(function(n){if(n)return e.makeKey(n,t.email).then(function(t){var o=t.keyB64===e.getKey().keyB64;return a.$setValidity("masterPassword",o),o?n:void 0})}),a.$formatters.unshift(function(n){if(n)return e.makeKey(n,t.email).then(function(t){var o=t.keyB64===e.getKey().keyB64;return a.$setValidity("masterPassword",o),n})})})}}}]),angular.module("bit.directives").directive("pageTitle",["$rootScope","$timeout","appSettings",function(e,t,n){return{link:function(n,o){e.$on("$stateChangeStart",function(e,n,r,a,i){var s="bitwarden Web Vault";n.data&&n.data.pageTitle&&(s=n.data.pageTitle+" - "+s),t(function(){o.text(s)})})}}}]),angular.module("bit.directives").directive("passwordMeter",function(){return{template:'
{{value}}%
',restrict:"A",scope:{password:"=passwordMeter",username:"=passwordMeterUsername",outerClass:"@?"},link:function(e){var t=function(e){e.value=function(e,t){if(!t||t===e)return 0;var n=t.length;return e&&""!==e&&(-1!==e.indexOf(t)&&(n-=15),-1!==t.indexOf(e)&&(n-=e.length)),t.length>0&&t.length<=4?n+=t.length:t.length>=5&&t.length<=7?n+=6:t.length>=8&&t.length<=15?n+=12:t.length>=16&&(n+=18),t.match(/[a-z]/)&&(n+=1),t.match(/[A-Z]/)&&(n+=5),t.match(/\d/)&&(n+=5),t.match(/.*\d.*\d.*\d/)&&(n+=5),t.match(/[!,@,#,$,%,^,&,*,?,_,~]/)&&(n+=5),t.match(/.*[!,@,#,$,%,^,&,*,?,_,~].*[!,@,#,$,%,^,&,*,?,_,~]/)&&(n+=5),t.match(/(?=.*[a-z])(?=.*[A-Z])/)&&(n+=2),t.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)&&(n+=2),t.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!,@,#,$,%,^,&,*,?,_,~])/)&&(n+=2),n=Math.round(2*n),Math.max(0,Math.min(100,n))}(e.username,e.password),e.valueClass=function(e){switch(Math.round(e/33)){case 0:case 1:return"danger";case 2:return"warning";case 3:return"success"}}(e.value)};e.$watch("password",function(){t(e)}),e.$watch("username",function(){t(e)})}}}),angular.module("bit.directives").directive("passwordViewer",function(){return{restrict:"A",link:function(e,t,n){var o=n.passwordViewer;o&&(t.onclick=function(e){},t.on("click",function(e){var n=$(o);n&&"password"===n.attr("type")?(t.removeClass("fa-eye").addClass("fa-eye-slash"),n.attr("type","text")):n&&"text"===n.attr("type")&&(t.removeClass("fa-eye-slash").addClass("fa-eye"),n.attr("type","password"))}))}}}),angular.module("bit.directives").directive("stopClick",function(){return function(e,t,n){$(t).click(function(e){e.preventDefault()})}}),angular.module("bit.directives").directive("stopProp",function(){return function(e,t,n){$(t).click(function(e){e.stopPropagation()})}}),angular.module("bit.directives").directive("totp",["$timeout","$q",function(e,t){return{template:'
{{sec}}{{codeFormatted}}
',restrict:"A",scope:{key:"=totp"},link:function(n){var o=null,r=new function(){var e="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",n=function(e,t,n){return t+1>=e.length&&(e=Array(t+1-e.length).join(n)+e),e},o=function(e){return parseInt(e,16)},r=function(e){for(var t=new Uint8Array(e.length/2),n=0;n>>4).toString(16)),n.push((15&t[o]).toString(16));return n.join("")}(e)}).catch(function(e){return null})};this.getCode=function(e){var s,l=Math.round((new Date).getTime()/1e3),c=n((s=Math.floor(l/30),(s<15.5?"0":"")+Math.round(s).toString(16)),16,"0"),u=r(c),d=a(e);return d.length&&u.length?i(d,u).then(function(e){if(!e)return null;var t=o(e.substring(e.length-1)),n=(o(e.substr(2*t,8))&o("7fffffff"))+"";return n=n.substr(n.length-6,6)}):t(function(e,t){e(null)})}},a=function(t){r.getCode(t.key).then(function(n){e(function(){n?(t.codeFormatted=n.substring(0,3)+" "+n.substring(3),t.code=n):(t.code=null,o&&clearInterval(o))})})},i=function(t){e(function(){var e=Math.round((new Date).getTime()/1e3)%30,n=30-e;t.sec=n,t.dash=(2.62*e).toFixed(2),t.low=n<=7,0===e&&a(t)})};n.$watch("key",function(){if(!n.key)return n.code=null,void(o&&clearInterval(o));a(n),i(n),o&&clearInterval(o),o=setInterval(function(){i(n)},1e3)}),n.$on("$destroy",function(){o&&clearInterval(o)}),n.clipboardError=function(e){alert("Your web browser does not support easy clipboard copying.")}}}}]),angular.module("bit.global").controller("appsController",["$scope","$state",function(e,t){}]),angular.module("bit.global").controller("mainController",["$scope","$state","authService","appSettings","toastr","$window","$document","cryptoService","$uibModal","apiService",function(e,t,n,o,r,a,i,s,l,c){var u=this;u.skinClass=o.selfHosted?"skin-blue-light":"skin-blue",u.bodyClass="",u.usingControlSidebar=u.openControlSidebar=!1,u.searchVaultText=null,u.version=o.version,u.outdatedBrowser=-1!==a.navigator.userAgent.indexOf("MSIE")||-1!==a.navigator.userAgent.indexOf("SamsungBrowser"),e.currentYear=(new Date).getFullYear(),e.$on("$viewContentLoaded",function(){n.getUserProfile().then(function(e){u.userProfile=e}),$.AdminLTE&&($.AdminLTE.layout&&($.AdminLTE.layout.fix(),$.AdminLTE.layout.fixSidebar()),$.AdminLTE.pushMenu&&$.AdminLTE.pushMenu.expandOnHover(),i.off("click",".sidebar li a"))}),e.$on("$stateChangeSuccess",function(e,t,n,o,r){u.usingEncKey=!!s.getEncKey(),u.searchVaultText=null,t.data.bodyClass?u.bodyClass=t.data.bodyClass:(u.bodyClass="",u.usingControlSidebar=!!t.data.controlSidebar,u.openControlSidebar=u.usingControlSidebar&&i.width()>768)}),e.$on("setSearchVaultText",function(e,t){u.searchVaultText=t}),e.addCipher=function(){e.$broadcast("vaultAddCipher")},e.addFolder=function(){e.$broadcast("vaultAddFolder")},e.addOrganizationCipher=function(){e.$broadcast("organizationVaultAddCipher")},e.addOrganizationCollection=function(){e.$broadcast("organizationCollectionsAdd")},e.inviteOrganizationUser=function(){e.$broadcast("organizationPeopleInvite")},e.addOrganizationGroup=function(){e.$broadcast("organizationGroupsAdd")},e.updateKey=function(){l.open({animation:!0,templateUrl:"app/settings/views/settingsUpdateKey.html",controller:"settingsUpdateKeyController"})},e.verifyEmail=function(){e.sendingVerify||(e.sendingVerify=!0,c.accounts.verifyEmail({},null).$promise.then(function(){r.success("Verification email sent."),e.sendingVerify=!1,e.verifyEmailSent=!0}).catch(function(){r.success("Verification email failed."),e.sendingVerify=!1}))},e.updateBrowser=function(){a.open("https://browser-update.org/update.html","_blank")};var d,p,m,g={scrollbarWidth:function(){if(!d){var e=$("body");e.addClass("bit-position-body-scrollbar-measure"),d=a.innerWidth-e[0].clientWidth,d=isFinite(d)?d:0,e.removeClass("bit-position-body-scrollbar-measure")}return d},scrollbarInfo:function(){return{width:g.scrollbarWidth(),visible:i.height()>$(a).height()}}};$(window).on("show.bs.dropdown",function(e){var t=m=$(e.target),n=t.data("appendTo");if(!n)return!0;p=t.find(".dropdown-menu");$(n).append(p.detach());var o=t.offset(),r={display:"block",top:o.top+t.outerHeight()-("body"!==n?$(window).scrollTop():0)};if(p.hasClass("dropdown-menu-right")){var i=g.scrollbarInfo(),s=0;i.visible&&i.width&&(s=i.width),r.right=a.innerWidth-s-(o.left+t.prop("offsetWidth"))+"px",r.left="auto"}else r.left=o.left+"px",r.right="auto";p.css(r)}),$(window).on("hide.bs.dropdown",function(e){if(!p)return!0;$(e.target).append(p.detach()),p.hide(),p=null,m=null}),e.$on("removeAppendedDropdownMenu",function(e,t){if(!p&&!m)return!0;m.append(p.detach()),p.hide(),p=null,m=null})}]),angular.module("bit.global").controller("paidOrgRequiredController",["$scope","$state","$uibModalInstance","$analytics","$uibModalStack","orgId","constants","authService",function(e,t,n,o,r,a,i,s){o.eventTrack("paidOrgRequiredController",{category:"Modal"}),s.getUserProfile().then(function(t){e.admin=t.organizations[a].type!==i.orgUserType.user}),e.go=function(){e.admin&&(o.eventTrack("Get Paid Org"),t.go("backend.org.billing",{orgId:a}).then(function(){r.dismissAll()}))},e.close=function(){n.dismiss("close")}}]),angular.module("bit.global").controller("premiumRequiredController",["$scope","$state","$uibModalInstance","$analytics","$uibModalStack",function(e,t,n,o,r){o.eventTrack("premiumRequiredController",{category:"Modal"}),e.go=function(){o.eventTrack("Get Premium"),t.go("backend.user.settingsPremium").then(function(){r.dismissAll()})},e.close=function(){n.dismiss("close")}}]),angular.module("bit.global").controller("sideNavController",["$scope","$state","authService","toastr","$analytics","constants","appSettings",function(e,t,n,o,r,a,i){e.$state=t,e.params=t.params,e.orgs=[],e.name="",i.selfHosted?(e.orgIconBgColor="#ffffff",e.orgIconBorder="3px solid #a0a0a0",e.orgIconTextColor="#333333"):(e.orgIconBgColor="#2c3b41",e.orgIconBorder="3px solid #1a2226",e.orgIconTextColor="#ffffff"),n.getUserProfile().then(function(n){if(e.name=n.extended&&n.extended.name?n.extended.name:n.email,n.organizations)if(t.includes("backend.org")&&t.params.orgId in n.organizations)e.orgProfile=n.organizations[t.params.orgId];else{var o=[];for(var r in n.organizations)n.organizations.hasOwnProperty(r)&&(n.organizations[r].enabled||n.organizations[r].type<2)&&o.push(n.organizations[r]);e.orgs=o}}),e.viewOrganization=function(e){e.type!==a.orgUserType.user?(r.eventTrack("View Organization From Side Nav"),t.go("backend.org.dashboard",{orgId:e.id})):o.error("You cannot manage this organization.")},e.searchVault=function(){t.go("backend.user.vault")},e.searchOrganizationVault=function(){t.go("backend.org.vault",{orgId:t.params.orgId})},e.isOrgOwner=function(e){return e&&e.type===a.orgUserType.owner}}]),angular.module("bit.global").controller("topNavController",["$scope",function(e){e.toggleControlSidebar=function(){var e=$("body");e.hasClass("control-sidebar-open")?e.removeClass("control-sidebar-open"):e.addClass("control-sidebar-open")}}]),angular.module("bit.filters").filter("enumLabelClass",function(){return function(e,t){if("number"!=typeof e)return e.toString();var n;switch(t){case"OrgUserStatus":switch(e){case 0:n="label-default";break;case 1:n="label-warning";break;case 2:default:n="label-success"}break;default:n="label-default"}return n}}),angular.module("bit.filters").filter("enumName",function(){return function(e,t){if("number"!=typeof e)return e.toString();var n;switch(t){case"OrgUserStatus":switch(e){case 0:n="Invited";break;case 1:n="Accepted";break;case 2:default:n="Confirmed"}break;case"OrgUserType":switch(e){case 0:n="Owner";break;case 1:n="Admin";break;case 2:default:n="User"}break;default:n=e.toString()}return n}}),angular.module("bit.tools").controller("reportsBreachController",["$scope","apiService","toastr","authService",function(e,t,n,o){e.loading=!0,e.error=!1,e.breachAccounts=[],e.email=null,e.$on("$viewContentLoaded",function(){o.getUserProfile().then(function(n){return e.email=n.email,t.hibp.get({email:e.email}).$promise}).then(function(t){for(var n=[],o=0;o0,meta:{},icon:null},i=t.Data;if(i){switch(o.name=s.decryptProperty(i.Name,n,!1,!0),o.type){case r.cipherType.login:o.subTitle=s.decryptProperty(i.Username,n,!0,!0),o.meta.password=s.decryptProperty(i.Password,n,!0,!0),o.meta.uri=s.decryptProperty(i.Uri,n,!0,!0),function(e,t,n){if(!s.disableWebsiteIcons&&t){var o=t,r=!1;if(0===o.indexOf("androidapp://")?e.icon="fa-android":0===o.indexOf("iosapp://")?e.icon="fa-apple":-1===o.indexOf("://")&&o.indexOf(".")>-1?(o="http://"+o,r=!0):r=0===o.indexOf("http")&&o.indexOf(".")>-1,n&&r)try{var i=new URL(o);e.meta.image=a.iconsUri+"/"+i.hostname+"/icon.png"}catch(e){}}e.icon||(e.icon="fa-globe")}(o,o.meta.uri,!0);break;case r.cipherType.secureNote:o.subTitle=null,o.icon="fa-sticky-note-o";break;case r.cipherType.card:o.subTitle="",o.meta.number=s.decryptProperty(i.Number,n,!0,!0);var l=s.decryptProperty(i.Brand,n,!0,!0);l&&(o.subTitle=l),o.meta.number&&o.meta.number.length>=4&&(""!==o.subTitle&&(o.subTitle+=", "),o.subTitle+="*"+o.meta.number.substr(o.meta.number.length-4)),o.icon="fa-credit-card";break;case r.cipherType.identity:var c=s.decryptProperty(i.FirstName,n,!0,!0),u=s.decryptProperty(i.LastName,n,!0,!0);o.subTitle="",c&&(o.subTitle=c),u&&(""!==o.subTitle&&(o.subTitle+=" "),o.subTitle+=u),o.icon="fa-id-card-o"}""===o.subTitle&&(o.subTitle=null)}return o};s.decryptAttachment=function(t,n){if(!n)throw"encryptedAttachment is undefined or null";return{id:n.Id,url:n.Url,fileName:e.decrypt(n.FileName,t),size:n.SizeName}},s.downloadAndDecryptAttachment=function(t,r,a){var i=n.defer(),s=new XMLHttpRequest;return s.open("GET",r.url,!0),s.responseType="arraybuffer",s.onload=function(n){s.response?e.decryptFromBytes(s.response,t).then(function(e){if(a){var t=new Blob([e]);if(o.navigator.msSaveOrOpenBlob)o.navigator.msSaveBlob(t,r.fileName);else{var n=o.document.createElement("a");n.href=o.URL.createObjectURL(t),n.download=r.fileName,o.document.body.appendChild(n),n.click(),o.document.body.removeChild(n)}}i.resolve(new Uint8Array(e))}):i.reject("No response")},s.send(null),i.promise},s.decryptFields=function(e,t){var n=[];if(t)for(var o=0;o104857600)){var a=new FileReader;return a.readAsArrayBuffer(o),a.onload=function(n){e.encryptToBytes(n.target.result,t).then(function(n){r.resolve({fileName:e.encrypt(o.name,t),data:new Uint8Array(n),size:o.size})})},a.onerror=function(e){r.reject("Error reading file.")},r.promise}r.reject("Maximum file size is 100 MB.")}},s.encryptFields=function(e,t){if(!e||!e.length)return null;for(var n=[],o=0;o2){var d=forge.util.decode64(a[2]),p=g(l+c,n.macKey,!1);if(!h(n.macKey,d,p))return console.error("MAC failed."),null}var m=forge.util.createBuffer(c),f=forge.cipher.createDecipher("AES-CBC",n.encKey);return f.start({iv:l}),f.update(m),f.finish(),"utf8"===(o=o||"utf8")?f.output.toString("utf8"):f.output.getBytes()}catch(e){throw console.error("Caught unhandled error in decrypt: "+e),e}},u.decryptFromBytes=function(e,n){try{if(!e)throw"no encBuf.";var o=new Uint8Array(e),r=o[0],a=null,i=null,s=null;switch(r){case t.encType.AesCbc128_HmacSha256_B64:case t.encType.AesCbc256_HmacSha256_B64:if(o.length<=49)return console.error("Enc type ("+r+") not valid."),null;i=w(o,1,17),s=w(o,17,49),a=w(o,49);break;case t.encType.AesCbc256_B64:if(o.length<=17)return console.error("Enc type ("+r+") not valid."),null;i=w(o,1,17),a=w(o,17);break;default:return console.error("Enc type ("+r+") not supported."),null}return function(e,t,n,o,r){if(!(r=r||u.getEncKey()||u.getKey()))throw"Encryption key unavailable.";if(e!==r.encType)throw"encType unavailable.";var a=r.getBuffers(),i=null;return p.importKey("raw",a.encKey,{name:"AES-CBC"},!1,["decrypt"]).then(function(e){if(i=e,!r.macKey||!o)return null;var s=new Uint8Array(n.byteLength+t.byteLength);return s.set(new Uint8Array(n),0),s.set(new Uint8Array(t),n.byteLength),f(s.buffer,a.macKey)}).then(function(e){return null===e?null:function(e,t,n){var o,r;return window.crypto.subtle.importKey("raw",e,{name:"HMAC",hash:{name:"SHA-256"}},!1,["sign"]).then(function(e){return r=e,window.crypto.subtle.sign({name:"HMAC",hash:{name:"SHA-256"}},r,t)}).then(function(e){return o=e,window.crypto.subtle.sign({name:"HMAC",hash:{name:"SHA-256"}},r,n)}).then(function(e){if(o.byteLength!==e.byteLength)return!1;for(var t=new Uint8Array(o),n=new Uint8Array(e),r=0;r1){var l=forge.util.decode64(a[1]),c=g(s,o.macKey,!1);if(!h(o.macKey,l,c))return console.error("MAC failed."),null}var d;if(r===t.encType.Rsa2048_OaepSha256_B64||r===t.encType.Rsa2048_OaepSha256_HmacSha256_B64)d=forge.md.sha256.create();else{if(r!==t.encType.Rsa2048_OaepSha1_B64&&r!==t.encType.Rsa2048_OaepSha1_HmacSha256_B64)throw"encType unavailable.";d=forge.md.sha1.create()}return n.decrypt(s,"RSA-OAEP",{md:d})};function g(e,t,n){var o=forge.hmac.create();o.start("sha256",t),o.update(e);var r=o.digest();return n?forge.util.encode64(r.getBytes()):r.getBytes()}function f(e,t){return p.importKey("raw",t,{name:"HMAC",hash:{name:"SHA-256"}},!1,["sign"]).then(function(t){return p.sign({name:"HMAC",hash:{name:"SHA-256"}},t,e)})}function h(e,t,n){var o=forge.hmac.create();return o.start("sha256",e),o.update(t),t=o.digest().getBytes(),o.start(null,null),o.update(n),t===(n=o.digest().getBytes())}function v(e,n,o){if(n&&(e=forge.util.decode64(e)),!e)throw"Must provide keyBytes";var r=forge.util.createBuffer(e);if(!r||0===r.length())throw"Couldn't make buffer";var a=r.length();if(null===o||void 0===o)if(32===a)o=t.encType.AesCbc256_B64;else{if(64!==a)throw"Unable to determine encType.";o=t.encType.AesCbc256_HmacSha256_B64}if(this.key=e,this.keyB64=forge.util.encode64(e),this.encType=o,o===t.encType.AesCbc256_B64&&32===a)this.encKey=e,this.macKey=null;else if(o===t.encType.AesCbc128_HmacSha256_B64&&32===a)this.encKey=r.getBytes(16),this.macKey=r.getBytes(16);else{if(o!==t.encType.AesCbc256_HmacSha256_B64||64!==a)throw"Unsupported encType/key length.";this.encKey=r.getBytes(32),this.macKey=r.getBytes(32)}}v.prototype.getBuffers=function(){if(this.keyBuf)return this.keyBuf;var e=function(e){for(var t=o.atob(e),n=new Uint8Array(t.length),r=0;r"+t+"":""+t+""}function r(e){var t=e.GroupId.substring(0,8);return'"+t+""}function a(e){var t=e.CollectionId.substring(0,8);return'"+t+""}function i(e){var t=e.OrganizationUserId.substring(0,8);return'"+t+""}return n}]),angular.module("bit.services").factory("importService",["constants",function(e){var t={};t.import=function(t,p,m,g){if(p){switch(t){case"bitwardencsv":K=p,N=m,Papa.parse(K,{header:!0,encoding:"UTF-8",complete:function(t){l(t);var n=[],o=[],r=[],a=0;angular.forEach(t.data,function(t,i){var l=n.length,c=o.length,u=t.folder&&""!==t.folder,d=u;if(u)for(a=0;ag+2&&(f.value=m[a].substr(g+2)),p.fields.push(f)}}}switch(t.type){case"login":case null:case void 0:p.type=e.cipherType.login;var h=t.login_totp||t.totp,v=t.login_uri||t.uri,y=t.login_username||t.username,b=t.login_password||t.password;p.login={totp:h&&""!==h?h:null,uri:v&&""!==v?s(v):null,username:y&&""!==y?y:null,password:b&&""!==b?b:null};break;case"note":p.type=e.cipherType.secureNote,p.secureNote={type:0}}if(o.push(p),d&&n.push({name:t.folder}),u){var w={key:c,value:l};r.push(w)}}),N(n,o,r)}});break;case"lastpass":d(p,m,g,!1);break;case"safeincloudxml":!function(t,n,o){var r=[],a=[],i=[],l=[],c=0,d=0;u(t,function(t){var u=$(t).find("database");if(u.length){var p=u.find("> label");if(p.length)for(c=0;c card");if(g.length)for(c=0;c field");for(d=0;d200?h.notes+=C+": "+b+"\n":(h.fields||(h.fields=[]),h.fields.push({name:C,value:b,type:e.fieldType.text})))}}var S=f.find("> notes");for(d=0;d label_id")).length){var k=$(p[0]).text(),T=l[k];null!==k&&""!==k&&null!==T&&i.push({key:a.length-1,value:T})}}}n(r,a,i)}else o()},o)}(p,m,g);break;case"keepass2xml":!function(t,n,o){var r=[],a=[],s=[];u(t,function(e){var t=$(e).find("Root");if(t.length){var i=t.find("> Group");i.length&&(l($(i[0]),!0,""),n(r,a,s))}else o()},o);function l(t,n,o){var c=r.length,u=o;n||(""!==u&&(u+=" > "),u+=t.find("> Name").text(),r.push({name:u}));var d=t.find("> Entry");if(d.length)for(var p=0;p String"),v=0;v Key").text(),w=y.find("> Value").text();if(""!==w)switch(b){case"URL":f.login.uri=i(w);break;case"UserName":f.login.username=w;break;case"Password":f.login.password=w;break;case"Title":f.name=w;break;case"Notes":f.notes=null===f.notes?w+"\n":f.notes+w+"\n";break;default:w.length>200||w.indexOf("\n")>-1?(f.notes||(f.notes=""),f.notes+=b+": "+w+"\n"):(f.fields||(f.fields=[]),f.fields.push({name:b,value:w,type:e.fieldType.text}))}}null===f.name&&(f.name="--"),a.push(f),n||s.push({key:g,value:c})}var C=t.find("> Group");if(C.length)for(var S=0;S "):null,l=n.length,c=o.length,u=null!==s,d=u,p=0;if(u)for(p=0;p-1||l.length>200?(null===n.notes?n.notes="":n.notes+="\n",n.notes+=c+": "+l.split("\\r\\n").join("\n").split("\\n").join("\n")):(n.fields||(n.fields=[]),n.fields.push({name:c,value:l,type:e.fieldType.text}))}}}}c(t,function(t){var o=t.split(/(?:\r\n|\r|\n)/);for(s=0;s=6){var i=n.length,l=o.length,c=t[0]&&""!==t[0],u=c,d=0;if(c)for(d=0;d6)for(d=6;d200?(p.notes||(p.notes=""),p.notes+=t[d]+": "+t[d+1]+"\n"):(p.fields||(p.fields=[]),p.fields.push({name:t[d],value:t[d+1],type:e.fieldType.text}));if(o.push(p),u&&n.push({name:t[0]}),c){var m={key:l,value:i};r.push(m)}}}),z(n,o,r)}});break;case"passworddragonxml":!function(t,n,o){var r=[],a=[],i=[],l=0;u(t,function(t){var c=$(t).find("PasswordManager");if(c.length){var u=c.find("> record");if(u.length)for(var d=0;d Account-Name"),g=m.length?$(m):null,f=p.find("> User-Id"),h=f.length?$(f):null,v=p.find("> Password"),y=v.length?$(v):null,b=p.find("> URL"),w=b.length?$(b):null,C=p.find("> Notes"),S=C.length?$(C):null,k=p.find("> Category"),T=k.length?$(k):null,P=T?T.text():null,I=r.length,E=a.length,z=P&&""!==P&&"Unfiled"!==P,O=z;if(z)for(l=0;l Attribute-"+l,l<10&&(U+=", ");var x=p.find(U);if(x.length)for(l=0;l200?(A.notes||(A.notes=""),A.notes+=F+": "+M+"\n"):(A.fields||(A.fields=[]),A.fields.push({name:F,value:M,type:e.fieldType.text})))}if(a.push(A),O&&r.push({name:P}),z){var G={key:E,value:I};i.push(G)}}n(r,a,i)}else o()},o)}(p,m,g);break;case"enpasscsv":P=p,I=m,Papa.parse(P,{encoding:"UTF-8",complete:function(t){l(t);for(var n=[],o=0;o2&&r.length%2==0)for(var c=0;c200?(i.notes||(i.notes=""),i.notes+=d+": "+u+"\n"):(i.fields||(i.fields=[]),i.fields.push({name:d,value:u,type:e.fieldType.text})):i.login.totp=u:i.login.password=u:i.login.username=u:i.login.uri=s(u)}}n.push(i)}}I([],n,[])}});break;case"pwsafexml":!function(t,n,o){var r=[],a=[],i=[],l=0;u(t,function(t){var c=$(t).find("passwordsafe");if(c.length){var u=c.attr("delimiter"),d=c.find("> entry");if(d.length)for(var p=0;p title"),f=g.length?$(g):null,h=m.find("> username"),v=h.length?$(h):null,y=m.find("> email"),b=y.length?$(y):null,w=b?b.text():null,C=m.find("> password"),S=C.length?$(C):null,k=m.find("> url"),T=k.length?$(k):null,P=m.find("> notes"),I=P.length?$(P):null,E=I?I.text().split(u).join("\n"):null,z=m.find("> group"),O=z.length?$(z):null,A=O?O.text().split(".").join(" > "):null,U=r.length,x=a.length,D=A&&""!==A,F=D;if(D)for(l=0;l Groups > Group[ID="'+t+'"]');if(o.length){n&&""!==n&&(n=" > "+n),n=o.attr("Name")+n;var r=o.attr("ParentID");return c(e,r,n)}return n}u(t,function(t){var u=$(t).find("root > Database");if(u.length){var d=u.find("> Logins > Login");if(d.length)for(var p=0;p Accounts > Account > LoginLinks > Login[SourceLoginID="'+h+'"]');if(S.length){var k=S.parent().parent();k.length&&(v=k.attr("Name"),y=k.attr("Link"),w=k.attr("ParentID"),(b=k.attr("Comments"))&&(b=b.split("/n").join("\n")))}}w&&""!==w&&(C=c(u,w,""));var T=r.length,P=a.length,I=C&&""!==C,E=I;if(I)for(l=0;l=3){var i=n.length,l=o.length,c=t[0]&&""!==t[0]&&"Unassigned"!==t[0],u=c,d=0;if(c)for(d=0;d3)for(var m=3;m200?(o.notes||(o.notes=""),o.notes+=r+": "+t[r]+"\n"):(o.fields||(o.fields=[]),o.fields.push({name:r,value:t[r],type:e.fieldType.text})))}a.push(o)}),n(r,a,[])}})}(p,m);break;case"clipperzhtml":!function(t,r,i){var l=[],u=[];c(t,function(t){var i=$(t).find("textarea"),c=i&&i.length?i.val():null,d=c?JSON.parse(c):null;if(d&&d.length)for(var p=0;p200?(g.notes||(g.notes=""),g.notes+=h.label+": "+h.value+"\n"):(g.fields||(g.fields=[]),g.fields.push({name:h.label,value:h.value,type:e.fieldType.text}))}}""===g.notes&&(g.notes=null),u.push(g)}r(l,u,[])},i)}(p,m,g);break;case"avirajson":!function(t,n,o){var r=[],a=[],s=0;c(t,function(t){var o=JSON.parse(t);if(o&&o.accounts)for(s=0;s").join("")).find("table.nobr");if(s.length)for(var c=0;c200?(p.notes||(p.notes=""),p.notes+=h+": "+v+"\n"):(p.fields||(p.fields=[]),p.fields.push({name:h,value:v,type:e.fieldType.text}))}p.notes&&""!==p.notes||(p.notes=null),p.name&&""!==p.name||(p.name="--"),u.push(p)}r(l,u,[])},s)}(p,m,g);break;case"saferpasscsv":!function(t,n,o){var r=[],a=[];Papa.parse(t,{header:!0,encoding:"UTF-8",complete:function(t){l(t),angular.forEach(t.data,function(t,n){a.push({type:e.cipherType.login,favorite:!1,notes:t.notes&&""!==t.notes?t.notes:null,name:t.url&&""!==t.url?function(e){var t=document.createElement("a");return t.href=e,t.hostname.startsWith("www.")?t.hostname.replace("www.",""):t.hostname}(t.url):"--",login:{uri:t.url&&""!==t.url?s(t.url):null,username:t.username&&""!==t.username?t.username:null,password:t.password&&""!==t.password?t.password:null}})}),n(r,a,[])}})}(p,m);break;case"ascendocsv":b=p,w=m,Papa.parse(b,{encoding:"UTF-8",complete:function(t){l(t);for(var s=[],c=0;c2&&u.length%2==0)for(var m=0;m200?(p.notes||(p.notes=""),p.notes+=f+": "+g+"\n"):(p.fields||(p.fields=[]),p.fields.push({name:f,value:g,type:e.fieldType.text})))}s.push(p)}}w([],s,[])}});break;case"passwordbossjson":!function(t,n,o){var r=[],a=[],s=0;c(t,function(t){var o=JSON.parse(t);if(o&&o.length)for(s=0;s200?(c.notes||(c.notes=""),c.notes+=u+": "+d+"\n"):(c.fields||(c.fields=[]),c.fields.push({name:u,value:d,type:e.fieldType.text}))}""===c.notes&&(c.notes=null),a.push(c)}}n(r,a,[])},o)}(p,m,g);break;case"zohovaultcsv":!function(t,n,o){function r(t,n){if(t&&""!==t)for(var o=t.split(/(?:\r\n|\r|\n)/),r=0;ri?a.substring(i+1):null;if(s&&""!==s&&l&&""!==l&&"SecretType"!==s){var c=s.toLowerCase();"user name"===c?n.login.username=l:"password"===c?n.login.password=l:l.length>200?(n.notes||(n.notes=""),n.notes+=s+": "+l+"\n"):(n.fields||(n.fields=[]),n.fields.push({name:s,value:l,type:e.fieldType.text}))}}}}Papa.parse(t,{header:!0,encoding:"UTF-8",complete:function(t){l(t);var o=[],a=[],s=[];angular.forEach(t.data,function(t,n){var l=t.ChamberName,c=o.length,u=a.length,d=l&&""!==l,p=d,m=0;if(d)for(m=0;m2&&a(2,c,b),b.name&&"--"!==b.name&&"Web Logins"!==p&&"Servers"!==p&&"Email Accounts"!==p&&(b.name=p+": "+b.name),""===b.notes&&(b.notes=null),o.push(b),h&&n.push({name:u}),f){var w={key:g,value:m};r.push(w)}}y(n,o,r)}});break;case"meldiumcsv":f=p,h=m,Papa.parse(f,{header:!0,encoding:"UTF-8",complete:function(t){l(t);for(var n=[],o=0;o30&&(m.name=m.name.substring(0,30));for(var g in p.attributes)p.attributes.hasOwnProperty(g)&&"username_value"!==g&&"xdg:schema"!==g&&(""!==m.notes&&(m.notes+="\n"),m.notes+=g+": "+p.attributes[g]);""===m.notes&&(m.notes=null),a.push(m),s.push({key:u,value:c})}}n(r,a,s)},o)}(p,m,g);break;default:g()}var f,h,v,y,b,w,C,S,k,T,P,I,E,z,O,A,U,x,D,F,M,G,K,N}else g()},t.importOrg=function(t,n,o,r){if(n){switch(t){case"bitwardencsv":a=n,i=o,Papa.parse(a,{header:!0,encoding:"UTF-8",complete:function(t){l(t);var n,o=[],r=[],a=[];angular.forEach(t.data,function(t,i){var l=r.length;if(t.collections&&""!==t.collections){var c=t.collections.split(",");for(n=0;nf+2&&(h.value=g[n].substr(f+2)),m.fields.push(h)}}}switch(t.type){case"login":case null:case void 0:m.type=e.cipherType.login;var v=t.login_totp||t.totp,y=t.login_uri||t.uri,b=t.login_username||t.username,w=t.login_password||t.password;m.login={totp:v&&""!==v?v:null,uri:y&&""!==y?s(y):null,username:b&&""!==b?b:null,password:w&&""!==w?w:null};break;case"note":m.type=e.cipherType.secureNote,m.secureNote={type:0}}r.push(m)}),i(o,r,a)}});break;case"lastpass":d(n,o,r,!0);break;default:r()}var a,i}else r()};var n=["password","pass word","passphrase","pass phrase","pass","code","code word","codeword","secret","secret word","key","keyword","key word","keyphrase","key phrase","form_pw","wppassword","pin","pwd","pw","pword","passwd","p","serial","serial#","license key","reg #","passwort"],o=["user","name","user name","username","login name","email","e-mail","id","userid","user id","login","form_loginname","wpname","mail","loginid","login id","log","first name","last name","card#","account #","member","member #","nom","benutzername"],r=["url","hyper link","hyperlink","link","host","hostname","host name","server","address","hyper ref","href","web","website","web site","site","web-site","uri","ort","adresse"];function a(e,t){if(!e||""===e)return!1;e=e.trim().toLowerCase();for(var n=0;n=0&&(e="http://"+e),s(e)}function s(e){return e.length>1e3?e.substring(0,1e3):e}function l(e){if(e.errors&&e.errors.length)for(var t=0;t-1||!a[1]||""===a[1]||("Notes"===a[0]?o.notes?o.notes+="\n"+a[1]:o.notes=a[1]:t.hasOwnProperty(a[0])?o.dataObj[t[a[0]]]=a[1]:(o.notes?o.notes+="\n":o.notes="",o.notes+=a[0]+": "+a[1]))}return o}function c(e){var t={cardholderName:e.ccname&&""!==e.ccname?e.ccname:null,number:e.ccnum&&""!==e.ccnum?e.ccnum:null,brand:e.ccnum&&""!==e.ccnum?function(e){if(!e)return null;var t=new RegExp("^4");return null!=e.match(t)?"Visa":/^(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(e)?"Mastercard":(t=new RegExp("^3[47]"),null!=e.match(t)?"Amex":(t=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)"),null!=e.match(t)?"Discover":(t=new RegExp("^36"),null!=e.match(t)?"Diners Club":(t=new RegExp("^30[0-5]"),null!=e.match(t)?"Diners Club":(t=new RegExp("^35(2[89]|[3-8][0-9])"),null!=e.match(t)?"JCB":(t=new RegExp("^(4026|417500|4508|4844|491(3|7))"),null!=e.match(t)?"Visa":null))))))}(e.ccnum):null,code:e.cccsc&&""!==e.cccsc?e.cccsc:null};if(e.ccexp&&""!==e.ccexp&&e.ccexp.indexOf("-")>-1){var n=e.ccexp.split("-");n.length>1&&(t.expYear=n[0],t.expMonth=n[1],2===t.expMonth.length&&"0"===t.expMonth[0]&&(t.expMonth=t.expMonth[1]))}return t}function u(t){var o=[],a=[],l=[],u=0;angular.forEach(t,function(t,n){var d=o.length,p=a.length,m=t.grouping&&""!==t.grouping&&"(none)"!==t.grouping,g=m;if(m)for(u=0;u1&&"NoteType"===y[0]&&("Credit Card"===y[1]||"Address"===y[1])){var b=null;"Credit Card"===y[1]?(b=i(h,{Number:"number","Name on Card":"cardholderName","Security Code":"code"},[]),f.type=e.cipherType.card,f.card=b.dataObj):"Address"===y[1]&&(b=i(h,{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"},[]),f.type=e.cipherType.identity,f.identity=b.dataObj),v=!0,f.notes=b.notes}}v||(f.secureNote={type:0},f.notes=t.extra&&""!==t.extra?t.extra:null)}else if(f.type===e.cipherType.card)f.card=c(t),f.notes=t.notes&&""!==t.notes?t.notes:null;else if(f.type===e.cipherType.identity&&(f.identity={title:t.title&&""!==t.title?t.title:null,firstName:t.firstname&&""!==t.firstname?t.firstname:null,middleName:t.middlename&&""!==t.middlename?t.middlename:null,lastName:t.lastname&&""!==t.lastname?t.lastname:null,username:t.username&&""!==t.username?t.username:null,company:t.company&&""!==t.company?t.company:null,ssn:t.ssn&&""!==t.ssn?t.ssn:null,address1:t.address1&&""!==t.address1?t.address1:null,address2:t.address2&&""!==t.address2?t.address2:null,address3:t.address3&&""!==t.address3?t.address3:null,city:t.city&&""!==t.city?t.city:null,state:t.state&&""!==t.state?t.state:null,postalCode:t.zip&&""!==t.zip?t.zip:null,country:t.country&&""!==t.country?t.country:null,email:t.email&&""!==t.email?t.email:null,phone:t.phone&&""!==t.phone?t.phone:null},f.notes=t.notes&&""!==t.notes?t.notes:null,f.identity.title&&(f.identity.title=f.identity.title.charAt(0).toUpperCase()+f.identity.title.slice(1)),t.ccnum&&""!==t.ccnum)){var w=JSON.parse(JSON.stringify(f));w.identity=null,w.type=e.cipherType.card,w.card=c(t),a.push(w)}if(a.push(f),g&&o.push({name:t.grouping}),m){var C={key:p,value:d};l.push(C)}}),n(o,a,l)}}return t}]),angular.module("bit.services").factory("passwordService",function(){var e={};e.generatePassword=function(e){var n=angular.extend({},{length:10,ambiguous:!1,number:!0,minNumber:1,uppercase:!0,minUppercase:1,lowercase:!0,minLowercase:1,special:!1,minSpecial:1},e);n.uppercase&&n.minUppercase<0&&(n.minUppercase=1),n.lowercase&&n.minLowercase<0&&(n.minLowercase=1),n.number&&n.minNumber<0&&(n.minNumber=1),n.special&&n.minSpecial<0&&(n.minSpecial=1),(!n.length||n.length<1)&&(n.length=10);var o=n.minUppercase+n.minLowercase+n.minNumber+n.minSpecial;n.length0)for(var a=0;a0)for(var i=0;i0)for(var s=0;s0)for(var l=0;l53)throw new Exception("We cannot generate numbers larger than 53 bits.");var i=Math.ceil(a/8),s=Math.pow(2,a)-1,l=new Uint8Array(i);window.crypto.getRandomValues(l);for(var c=8*(i-1),u=0;u=r?t(e,n):e+o}return e}),angular.module("bit.services").factory("tokenService",["$sessionStorage","$localStorage","jwtHelper",function(e,t,n){var o={},r=null,a=null;return o.setToken=function(t){e.accessToken=t,r=t},o.getToken=function(){return r||(r=e.accessToken),r||null},o.clearToken=function(){r=null,delete e.accessToken},o.setRefreshToken=function(t){e.refreshToken=t,a=t},o.getRefreshToken=function(){return a||(a=e.refreshToken),a||null},o.clearRefreshToken=function(){a=null,delete e.refreshToken},o.setTwoFactorToken=function(e,n){t.twoFactor||(t.twoFactor={}),t.twoFactor[n]=e},o.getTwoFactorToken=function(e){return t.twoFactor?t.twoFactor[e]:null},o.clearTwoFactorToken=function(e){e?t.twoFactor&&t.twoFactor[e]&&delete t.twoFactor[e]:delete t.twoFactor},o.clearTokens=function(){o.clearToken(),o.clearRefreshToken()},o.tokenSecondsRemaining=function(e,t){var o=n.getTokenExpirationDate(e);if(t=t||0,null===o)return 0;var r=o.valueOf()-((new Date).valueOf()+1e3*t);return Math.round(r/1e3)},o.tokenNeedsRefresh=function(e,t){t=t||5;return o.tokenSecondsRemaining(e)<60*t},o}]),angular.module("bit.services").factory("utilsService",["constants",function(e){var t,n={};n.getDeviceType=function(n){if(t)return t;var o;return t=navigator.userAgent.indexOf(" Vivaldi/")>=0?e.deviceType.vivaldi:window.chrome&&window.chrome.webstore?e.deviceType.chrome:"undefined"!=typeof InstallTrigger?e.deviceType.firefox:window.opr&&opr.addons||window.opera||navigator.userAgent.indexOf(" OPR/")>=0?e.deviceType.firefox:/constructor/i.test(window.HTMLElement)||(o=!window.safari||"undefined"!=typeof safari&&safari.pushNotification,"[object SafariRemoteNotification]"===o.toString())?e.deviceType.opera:document.documentMode?e.deviceType.ie:window.StyleMedia?e.deviceType.edge:e.deviceType.unknown};return n}]),angular.module("bit.services").factory("validationService",function(){var e={};return e.addErrors=function(t,n){var o=n.data,r="An unexpected error has occurred.";if(t.$errors=[],o&&angular.isObject(o))if(o&&o.ErrorModel&&(o=o.ErrorModel),o.ValidationErrors){for(var a in o.ValidationErrors)if(o.ValidationErrors.hasOwnProperty(a))for(var i=0;i1;var n=0;if(e.expiration=t.Expiration,t.License,e.plan={name:t.Plan,type:t.PlanType,seats:t.Seats},e.storage=null,e&&t.MaxStorageGb&&(e.storage={currentGb:t.StorageGb||0,maxGb:t.MaxStorageGb,currentName:t.StorageName||"0 GB"},e.storage.percentage=+(e.storage.currentGb/e.storage.maxGb*100).toFixed(2)),e.subscription=null,t.Subscription&&(e.subscription={trialEndDate:t.Subscription.TrialEndDate,cancelledDate:t.Subscription.CancelledDate,status:t.Subscription.Status,cancelled:t.Subscription.Cancelled,markedForCancel:!t.Subscription.Cancelled&&t.Subscription.CancelAtEndDate}),e.nextInvoice=null,t.UpcomingInvoice&&(e.nextInvoice={date:t.UpcomingInvoice.Date,amount:t.UpcomingInvoice.Amount}),t.Subscription&&t.Subscription.Items)for(e.subscription.items=[],n=0;n=s},e.submit=function(i){var s=r.encryptCollection(i,t.params.orgId);if(e.useGroups){s.groups=[];for(var l in e.selectedGroups)if(e.selectedGroups.hasOwnProperty(l))for(var c=0;c0&&(n[0].name=t.name)})},e.users=function(e){o.open({animation:!0,templateUrl:"app/organization/views/organizationCollectionsUsers.html",controller:"organizationCollectionsUsersController",size:"lg",resolve:{collection:function(){return e}}}).result.then(function(){})},e.groups=function(e){o.open({animation:!0,templateUrl:"app/organization/views/organizationCollectionsGroups.html",controller:"organizationCollectionsGroupsController",resolve:{collection:function(){return e}}}).result.then(function(){})},e.delete=function(o){confirm("Are you sure you want to delete this collection ("+o.name+")?")&&n.collections.del({orgId:t.params.orgId,id:o.id},function(){var t=e.collections.indexOf(o);t>-1&&e.collections.splice(t,1),s.eventTrack("Deleted Collection"),i.success(o.name+" has been deleted.","Collection Deleted")},function(){i.error(o.name+" was not able to be deleted.","Error")})}}]),angular.module("bit.organization").controller("organizationCollectionsEditController",["$scope","$state","$uibModalInstance","apiService","cipherService","$analytics","id","authService",function(e,t,n,o,r,a,i,s){a.eventTrack("organizationCollectionsEditController",{category:"Modal"});var l=0;e.collection={},e.groups=[],e.selectedGroups={},e.loading=!0,e.useGroups=!1,n.opened.then(function(){return o.collections.getDetails({orgId:t.params.orgId,id:i}).$promise}).then(function(t){e.collection=r.decryptCollection(t);var n={};if(t.Groups)for(var o=0;o=l},e.submit=function(s){var l=r.encryptCollection(s,t.params.orgId);if(e.useGroups){l.groups=[];for(var c in e.selectedGroups)if(e.selectedGroups.hasOwnProperty(c))for(var u=0;u-1&&e.users.splice(t,1)},function(){s.error("Unable to remove user.","Error")})},e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.organization").controller("organizationDashboardController",["$scope","authService","$state","appSettings",function(e,t,n,o){e.selfHosted=o.selfHosted,e.$on("$viewContentLoaded",function(){t.getUserProfile().then(function(t){t.organizations&&(e.orgProfile=t.organizations[n.params.orgId])})}),e.goBilling=function(){n.go("backend.org.billing",{orgId:n.params.orgId})}}]),angular.module("bit.organization").controller("organizationDeleteController",["$scope","$state","apiService","$uibModalInstance","cryptoService","authService","toastr","$analytics",function(e,t,n,o,r,a,i,s){s.eventTrack("organizationDeleteController",{category:"Modal"}),e.submit=function(){e.submitPromise=r.hashPassword(e.masterPassword).then(function(e){return n.organizations.del({id:t.params.orgId},{masterPasswordHash:e}).$promise}).then(function(){return o.dismiss("cancel"),a.removeProfileOrganization(t.params.orgId),s.eventTrack("Deleted Organization"),t.go("backend.user.vault")}).then(function(){i.success("This organization and all associated data has been deleted.","Organization Deleted")})},e.close=function(){o.dismiss("cancel")}}]),angular.module("bit.organization").controller("organizationEventsController",["$scope","$state","apiService","$uibModal","$filter","toastr","$analytics","constants","eventService","$compile","$sce",function(e,t,n,o,r,a,i,s,l,c,u){e.events=[],e.orgUsers=[],e.loading=!0,e.continuationToken=null;var d=l.getDefaultDateFilters();e.filterStart=d.start,e.filterEnd=d.end,e.$on("$viewContentLoaded",function(){n.organizationUsers.list({orgId:t.params.orgId}).$promise.then(function(t){var n=[];for(p=0;p"+r.message+"")(e);n.push({message:u.trustAsHtml(a[0].outerHTML),appIcon:r.appIcon,appName:r.appName,userId:o,userName:o?m[o]||"-":"-",date:t.Data[p].Date,ip:t.Data[p].IpAddress})}e.events&&e.events.length>0?e.events=e.events.concat(n):e.events=n,e.loading=!1});alert(r.error)}}}]),angular.module("bit.organization").controller("organizationGroupsAddController",["$scope","$state","$uibModalInstance","apiService","cipherService","$analytics",function(e,t,n,o,r,a){a.eventTrack("organizationGroupsAddController",{category:"Modal"}),e.collections=[],e.selectedCollections={},e.loading=!0,n.opened.then(function(){return o.collections.listOrganization({orgId:t.params.orgId}).$promise}).then(function(n){e.collections=r.decryptCollections(n.Data,t.params.orgId,!0),e.loading=!1}),e.toggleCollectionSelectionAll=function(t){var n={};if(t.target.checked)for(var o=0;o0&&(n[0].name=t.name)})},e.users=function(e){o.open({animation:!0,templateUrl:"app/organization/views/organizationGroupsUsers.html",controller:"organizationGroupsUsersController",size:"lg",resolve:{group:function(){return e}}}).result.then(function(){})},e.delete=function(o){confirm("Are you sure you want to delete this group ("+o.name+")?")&&n.groups.del({orgId:t.params.orgId,id:o.id},function(){var t=e.groups.indexOf(o);t>-1&&e.groups.splice(t,1),i.eventTrack("Deleted Group"),a.success(o.name+" has been deleted.","Group Deleted")},function(){a.error(o.name+" was not able to be deleted.","Error")})}}]),angular.module("bit.organization").controller("organizationGroupsEditController",["$scope","$state","$uibModalInstance","apiService","cipherService","$analytics","id",function(e,t,n,o,r,a,i){a.eventTrack("organizationGroupsEditController",{category:"Modal"}),e.collections=[],e.selectedCollections={},e.loading=!0,n.opened.then(function(){return o.groups.getDetails({orgId:t.params.orgId,id:i}).$promise}).then(function(n){e.group={id:i,name:n.Name,externalId:n.ExternalId,accessAll:n.AccessAll};var r={};if(n.Collections)for(var a=0;a-1&&e.users.splice(t,1)},function(){i.error("Unable to remove user.","Error")})},e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.organization").controller("organizationPeopleController",["$scope","$state","$uibModal","cryptoService","apiService","authService","toastr","$analytics","$filter","$uibModalStack",function(e,t,n,o,r,a,i,s,l,c){e.users=[],e.useGroups=!1,e.useEvents=!1,e.$on("$viewContentLoaded",function(){u(),a.getUserProfile().then(function(n){if(n.organizations){var o=n.organizations[t.params.orgId];e.useGroups=!!o.useGroups,e.useEvents=!!o.useEvents}})}),e.reinvite=function(e){r.organizationUsers.reinvite({orgId:t.params.orgId,id:e.id},null,function(){s.eventTrack("Reinvited User"),i.success(e.email+" has been invited again.","User Invited")},function(){i.error("Unable to invite user.","Error")})},e.delete=function(n){confirm("Are you sure you want to remove this user ("+n.email+")?")&&r.organizationUsers.del({orgId:t.params.orgId,id:n.id},null,function(){s.eventTrack("Deleted User"),i.success(n.email+" has been removed.","User Removed");var t=e.users.indexOf(n);t>-1&&e.users.splice(t,1)},function(){i.error("Unable to remove user.","Error")})},e.confirm=function(e){r.users.getPublicKey({id:e.userId},function(n){var a=o.getOrgKey(t.params.orgId);if(a){var l=o.rsaEncrypt(a.key,n.PublicKey);r.organizationUsers.confirm({orgId:t.params.orgId,id:e.id},{key:l},function(){e.status=2,s.eventTrack("Confirmed User"),i.success(e.email+" has been confirmed.","User Confirmed")},function(){i.error("Unable to confirm user.","Error")})}else i.error("Unable to confirm user.","Error")},function(){i.error("Unable to confirm user.","Error")})},e.$on("organizationPeopleInvite",function(t,n){e.invite()}),e.invite=function(){n.open({animation:!0,templateUrl:"app/organization/views/organizationPeopleInvite.html",controller:"organizationPeopleInviteController"}).result.then(function(){u()})},e.edit=function(e){n.open({animation:!0,templateUrl:"app/organization/views/organizationPeopleEdit.html",controller:"organizationPeopleEditController",resolve:{orgUser:function(){return e}}}).result.then(function(){u()})},e.groups=function(e){n.open({animation:!0,templateUrl:"app/organization/views/organizationPeopleGroups.html",controller:"organizationPeopleGroupsController",resolve:{orgUser:function(){return e}}}).result.then(function(){})},e.events=function(e){n.open({animation:!0,templateUrl:"app/organization/views/organizationPeopleEvents.html",controller:"organizationPeopleEventsController",resolve:{orgUser:function(){return e},orgId:function(){return t.params.orgId}}})};function u(){r.organizationUsers.list({orgId:t.params.orgId},function(n){for(var o=[],r=0;r"+r.message+"")(e);n.push({message:l.trustAsHtml(i[0].outerHTML),appIcon:r.appIcon,appName:r.appName,date:t.Data[o].Date,ip:t.Data[o].IpAddress})}e.events&&e.events.length>0?e.events=e.events.concat(n):e.events=n,e.loading=!1});alert(r.error)}}e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.organization").controller("organizationPeopleGroupsController",["$scope","$state","$uibModalInstance","apiService","orgUser","$analytics",function(e,t,n,o,r,a){a.eventTrack("organizationPeopleGroupsController",{category:"Modal"}),e.loading=!0,e.groups=[],e.selectedGroups={},e.orgUser=r,n.opened.then(function(){return o.groups.listOrganization({orgId:t.params.orgId}).$promise}).then(function(n){for(var a=[],i=0;i=t?e:new Array(t-e.length+1).join(n)+e}}]),angular.module("bit.organization").controller("organizationSettingsImportController",["$scope","$state","apiService","$uibModalInstance","cipherService","toastr","importService","$analytics","$sce","validationService","cryptoService",function(e,t,n,o,r,a,i,s,l,c,u){s.eventTrack("organizationSettingsImportController",{category:"Modal"}),e.model={source:""},e.source={},e.splitFeatured=!1,e.options=[{id:"bitwardencsv",name:"bitwarden (csv)",featured:!0,sort:1,instructions:l.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:!0,sort:2,instructions:l.trustAsHtml('See detailed instructions on our help site at https://help.bitwarden.com/article/import-from-lastpass/')}],e.setSource=function(){for(var t=0;t-1&&e.cipher.fields.splice(n,1)},e.clipboardSuccess=function(e){e.clearSelection(),d(e)},e.clipboardError=function(e,t){t&&d(e),alert("Your web browser does not support easy clipboard copying. Copy it manually instead.")};function d(e){var t=$(e.trigger).parent().prev();"text"===t.attr("type")&&t.select()}e.close=function(){n.dismiss("close")},e.showUpgrade=function(){c.open({animation:!0,templateUrl:"app/views/paidOrgRequired.html",controller:"paidOrgRequiredController",resolve:{orgId:function(){return l}}})}}]),angular.module("bit.organization").controller("organizationVaultAttachmentsController",["$scope","apiService","$uibModalInstance","cryptoService","cipherService","cipherId","$analytics","validationService","toastr","$timeout",function(e,t,n,o,r,a,i,s,l,c){i.eventTrack("organizationVaultAttachmentsController",{category:"Modal"}),e.cipher={},e.loading=!0,e.isPremium=!0,e.canUseAttachments=!0;var u=!1;t.ciphers.getAdmin({id:a},function(t){e.cipher=r.decryptCipher(t),e.loading=!1},function(){e.loading=!1}),e.save=function(c){var d=document.getElementById("file").files;if(d&&d.length){var p=o.getOrgKey(e.cipher.organizationId);e.savePromise=r.encryptAttachmentFile(p,d[0]).then(function(e){var n=new FormData,o=new Blob([e.data],{type:"application/octet-stream"});return n.append("data",o,e.fileName),t.ciphers.postAttachment({id:a},n).$promise}).then(function(e){i.eventTrack("Added Attachment"),l.success("The attachment has been added."),u=!0,n.close(!0)},function(e){var t=s.parseErrors(e);l.error(t.length?t[0]:"An error occurred.")})}else s.addError(c,"file","Select a file.",!0)},e.download=function(t){t.loading=!0;var n=o.getOrgKey(e.cipher.organizationId);r.downloadAndDecryptAttachment(n,t,!0).then(function(e){c(function(){t.loading=!1})},function(){c(function(){t.loading=!1})})},e.remove=function(n){confirm("Are you sure you want to delete this attachment ("+n.fileName+")?")&&(n.loading=!0,t.ciphers.delAttachment({id:a,attachmentId:n.id}).$promise.then(function(){n.loading=!1,i.eventTrack("Deleted Organization Attachment");var t=e.cipher.attachments.indexOf(n);t>-1&&e.cipher.attachments.splice(t,1)},function(){l.error("Cannot delete attachment."),n.loading=!1}))},e.close=function(){n.dismiss("cancel")},e.$on("modal.closing",function(t,o,r){u||(t.preventDefault(),u=!0,n.close(!!e.cipher.attachments&&e.cipher.attachments.length>0))})}]),angular.module("bit.organization").controller("organizationVaultCipherCollectionsController",["$scope","apiService","$uibModalInstance","cipherService","cipher","$analytics","collections",function(e,t,n,o,r,a,i){a.eventTrack("organizationVaultCipherCollectionsController",{category:"Modal"}),e.cipher={},e.collections=[],e.selectedCollections={},n.opened.then(function(){for(var t=[],n=0;n0?e.events=e.events.concat(n):e.events=n,e.loading=!1});alert(r.error)}}e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.organization").controller("organizationVaultController",["$scope","apiService","cipherService","$analytics","$q","$state","$localStorage","$uibModal","$filter","authService","$uibModalStack",function(e,t,n,o,r,a,i,s,l,c,u){e.ciphers=[],e.collections=[],e.loading=!0,e.useEvents=!1,e.$on("$viewContentLoaded",function(){c.getUserProfile().then(function(t){if(t.organizations){var n=t.organizations[a.params.orgId];e.useEvents=!!n.useEvents}});var o=t.collections.listOrganization({orgId:a.params.orgId},function(t){for(var o=([{id:null,name:"Unassigned",collapsed:i.collapsedOrgCollections&&"unassigned"in i.collapsedOrgCollections}]),r=0;r-1:null===e.id}},e.collectionSort=function(e){return e.id?e.name.toLowerCase():"î º"},e.collapseExpand=function(e){i.collapsedOrgCollections||(i.collapsedOrgCollections={});var t=e.id||"unassigned";t in i.collapsedOrgCollections?delete i.collapsedOrgCollections[t]:i.collapsedOrgCollections[t]=!0},e.editCipher=function(t){s.open({animation:!0,templateUrl:"app/vault/views/vaultEditCipher.html",controller:"organizationVaultEditCipherController",resolve:{cipherId:function(){return t.id},orgId:function(){return a.params.orgId}}}).result.then(function(n){var o;"edit"===n.action?(o=e.ciphers.indexOf(t))>-1&&(n.data.collectionIds=e.ciphers[o].collectionIds,e.ciphers[o]=n.data):"delete"===n.action&&(o=e.ciphers.indexOf(t))>-1&&e.ciphers.splice(o,1)})},e.$on("organizationVaultAddCipher",function(t,n){e.addCipher()}),e.addCipher=function(){s.open({animation:!0,templateUrl:"app/vault/views/vaultAddCipher.html",controller:"organizationVaultAddCipherController",resolve:{orgId:function(){return a.params.orgId}}}).result.then(function(t){e.ciphers.push(t)})},e.editCollections=function(t){s.open({animation:!0,templateUrl:"app/organization/views/organizationVaultCipherCollections.html",controller:"organizationVaultCipherCollectionsController",resolve:{cipher:function(){return t},collections:function(){return e.collections}}}).result.then(function(e){e.collectionIds&&(t.collectionIds=e.collectionIds)})},e.viewEvents=function(e){s.open({animation:!0,templateUrl:"app/organization/views/organizationVaultCipherEvents.html",controller:"organizationVaultCipherEventsController",resolve:{cipher:function(){return e}}})},e.attachments=function(e){c.getUserProfile().then(function(t){return!!t.organizations[e.organizationId].maxStorageGb}).then(function(t){if(t){s.open({animation:!0,templateUrl:"app/vault/views/vaultAttachments.html",controller:"organizationVaultAttachmentsController",resolve:{cipherId:function(){return e.id}}}).result.then(function(t){e.hasAttachments=t})}else s.open({animation:!0,templateUrl:"app/views/paidOrgRequired.html",controller:"paidOrgRequiredController",resolve:{orgId:function(){return e.organizationId}}})})},e.removeCipher=function(e,n){if(confirm("Are you sure you want to remove this item ("+e.name+") from the collection ("+n.name+") ?")){for(var r={collectionIds:[]},a=0;a-1&&e.ciphers.splice(t,1)})}}]),angular.module("bit.organization").controller("organizationVaultEditCipherController",["$scope","apiService","$uibModalInstance","cryptoService","cipherService","passwordService","cipherId","$analytics","orgId","$uibModal","constants",function(e,t,n,o,r,a,i,s,l,c,u){s.eventTrack("organizationVaultEditCipherController",{category:"Modal"}),e.cipher={},e.hideFolders=e.hideFavorite=e.fromOrg=!0,e.constants=u,t.ciphers.getAdmin({id:i},function(t){e.cipher=r.decryptCipher(t),e.useTotp=e.cipher.organizationUseTotp}),e.save=function(o){var a=r.encryptCipher(o,e.cipher.type);e.savePromise=t.ciphers.putAdmin({id:i},a,function(e){s.eventTrack("Edited Organization Cipher");var t=r.decryptCipherPreview(e);n.close({action:"edit",data:t})}).$promise},e.generatePassword=function(){e.cipher.login.password&&!confirm("Are you sure you want to overwrite the current password?")||(s.eventTrack("Generated Password From Edit"),e.cipher.login.password=a.generatePassword({length:14,special:!0}))},e.addField=function(){e.cipher.login.fields||(e.cipher.login.fields=[]),e.cipher.fields.push({type:u.fieldType.text.toString(),name:null,value:null})},e.removeField=function(t){var n=e.cipher.fields.indexOf(t);n>-1&&e.cipher.fields.splice(n,1)},e.clipboardSuccess=function(e){e.clearSelection(),d(e)},e.clipboardError=function(e,t){t&&d(e),alert("Your web browser does not support easy clipboard copying. Copy it manually instead.")};function d(e){var t=$(e.trigger).parent().prev();"text"===t.attr("type")&&t.select()}e.delete=function(){confirm("Are you sure you want to delete this item ("+e.cipher.name+")?")&&t.ciphers.delAdmin({id:e.cipher.id},function(){s.eventTrack("Deleted Organization Cipher From Edit"),n.close({action:"delete",data:e.cipher.id})})},e.close=function(){n.dismiss("cancel")},e.showUpgrade=function(){c.open({animation:!0,templateUrl:"app/views/paidOrgRequired.html",controller:"paidOrgRequiredController",resolve:{orgId:function(){return l}}})}}]),angular.module("bit.vault").controller("settingsAddEditEquivalentDomainController",["$scope","$uibModalInstance","$analytics","domainIndex","domains",function(e,t,n,o,r){n.eventTrack("settingsAddEditEquivalentDomainController",{category:"Modal"}),e.domains=r,e.index=o,e.submit=function(r){n.eventTrack((o?"Edited":"Added")+" Equivalent Domain"),t.close({domains:e.domains,index:o})},e.close=function(){t.dismiss("close")}}]),angular.module("bit.settings").controller("settingsBillingAdjustStorageController",["$scope","$state","$uibModalInstance","apiService","$analytics","toastr","add",function(e,t,n,o,r,a,i){r.eventTrack("settingsBillingAdjustStorageController",{category:"Modal"}),e.add=i,e.storageAdjustment=0,e.submit=function(){var t={storageGbAdjustment:e.storageAdjustment};i||(t.storageGbAdjustment*=-1),e.submitPromise=o.accounts.putStorage(null,t).$promise.then(function(t){i?(r.eventTrack("Added Storage"),a.success("You have added "+e.storageAdjustment+" GB.")):(r.eventTrack("Removed Storage"),a.success("You have removed "+e.storageAdjustment+" GB.")),n.close()})},e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.organization").controller("settingsBillingChangePaymentController",["$scope","$state","$uibModalInstance","apiService","$analytics","toastr","existingPaymentMethod","appSettings","$timeout","stripe",function(e,t,n,o,r,a,i,s,l,c){r.eventTrack("settingsBillingChangePaymentController",{category:"Modal"}),e.existingPaymentMethod=i,e.paymentMethod="card",e.dropinLoaded=!1,e.showPaymentOptions=!1,e.hideBank=!0,e.card={};var u=null;e.changePaymentMethod=function(t){e.paymentMethod=t,"paypal"===e.paymentMethod&&braintree.dropin.create({authorization:s.braintreeKey,container:"#bt-dropin-container",paymentOptionPriority:["paypal"],paypal:{flow:"vault",buttonStyle:{label:"pay",size:"medium",shape:"pill",color:"blue"}}},function(t,n){t?console.error(t):(u=n,l(function(){e.dropinLoaded=!0}))})},e.submit=function(){e.submitPromise=(t=e.card,"paypal"===e.paymentMethod?u.requestPaymentMethod().then(function(e){return e.nonce}).catch(function(e){throw e.message}):c.card.createToken(t).then(function(e){return e.id}).catch(function(e){throw e.message})).then(function(e){if(!e)throw"No payment token.";var t={paymentToken:e};return o.accounts.putPayment(null,t).$promise},function(e){throw e}).then(function(t){e.card=null,i?(r.eventTrack("Changed Payment Method"),a.success("You have changed your payment method.")):(r.eventTrack("Added Payment Method"),a.success("You have added a payment method.")),n.close()});var t},e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.settings").controller("settingsBillingController",["$scope","apiService","authService","$state","$uibModal","toastr","$analytics","appSettings",function(e,t,n,o,r,a,i,s){e.selfHosted=s.selfHosted,e.charges=[],e.paymentSource=null,e.subscription=null,e.loading=!0;var l=null;e.expiration=null,e.$on("$viewContentLoaded",function(){c()}),e.changePayment=function(){if(!e.selfHosted){r.open({animation:!0,templateUrl:"app/settings/views/settingsBillingChangePayment.html",controller:"settingsBillingChangePaymentController",resolve:{existingPaymentMethod:function(){return e.paymentSource?e.paymentSource.description:null}}}).result.then(function(){c()})}},e.adjustStorage=function(t){if(!e.selfHosted){r.open({animation:!0,templateUrl:"app/settings/views/settingsBillingAdjustStorage.html",controller:"settingsBillingAdjustStorageController",resolve:{add:function(){return t}}}).result.then(function(){c()})}},e.cancel=function(){e.selfHosted||confirm("Are you sure you want to cancel? You will lose access to all premium features at the end of this billing cycle.")&&t.accounts.putCancelPremium({},{}).$promise.then(function(e){i.eventTrack("Canceled Premium"),a.success("Premium subscription has been canceled."),c()})},e.reinstate=function(){e.selfHosted||confirm("Are you sure you want to remove the cancellation request and reinstate your premium membership?")&&t.accounts.putReinstatePremium({},{}).$promise.then(function(e){i.eventTrack("Reinstated Premium"),a.success("Premium cancellation request has been removed."),c()})},e.updateLicense=function(){if(e.selfHosted){r.open({animation:!0,templateUrl:"app/settings/views/settingsBillingUpdateLicense.html",controller:"settingsBillingUpdateLicenseController"}).result.then(function(){c()})}},e.license=function(){if(!e.selfHosted){var t=JSON.stringify(l,null,2),n=new Blob([t]);if(window.navigator.msSaveOrOpenBlob)window.navigator.msSaveBlob(n,"bitwarden_premium_license.json");else{var o=window.document.createElement("a");o.href=window.URL.createObjectURL(n,{type:"text/plain"}),o.download="bitwarden_premium_license.json",document.body.appendChild(o),o.click(),document.body.removeChild(o)}}};function c(){n.getUserProfile().then(function(n){return e.premium=n.premium,n.premium?t.accounts.getBilling({}).$promise:null}).then(function(t){if(!t)return o.go("backend.user.settingsPremium");var n=0;if(e.expiration=t.Expiration,l=t.License,e.storage=null,t&&t.MaxStorageGb&&(e.storage={currentGb:t.StorageGb||0,maxGb:t.MaxStorageGb,currentName:t.StorageName||"0 GB"},e.storage.percentage=+(e.storage.currentGb/e.storage.maxGb*100).toFixed(2)),e.subscription=null,t&&t.Subscription&&(e.subscription={trialEndDate:t.Subscription.TrialEndDate,cancelledDate:t.Subscription.CancelledDate,status:t.Subscription.Status,cancelled:t.Subscription.Cancelled,markedForCancel:!t.Subscription.Cancelled&&t.Subscription.CancelAtEndDate}),e.nextInvoice=null,t&&t.UpcomingInvoice&&(e.nextInvoice={date:t.UpcomingInvoice.Date,amount:t.UpcomingInvoice.Amount}),t&&t.Subscription&&t.Subscription.Items)for(e.subscription.items=[],n=0;n-1&&e.model.organizations.splice(n,1),r.success("You have left the organization."),c()})},function(e){r.error("Unable to leave this organization."),c()})},e.sessions=function(){n.open({animation:!0,templateUrl:"app/settings/views/settingsSessions.html",controller:"settingsSessionsController"})},e.delete=function(){n.open({animation:!0,templateUrl:"app/settings/views/settingsDelete.html",controller:"settingsDeleteController"})},e.purge=function(){n.open({animation:!0,templateUrl:"app/settings/views/settingsPurge.html",controller:"settingsPurgeController"})};function c(){$("html, body").animate({scrollTop:0},200)}}]),angular.module("bit.settings").controller("settingsCreateOrganizationController",["$scope","$state","apiService","cryptoService","toastr","$analytics","authService","constants","appSettings","validationService","stripe",function(e,t,n,o,r,a,i,s,l,c,u){e.plans=s.plans,e.storageGb=s.storageGb,e.paymentMethod="card",e.selfHosted=l.selfHosted,e.model={plan:"free",additionalSeats:0,interval:"year",ownedBusiness:!1,additionalStorageGb:null},e.totalPrice=function(){return"month"===e.model.interval?(e.model.additionalSeats||0)*(e.plans[e.model.plan].monthlySeatPrice||0)+(e.model.additionalStorageGb||0)*e.storageGb.monthlyPrice+(e.plans[e.model.plan].monthlyBasePrice||0):(e.model.additionalSeats||0)*(e.plans[e.model.plan].annualSeatPrice||0)+(e.model.additionalStorageGb||0)*e.storageGb.yearlyPrice+(e.plans[e.model.plan].annualBasePrice||0)},e.changePaymentMethod=function(t){e.paymentMethod=t},e.changedPlan=function(){e.plans[e.model.plan].hasOwnProperty("monthPlanType")&&(e.model.interval="year"),e.plans[e.model.plan].noAdditionalSeats?e.model.additionalSeats=0:e.model.additionalSeats||e.plans[e.model.plan].baseSeats||e.plans[e.model.plan].noAdditionalSeats||(e.model.additionalSeats=1)},e.changedBusiness=function(){e.model.ownedBusiness&&(e.model.plan="teams")},e.submit=function(s,l){var d=o.makeShareKey(),p=o.encrypt("Default Collection",d.key);if(e.selfHosted){var m=document.getElementById("file").files;if(!m||!m.length)return void c.addError(l,"file","Select a license file.",!0);var g=new FormData;g.append("license",m[0]),g.append("key",d.ct),g.append("collectionName",p),e.submitPromise=n.organizations.postLicense(g).$promise.then(v)}else if("free"===s.plan){var f={name:s.name,planType:s.plan,key:d.ct,billingEmail:s.billingEmail,collectionName:p};e.submitPromise=n.organizations.post(f).$promise.then(v)}else{var h=null;if("card"===e.paymentMethod)h=u.card.createToken(s.card);else{if("bank"!==e.paymentMethod)return;s.bank.currency="USD",s.bank.country="US",h=u.bankAccount.createToken(s.bank)}e.submitPromise=h.then(function(t){var o={name:s.name,planType:"month"===s.interval?e.plans[s.plan].monthPlanType:e.plans[s.plan].annualPlanType,key:d.ct,paymentToken:t.id,additionalSeats:s.additionalSeats,additionalStorageGb:s.additionalStorageGb,billingEmail:s.billingEmail,businessName:s.ownedBusiness?s.businessName:null,country:"card"===e.paymentMethod?s.card.address_country:null,collectionName:p};return n.organizations.post(o).$promise},function(e){throw e.message}).then(v)}function v(e){a.eventTrack("Created Organization"),i.addProfileOrganizationOwner(e,d.ct),i.refreshAccessToken().then(function(){y(e.Id)},function(){y(e.Id)})}function y(e){t.go("backend.org.dashboard",{orgId:e}).then(function(){r.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(e,t,n,o,r,a,i,s,l){s.eventTrack("settingsDeleteController",{category:"Modal"}),e.submit=function(c){var u;e.submitPromise=a.getUserProfile().then(function(e){return u=e,r.hashPassword(c.masterPassword)}).then(function(e){return n.accounts.postDelete({masterPasswordHash:e}).$promise}).then(function(){return o.dismiss("cancel"),a.logOut(),l.clearTwoFactorToken(u.email),s.eventTrack("Deleted Account"),t.go("frontend.login.info")}).then(function(){i.success("Your account has been closed and all associated data has been deleted.","Account Deleted")})},e.close=function(){o.dismiss("cancel")}}]),angular.module("bit.settings").controller("settingsDomainsController",["$scope","$state","apiService","toastr","$analytics","$uibModal",function(e,t,n,o,r,a){e.globalEquivalentDomains=[],e.equivalentDomains=[],n.settings.getDomains({},function(t){var n;if(t.EquivalentDomains)for(n=0;n

bitwarden two-step login recovery code:

'+e.code+'

'+new Date+"

"),t.print(),t.close()}};e.close=function(){n.close()}}]),angular.module("bit.settings").controller("settingsTwoStepU2fController",["$scope","apiService","$uibModalInstance","cryptoService","authService","toastr","$analytics","constants","$timeout","$window",function(e,t,n,o,r,a,i,s,l,c){i.eventTrack("settingsTwoStepU2fController",{category:"Modal"});var u,d=!1;e.deviceResponse=null,e.deviceListening=!1,e.deviceError=!1,l(function(){$("#masterPassword").focus()}),e.auth=function(n){e.authPromise=o.hashPassword(n.masterPassword).then(function(e){return u=e,t.twoFactor.getU2f({},{masterPasswordHash:u}).$promise}).then(function(t){return e.enabled=t.Enabled,e.challenge=t.Challenge,e.authed=!0,e.readDevice()})},e.readDevice=function(){d||e.enabled||(console.log("listening for key..."),e.deviceResponse=null,e.deviceError=!1,e.deviceListening=!0,c.u2f.register(e.challenge.AppId,[{version:e.challenge.Version,challenge:e.challenge.Challenge}],[],function(t){e.deviceListening=!1;{if(5!==t.errorCode)return t.errorCode?(l(function(){e.deviceError=!0}),void console.log("error: "+t.errorCode)):void l(function(){e.deviceResponse=JSON.stringify(t)});e.readDevice()}},10))},e.submit=function(){e.enabled?function(){if(!confirm("Are you sure you want to disable the U2F provider?"))return;e.submitPromise=t.twoFactor.disable({},{masterPasswordHash:u,type:s.twoFactorProvider.u2f},function(t){i.eventTrack("Disabled Two-step U2F"),a.success("U2F has been disabled."),e.enabled=t.Enabled,e.close()}).$promise}():e.submitPromise=t.twoFactor.putU2f({},{deviceResponse:e.deviceResponse,masterPasswordHash:u},function(t){i.eventTrack("Enabled Two-step U2F"),e.enabled=t.Enabled,e.challenge=null,e.deviceResponse=null,e.deviceError=!1}).$promise};e.close=function(){d=!0,n.close(e.enabled)},e.$on("modal.closing",function(t,n,o){d||(t.preventDefault(),e.close())})}]),angular.module("bit.settings").controller("settingsTwoStepYubiController",["$scope","apiService","$uibModalInstance","cryptoService","authService","toastr","$analytics","constants","$timeout",function(e,t,n,o,r,a,i,s,l){i.eventTrack("settingsTwoStepYubiController",{category:"Modal"});var c;l(function(){$("#masterPassword").focus()}),e.auth=function(n){var a=null;e.authPromise=o.hashPassword(n.masterPassword).then(function(e){return c=e,t.twoFactor.getYubi({},{masterPasswordHash:c}).$promise}).then(function(e){return a=e,r.getUserProfile()}).then(function(t){t,u(a),e.authed=!0})},e.remove=function(e){e.key=null,e.existingKey=null},e.submit=function(n){e.submitPromise=t.twoFactor.putYubi({},{key1:n.key1.key,key2:n.key2.key,key3:n.key3.key,nfc:n.nfc,masterPasswordHash:c},function(e){i.eventTrack("Saved Two-step YubiKey"),a.success("YubiKey saved."),u(e)}).$promise},e.disable=function(){confirm("Are you sure you want to disable the YubiKey provider?")&&(e.disableLoading=!0,e.submitPromise=t.twoFactor.disable({},{masterPasswordHash:c,type:s.twoFactorProvider.yubikey},function(t){e.disableLoading=!1,i.eventTrack("Disabled Two-step YubiKey"),a.success("YubiKey has been disabled."),e.enabled=t.Enabled,e.close()},function(t){a.error("Failed to disable."),e.disableLoading=!1}).$promise)};function u(t){e.enabled=t.Enabled,e.updateModel={key1:{key:t.Key1,existingKey:d(t.Key1,"*",44)},key2:{key:t.Key2,existingKey:d(t.Key2,"*",44)},key3:{key:t.Key3,existingKey:d(t.Key3,"*",44)},nfc:!0===t.Nfc||!t.Enabled}}function d(e,t,n){if(!e||!t||e.length>=n)return e;for(var o=(n-e.length)/t.length,r=0;r=t?e:new Array(t-e.length+1).join(n)+e}}]),angular.module("bit.tools").controller("toolsImportController",["$scope","$state","apiService","$uibModalInstance","cryptoService","cipherService","toastr","importService","$analytics","$sce","validationService",function(e,t,n,o,r,a,i,s,l,c,u){l.eventTrack("toolsImportController",{category:"Modal"}),e.model={source:""},e.source={},e.splitFeatured=!0,e.options=[{id:"bitwardencsv",name:"bitwarden (csv)",featured:!0,sort:1,instructions:c.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:!0,sort:2,instructions:c.trustAsHtml('See detailed instructions on our help site at https://help.bitwarden.com/article/import-from-lastpass/')},{id:"chromecsv",name:"Chrome (csv)",featured:!0,sort:3,instructions:c.trustAsHtml('See detailed instructions on our help site at https://help.bitwarden.com/article/import-from-chrome/')},{id:"firefoxpasswordexportercsvxml",name:"Firefox Password Exporter (xml)",featured:!0,sort:4,instructions:c.trustAsHtml('Use the Password Exporter addon for FireFox to export your passwords to a XML file. After installing the addon, type about:addons in your FireFox navigation bar. Locate the Password Exporter addon and click the "Options" button. In the dialog that pops up, click the "Export Passwords" button to save the XML file.')},{id:"keepass2xml",name:"KeePass 2 (xml)",featured:!0,sort:5,instructions:c.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:c.trustAsHtml('Using the KeePassX desktop application, navigate to "Database" > "Export to CSV file" and save the CSV file.')},{id:"dashlanecsv",name:"Dashlane (csv)",featured:!0,sort:7,instructions:c.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:!0,sort:6,instructions:c.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:c.trustAsHtml('See detailed instructions on our help site at https://help.bitwarden.com/article/import-from-1password/')},{id:"roboformhtml",name:"RoboForm (html)",instructions:c.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:c.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:c.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:c.trustAsHtml('Using the SaveInCloud desktop application, navigate to "File" > "Export" > "As XML" and save the XML file.')},{id:"pwsafexml",name:"Password Safe (xml)",instructions:c.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:c.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:c.trustAsHtml('Using the mSecure desktop application, navigate to "File" > "Export" > "CSV File..." and save the CSV file.')},{id:"truekeycsv",name:"True Key (csv)",instructions:c.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:c.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:c.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:c.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:c.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:c.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:c.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:c.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:c.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:c.trustAsHtml('Using the Universal Password Manager desktop application, navigate to "Database" > "Export" and save the CSV file.')},{id:"ascendocsv",name:"Ascendo DataVault (csv)",instructions:c.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:c.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:c.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:c.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:c.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:c.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.')}],e.setSource=function(){for(var t=0;t-1&&e.cipher.fields.splice(n,1)},e.toggleFavorite=function(){e.cipher.favorite=!e.cipher.favorite},e.clipboardSuccess=function(e){e.clearSelection(),g(e)},e.clipboardError=function(e,t){t&&g(e),alert("Your web browser does not support easy clipboard copying. Copy it manually instead.")},e.folderSort=function(e){return e.id?e.name.toLowerCase():"î º"};function g(e){var t=$(e.trigger).parent().prev();"text"===t.attr("type")&&t.select()}e.close=function(){n.dismiss("close")},e.showUpgrade=function(){d.open({animation:!0,templateUrl:"app/views/premiumRequired.html",controller:"premiumRequiredController"})}}]),angular.module("bit.vault").controller("vaultAddFolderController",["$scope","apiService","$uibModalInstance","cryptoService","cipherService","$analytics",function(e,t,n,o,r,a){a.eventTrack("vaultAddFolderController",{category:"Modal"}),e.savePromise=null,e.save=function(o){var i=r.encryptFolder(o);e.savePromise=t.folders.post(i,function(e){a.eventTrack("Created Folder");var t=r.decryptFolder(e);n.close(t)}).$promise},e.close=function(){n.dismiss("close")}}]),angular.module("bit.vault").controller("vaultAttachmentsController",["$scope","apiService","$uibModalInstance","cryptoService","cipherService","cipherId","$analytics","validationService","toastr","$timeout","authService","$uibModal",function(e,t,n,o,r,a,i,s,l,c,u,d){i.eventTrack("vaultAttachmentsController",{category:"Modal"}),e.cipher={},e.readOnly=!0,e.loading=!0,e.isPremium=!0,e.canUseAttachments=!0;var p=!1;u.getUserProfile().then(function(n){return e.isPremium=n.premium,t.ciphers.get({id:a}).$promise}).then(function(t){e.cipher=r.decryptCipher(t),e.readOnly=!e.cipher.edit,e.canUseAttachments=e.isPremium||e.cipher.organizationId,e.loading=!1},function(){e.loading=!1}),e.save=function(n){var o=document.getElementById("file"),c=o.files;c&&c.length?e.savePromise=r.encryptAttachmentFile(m(),c[0]).then(function(e){var n=new FormData,o=new Blob([e.data],{type:"application/octet-stream"});return n.append("data",o,e.fileName),t.ciphers.postAttachment({id:a},n).$promise}).then(function(t){i.eventTrack("Added Attachment"),e.cipher=r.decryptCipher(t),o.type="",o.type="file",o.value=""},function(e){var t=s.parseErrors(e);l.error(t.length?t[0]:"An error occurred.")}):s.addError(n,"file","Select a file.",!0)},e.download=function(t){if(t.loading=!0,!e.canUseAttachments)return t.loading=!1,void alert("Premium membership is required to use this feature.");r.downloadAndDecryptAttachment(m(),t,!0).then(function(e){c(function(){t.loading=!1})},function(){c(function(){t.loading=!1})})};function m(){return e.cipher.organizationId?o.getOrgKey(e.cipher.organizationId):null}e.remove=function(n){confirm("Are you sure you want to delete this attachment ("+n.fileName+")?")&&(n.loading=!0,t.ciphers.delAttachment({id:a,attachmentId:n.id}).$promise.then(function(){n.loading=!1,i.eventTrack("Deleted Attachment");var t=e.cipher.attachments.indexOf(n);t>-1&&e.cipher.attachments.splice(t,1)},function(){l.error("Cannot delete attachment."),n.loading=!1}))},e.close=function(){n.dismiss("cancel")},e.$on("modal.closing",function(t,o,r){p||(t.preventDefault(),p=!0,n.close(!!e.cipher.attachments&&e.cipher.attachments.length>0))}),e.showUpgrade=function(){d.open({animation:!0,templateUrl:"app/views/premiumRequired.html",controller:"premiumRequiredController"})}}]),angular.module("bit.vault").controller("vaultCipherCollectionsController",["$scope","apiService","$uibModalInstance","cipherService","cipherId","$analytics",function(e,t,n,o,r,a){a.eventTrack("vaultCipherCollectionsController",{category:"Modal"}),e.cipher={},e.readOnly=!1,e.loadingCipher=!0,e.loadingCollections=!0,e.selectedCollections={},e.collections=[];var i=null;n.opened.then(function(){t.ciphers.getDetails({id:r}).$promise.then(function(t){if(e.loadingCipher=!1,e.readOnly=!t.Edit,t.Edit&&t.OrganizationId){1===t.Type&&(e.cipher=o.decryptCipherPreview(t));var n={};if(t.CollectionIds)for(var r=0;r-1&&(t.sort=n)})}),d.vaultCiphers=e.ciphers=o("orderBy")(t,["sort","name","subTitle"]);var n=function(e,t){var n=[],o=0,r=e.length;for(;o0){e.ciphers=n[0];var r=200;angular.forEach(n,function(t,n){n>0&&u(function(){Array.prototype.push.apply(e.ciphers,t)},r+=200)})}}function y(){d.vaultCiphers=e.ciphers=o("orderBy")(d.vaultCiphers,["name","subTitle"])}function b(e){return e.id?e.name.toLowerCase():"î º"}e.clipboardError=function(e){alert("Your web browser does not support easy clipboard copying. Edit the item and copy it manually instead.")},e.collapseExpand=function(e,t){c.collapsedFolders||(c.collapsedFolders={});var n=t?"favorite":e.id||"none";n in c.collapsedFolders?delete c.collapsedFolders[n]:c.collapsedFolders[n]=!0},e.collapseAll=function(){if(c.collapsedFolders||(c.collapsedFolders={}),c.collapsedFolders.none=!0,c.collapsedFolders.favorite=!0,d.vaultGroupings)for(var e=0;e-1&&(d.vaultCiphers[o]=t.data),y()}else"partialEdit"===t.action?(n.folderId=t.data.folderId,n.favorite=t.data.favorite):"delete"===t.action&&k(n)})},e.$on("vaultAddCipher",function(t,n){e.addCipher()}),e.addCipher=function(e,n){t.open({animation:!0,templateUrl:"app/vault/views/vaultAddCipher.html",controller:"vaultAddCipherController",resolve:{selectedFolder:function(){return e&&e.folder?e:null},checkedFavorite:function(){return n}}}).result.then(function(e){d.vaultCiphers.push(e),y()})},e.deleteCipher=function(e){confirm("Are you sure you want to delete this item ("+e.name+")?")&&n.ciphers.del({id:e.id},function(){m.eventTrack("Deleted Item"),k(e)})},e.attachments=function(e){a.getUserProfile().then(function(t){return{isPremium:t.premium,orgUseStorage:e.organizationId&&!!t.organizations[e.organizationId].maxStorageGb}}).then(function(n){if(!e.hasAttachments){if(e.organizationId&&!n.orgUseStorage)return void t.open({animation:!0,templateUrl:"app/views/paidOrgRequired.html",controller:"paidOrgRequiredController",resolve:{orgId:function(){return e.organizationId}}});if(!e.organizationId&&!n.isPremium)return void t.open({animation:!0,templateUrl:"app/views/premiumRequired.html",controller:"premiumRequiredController"})}if(e.organizationId||r.getEncKey()){t.open({animation:!0,templateUrl:"app/vault/views/vaultAttachments.html",controller:"vaultAttachmentsController",resolve:{cipherId:function(){return e.id}}}).result.then(function(t){e.hasAttachments=t})}else i.error("You cannot use this feature until you update your encryption key.","Feature Unavailable")})},e.editFolder=function(e){t.open({animation:!0,templateUrl:"app/vault/views/vaultEditFolder.html",controller:"vaultEditFolderController",size:"sm",resolve:{folderId:function(){return e.id}}}).result.then(function(t){e.name=t.name})},e.$on("vaultAddFolder",function(t,n){e.addFolder()}),e.addFolder=function(){t.open({animation:!0,templateUrl:"app/vault/views/vaultAddFolder.html",controller:"vaultAddFolderController",size:"sm"}).result.then(function(e){e.folder=!0,d.vaultGroupings.push(e),h(d.vaultGroupings)})},e.deleteFolder=function(t){confirm("Are you sure you want to delete this folder ("+t.name+")?")&&n.folders.del({id:t.id},function(){m.eventTrack("Deleted Folder");var n=d.vaultGroupings.indexOf(t);n>-1&&(d.vaultGroupings.splice(n,1),e.folderCount--)})},e.canDeleteFolder=function(e){if(!e||!e.id||!d.vaultCiphers)return!1;var t=o("filter")(d.vaultCiphers,{folderId:e.id});return t&&0===t.length},e.share=function(e){t.open({animation:!0,templateUrl:"app/vault/views/vaultShareCipher.html",controller:"vaultShareCipherController",resolve:{cipherId:function(){return e.id}}}).result.then(function(t){e.organizationId=t})},e.editCollections=function(e){t.open({animation:!0,templateUrl:"app/vault/views/vaultCipherCollections.html",controller:"vaultCipherCollectionsController",resolve:{cipherId:function(){return e.id}}}).result.then(function(t){t.collectionIds&&!t.collectionIds.length?k(e):t.collectionIds&&(e.collectionIds=t.collectionIds)})},e.filterGrouping=function(t){e.groupingIdFilter=t.id,$.AdminLTE&&$.AdminLTE.layout&&u(function(){$.AdminLTE.layout.fix()},0)},e.filterType=function(t){e.typeFilter=t,$.AdminLTE&&$.AdminLTE.layout&&u(function(){$.AdminLTE.layout.fix()},0)},e.clearFilters=function(){e.groupingIdFilter=void 0,e.typeFilter=void 0,$.AdminLTE&&$.AdminLTE.layout&&u(function(){$.AdminLTE.layout.fix()},0)},e.groupingFilter=function(t){return void 0===e.groupingIdFilter||t.id===e.groupingIdFilter},e.cipherFilter=function(t){return function(n){var o=null===t;return!o&&t.folder&&n.folderId===t.id?o=!0:!o&&t.collection&&n.collectionIds.indexOf(t.id)>-1&&(o=!0),o&&(void 0===e.typeFilter||n.type===e.typeFilter)}},e.unselectAll=function(){S(!1)},e.selectFolder=function(e,t){$(t.currentTarget).closest(".box").find('input[name="cipherSelection"]').prop("checked",!0)},e.select=function(e){var t=$(e.currentTarget).closest("tr").find('input[name="cipherSelection"]');t.prop("checked",!t.prop("checked"))};function w(e,t,n){return n.indexOf(e)===t}function C(){return $('input[name="cipherSelection"]:checked').map(function(){return $(this).val()}).get().filter(w)}function S(e){$('input[name="cipherSelection"]').prop("checked",e)}e.bulkMove=function(){var e=C();if(0!==e.length){t.open({animation:!0,templateUrl:"app/vault/views/vaultMoveCiphers.html",controller:"vaultMoveCiphersController",size:"sm",resolve:{ids:function(){return e}}}).result.then(function(t){for(var n=0;n-1&&d.vaultCiphers.splice(n,1),(n=e.ciphers.indexOf(t))>-1&&e.ciphers.splice(n,1)}}]),angular.module("bit.vault").controller("vaultEditCipherController",["$scope","apiService","$uibModalInstance","cryptoService","cipherService","passwordService","cipherId","$analytics","$rootScope","authService","$uibModal","constants","$filter",function(e,t,n,o,r,a,i,s,l,c,u,d,p){s.eventTrack("vaultEditCipherController",{category:"Modal"}),e.folders=p("filter")(l.vaultGroupings,{folder:!0}),e.cipher={},e.readOnly=!1,e.constants=d,c.getUserProfile().then(function(n){return e.useTotp=n.premium,t.ciphers.get({id:i}).$promise}).then(function(t){e.cipher=r.decryptCipher(t),e.readOnly=!e.cipher.edit,e.useTotp=e.useTotp||e.cipher.organizationUseTotp}),e.save=function(o){if(e.readOnly)e.savePromise=t.ciphers.putPartial({id:i},{folderId:o.folderId,favorite:o.favorite},function(e){s.eventTrack("Partially Edited Cipher"),n.close({action:"partialEdit",data:{id:i,favorite:o.favorite,folderId:o.folderId&&""!==o.folderId?o.folderId:null}})}).$promise;else{var a=r.encryptCipher(o,e.cipher.type);e.savePromise=t.ciphers.put({id:i},a,function(e){s.eventTrack("Edited Cipher");var t=r.decryptCipherPreview(e);n.close({action:"edit",data:t})}).$promise}},e.generatePassword=function(){e.cipher.login.password&&!confirm("Are you sure you want to overwrite the current password?")||(s.eventTrack("Generated Password From Edit"),e.cipher.login.password=a.generatePassword({length:14,special:!0}))},e.addField=function(){e.cipher.fields||(e.cipher.fields=[]),e.cipher.fields.push({type:d.fieldType.text.toString(),name:null,value:null})},e.removeField=function(t){var n=e.cipher.fields.indexOf(t);n>-1&&e.cipher.fields.splice(n,1)},e.toggleFavorite=function(){e.cipher.favorite=!e.cipher.favorite},e.clipboardSuccess=function(e){e.clearSelection(),m(e)},e.clipboardError=function(e,t){t&&m(e),alert("Your web browser does not support easy clipboard copying. Copy it manually instead.")},e.folderSort=function(e){return e.id?e.name.toLowerCase():"î º"};function m(e){var t=$(e.trigger).parent().prev();"text"===t.attr("type")&&t.select()}e.delete=function(){confirm("Are you sure you want to delete this item ("+e.cipher.name+")?")&&t.ciphers.del({id:e.cipher.id},function(){s.eventTrack("Deleted Cipher From Edit"),n.close({action:"delete",data:e.cipher.id})})},e.close=function(){n.dismiss("cancel")},e.showUpgrade=function(){u.open({animation:!0,templateUrl:"app/views/premiumRequired.html",controller:"premiumRequiredController"})}}]),angular.module("bit.vault").controller("vaultEditFolderController",["$scope","apiService","$uibModalInstance","cryptoService","cipherService","folderId","$analytics",function(e,t,n,o,r,a,i){i.eventTrack("vaultEditFolderController",{category:"Modal"}),e.folder={},t.folders.get({id:a},function(t){e.folder=r.decryptFolder(t)}),e.savePromise=null,e.save=function(o){var s=r.encryptFolder(o);e.savePromise=t.folders.put({id:a},s,function(e){i.eventTrack("Edited Folder");var t=r.decryptFolder(e);n.close(t)}).$promise},e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.vault").controller("vaultMoveCiphersController",["$scope","apiService","$uibModalInstance","ids","$analytics","$rootScope","$filter",function(e,t,n,o,r,a,i){r.eventTrack("vaultMoveCiphersController",{category:"Modal"}),e.folders=i("filter")(a.vaultGroupings,{folder:!0}),e.count=o.length,e.save=function(){e.savePromise=t.ciphers.moveMany({ids:o,folderId:e.folderId},function(){r.eventTrack("Bulk Moved Ciphers"),n.close(e.folderId||null)}).$promise},e.folderSort=function(e){return e.id?e.name.toLowerCase():"!"},e.close=function(){n.dismiss("cancel")}}]),angular.module("bit.vault").controller("vaultShareCipherController",["$scope","apiService","$uibModalInstance","authService","cipherService","cipherId","$analytics","$state","cryptoService","$q","toastr",function(e,t,n,o,r,a,i,s,l,c,u){i.eventTrack("vaultShareCipherController",{category:"Modal"}),e.model={},e.cipher={},e.collections=[],e.selectedCollections={},e.organizations=[];var d={};e.loadingCollections=!0,e.loading=!0,e.readOnly=!1,t.ciphers.get({id:a}).$promise.then(function(t){return e.readOnly=!t.Edit,t.Edit&&(e.cipher=r.decryptCipher(t)),t.Edit}).then(function(t){if(e.loading=!1,t)return o.getUserProfile()}).then(function(n){if(n&&n.organizations){var o=[],a=!1;for(var i in n.organizations)n.organizations.hasOwnProperty(i)&&n.organizations[i].enabled&&(o.push({id:n.organizations[i].id,name:n.organizations[i].name}),d[n.organizations[i].id]=0,a||(a=!0,e.model.organizationId=n.organizations[i].id));e.organizations=o,t.collections.listMe({writeOnly:!0},function(t){for(var n=[],o=0;o -1) { + 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(''); + + + var refreshPromise; + jwtInterceptorProvider.tokenGetter = /*@ngInject*/ ["options", "tokenService", "authService", function (options, tokenService, authService) { + if (options.url.indexOf(appSettings.apiUri + '/') === -1) { + 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.user.apps', { + url: '^/apps', + templateUrl: 'app/views/apps.html', + controller: 'appsController', + data: { pageTitle: 'Get the Apps' } + }) + .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' } + }) + .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.vaultGroupings = 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 + }, + 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 + }, + { + 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 + }, + { + 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 + }, + { + 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 + }, + { + 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 + } + ], + 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) { + params = $scope.twoFactorProviders[constants.twoFactorProvider.duo]; + + $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.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.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.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.global') + + .controller('appsController', ["$scope", "$state", function ($scope, $state) { + + }]); + +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.$on('setSearchVaultText', function (event, val) { + vm.searchVaultText = val; + }); + + $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.searchVault = function () { + $state.go('backend.user.vault'); + }; + + $scope.searchOrganizationVault = function () { + $state.go('backend.org.vault', { orgId: $state.params.orgId }); + }; + + $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", function ($scope, $state, apiService, toastr, authService, $uibModal, + $analytics, appSettings) { + $scope.selfHosted = appSettings.selfHosted; + $scope.model = {}; + $scope.$on('$viewContentLoaded', function () { + apiService.organizations.get({ id: $state.params.orgId }, 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.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' + }); + }; + }]); + +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 collectionsDict = {}; + for (var 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 = decCiphers[i].login.uri; + cipher.login_username = decCiphers[i].login.username; + cipher.login_password = decCiphers[i].login.password; + cipher.login_totp = decCiphers[i].login.totp; + 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", function ($scope, apiService, $uibModalInstance, cryptoService, + cipherService, passwordService, $analytics, authService, orgId, $uibModal, constants) { + $analytics.eventTrack('organizationVaultAddCipherController', { category: 'Modal' }); + $scope.constants = constants; + $scope.selectedType = constants.cipherType.login.toString(); + $scope.cipher = { + type: constants.cipherType.login, + login: {}, + 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.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.postAttachment({ 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.delAttachment({ 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", function ($scope, apiService, cipherService, $analytics, $q, $state, + $localStorage, $uibModal, $filter, authService, $uibModalStack) { + $scope.ciphers = []; + $scope.collections = []; + $scope.loading = true; + $scope.useEvents = false; + + $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', + collapsed: $localStorage.collapsedOrgCollections && 'unassigned' in $localStorage.collapsedOrgCollections + }]; + + for (var i = 0; i < collections.Data.length; i++) { + var decCollection = cipherService.decryptCollection(collections.Data[i], null, true); + decCollection.collapsed = $localStorage.collapsedOrgCollections && + decCollection.id in $localStorage.collapsedOrgCollections; + 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; + $("#search").focus(); + + if ($state.params.search) { + $uibModalStack.dismissAll(); + $scope.$emit('setSearchVaultText', $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.filterByCollection = function (collection) { + return function (cipher) { + if (!cipher.collectionIds || !cipher.collectionIds.length) { + return collection.id === null; + } + + return cipher.collectionIds.indexOf(collection.id) > -1; + }; + }; + + $scope.collectionSort = function (item) { + if (!item.id) { + return 'î º'; + } + + return item.name.toLowerCase(); + }; + + $scope.collapseExpand = function (collection) { + if (!$localStorage.collapsedOrgCollections) { + $localStorage.collapsedOrgCollections = {}; + } + + var id = collection.id || 'unassigned'; + + if (id in $localStorage.collapsedOrgCollections) { + delete $localStorage.collapsedOrgCollections[id]; + } + else { + $localStorage.collapsedOrgCollections[id] = true; + } + }; + + $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; } + } + }); + + 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.removeCipher = function (cipher, collection) { + if (!confirm('Are you sure you want to remove this item (' + cipher.name + ') from the ' + + 'collection (' + collection.name + ') ?')) { + return; + } + + var request = { + collectionIds: [] + }; + + for (var i = 0; i < cipher.collectionIds.length; i++) { + if (cipher.collectionIds[i] !== collection.id) { + request.collectionIds.push(cipher.collectionIds[i]); + } + } + + apiService.ciphers.putCollections({ id: cipher.id }, request).$promise.then(function (response) { + $analytics.eventTrack('Removed Cipher From Collection'); + cipher.collectionIds = request.collectionIds; + }); + }; + + $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); + } + }); + }; + }]); + +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; + }); + + $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.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; } + } + }); + }; + }]); + +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.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: {} }, + listDetails: { url: _apiUri + '/ciphers/details', 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' } + }, + 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' } } + }); + + _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: {} }, + 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: {} }, + 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: {} }, + putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} }, + disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} }, + 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.vaultGroupings = $rootScope.vaultCiphers = 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, + 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, + 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, + folderId: encryptedCipher.FolderId, + favorite: encryptedCipher.Favorite, + edit: encryptedCipher.Edit, + organizationUseTotp: encryptedCipher.OrganizationUseTotp, + attachments: null, + icon: null + }; + + var cipherData = encryptedCipher.Data; + if (cipherData) { + cipher.name = cryptoService.decrypt(cipherData.Name, key); + cipher.notes = _service.decryptProperty(cipherData.Notes, key, true, false); + cipher.fields = _service.decryptFields(key, cipherData.Fields); + + var dataObj = {}; + switch (cipher.type) { + case constants.cipherType.login: + dataObj.uri = _service.decryptProperty(cipherData.Uri, key, true, false); + dataObj.username = _service.decryptProperty(cipherData.Username, key, true, false); + dataObj.password = _service.decryptProperty(cipherData.Password, key, true, false); + dataObj.totp = _service.decryptProperty(cipherData.Totp, key, true, false); + cipher.login = dataObj; + cipher.icon = 'fa-globe'; + break; + case constants.cipherType.secureNote: + dataObj.type = cipherData.Type; + cipher.secureNote = dataObj; + cipher.icon = 'fa-sticky-note-o'; + break; + case constants.cipherType.card: + dataObj.cardholderName = _service.decryptProperty(cipherData.CardholderName, key, true, false); + dataObj.number = _service.decryptProperty(cipherData.Number, key, true, false); + dataObj.brand = _service.decryptProperty(cipherData.Brand, key, true, false); + dataObj.expMonth = _service.decryptProperty(cipherData.ExpMonth, key, true, false); + dataObj.expYear = _service.decryptProperty(cipherData.ExpYear, key, true, false); + dataObj.code = _service.decryptProperty(cipherData.Code, key, true, false); + cipher.card = dataObj; + cipher.icon = 'fa-credit-card'; + break; + case constants.cipherType.identity: + dataObj.title = _service.decryptProperty(cipherData.Title, key, true, false); + dataObj.firstName = _service.decryptProperty(cipherData.FirstName, key, true, false); + dataObj.middleName = _service.decryptProperty(cipherData.MiddleName, key, true, false); + dataObj.lastName = _service.decryptProperty(cipherData.LastName, key, true, false); + dataObj.address1 = _service.decryptProperty(cipherData.Address1, key, true, false); + dataObj.address2 = _service.decryptProperty(cipherData.Address2, key, true, false); + dataObj.address3 = _service.decryptProperty(cipherData.Address3, key, true, false); + dataObj.city = _service.decryptProperty(cipherData.City, key, true, false); + dataObj.state = _service.decryptProperty(cipherData.State, key, true, false); + dataObj.postalCode = _service.decryptProperty(cipherData.PostalCode, key, true, false); + dataObj.country = _service.decryptProperty(cipherData.Country, key, true, false); + dataObj.company = _service.decryptProperty(cipherData.Company, key, true, false); + dataObj.email = _service.decryptProperty(cipherData.Email, key, true, false); + dataObj.phone = _service.decryptProperty(cipherData.Phone, key, true, false); + dataObj.ssn = _service.decryptProperty(cipherData.SSN, key, true, false); + dataObj.username = _service.decryptProperty(cipherData.Username, key, true, false); + dataObj.passportNumber = _service.decryptProperty(cipherData.PassportNumber, key, true, false); + dataObj.licenseNumber = _service.decryptProperty(cipherData.LicenseNumber, key, true, false); + cipher.identity = dataObj; + cipher.icon = 'fa-id-card-o'; + break; + default: + break; + } + } + + if (!encryptedCipher.Attachments) { + return cipher; + } + + cipher.attachments = []; + for (var 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, + folderId: encryptedCipher.FolderId, + favorite: encryptedCipher.Favorite, + edit: encryptedCipher.Edit, + organizationUseTotp: encryptedCipher.OrganizationUseTotp, + hasAttachments: !!encryptedCipher.Attachments && encryptedCipher.Attachments.length > 0, + meta: {}, + icon: null + }; + + var cipherData = encryptedCipher.Data; + if (cipherData) { + cipher.name = _service.decryptProperty(cipherData.Name, key, false, true); + + var dataObj = {}; + switch (cipher.type) { + case constants.cipherType.login: + cipher.subTitle = _service.decryptProperty(cipherData.Username, key, true, true); + cipher.meta.password = _service.decryptProperty(cipherData.Password, key, true, true); + cipher.meta.uri = _service.decryptProperty(cipherData.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(cipherData.Number, key, true, true); + var brand = _service.decryptProperty(cipherData.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(cipherData.FirstName, key, true, true); + var lastName = _service.decryptProperty(cipherData.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) + }; + + switch (cipher.type) { + case constants.cipherType.login: + var loginData = unencryptedCipher.login; + cipher.login = { + uri: encryptProperty(loginData.uri, key), + username: encryptProperty(loginData.username, key), + password: encryptProperty(loginData.password, key), + totp: encryptProperty(loginData.totp, key) + }; + 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 (var 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 (!$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 (!$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(key.macKey, 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 (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(keyBuf.macKey, 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(key.macKey, 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/ + function macsEqual(macKey, mac1, mac2) { + var hmac = forge.hmac.create(); + + hmac.start('sha256', macKey); + hmac.update(mac1); + mac1 = hmac.digest().getBytes(); + + hmac.start(null, null); + hmac.update(mac2); + mac2 = hmac.digest().getBytes(); + + return mac1 === mac2; + } + + function macsEqualWC(macKeyBuf, mac1Buf, mac2Buf) { + var mac1, + macKey; + + return window.crypto.subtle.importKey('raw', macKeyBuf, { 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); + } + + 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 'firefoxpasswordexportercsvxml': + importFirefoxPasswordExporterCsvXml(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', + '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', + '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 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.startsWith('http') && uri.indexOf('.') >= 0) { + uri = 'http://' + uri; + } + + return trimUri(uri); + } + + function trimUri(uri) { + if (uri.length > 1000) { + return uri.substring(0, 1000); + } + + return uri; + } + + 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); + } + } + + switch (value.type) { + case 'login': case null: case undefined: + cipher.type = constants.cipherType.login; + + var totp = value.login_totp || value.totp; + var uri = 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, + uri: uri && uri !== '' ? trimUri(uri) : null, + 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); + } + } + + switch (value.type) { + case 'login': case null: case undefined: + cipher.type = constants.cipherType.login; + + var totp = value.login_totp || value.totp; + var uri = 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, + uri: uri && uri !== '' ? trimUri(uri) : null, + 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 = { + uri: value.url && value.url !== '' ? trimUri(value.url) : null, + 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 (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.uri = trimUri(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: { + uri: 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.uri = trimUri(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: { + uri: 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.uri = fixUri(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: { + uri: value.URL && value.URL !== '' ? fixUri(value.URL) : null, + 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 = { + uri: item.location && item.location !== '' ? fixUri(item.location) : null, + 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: { + uri: 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.uri && property === 'urls') { + var urls = value[property].split(/(?:\r\n|\r|\n)/); + cipher.login.uri = fixUri(urls[0]); + + for (var j = 1; j < urls.length; j++) { + if (cipher.notes !== '') { + cipher.notes += '\n'; + } + + cipher.notes += ('url ' + (j + 1) + ': ' + urls[j]); + } + } + 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: { + uri: value.url && value.url !== '' ? trimUri(value.url) : null, + username: value.username && value.username !== '' ? value.username : null, + password: value.password && value.password !== '' ? value.password : null + } + }); + }); + + success(folders, ciphers, []); + } + }); + } + + function importFirefoxPasswordExporterCsvXml(file, success, error) { + var folders = [], + ciphers = []; + + function getNameFromHost(host) { + var name = '--'; + try { + if (host && host !== '') { + var parser = document.createElement('a'); + parser.href = host; + if (parser.hostname) { + name = parser.hostname; + } + } + } + catch (e) { + // do nothing + } + + return name; + } + + function parseXml(xmlDoc) { + var xml = $(xmlDoc); + + var entries = xml.find('entry'); + for (var i = 0; i < entries.length; i++) { + var entry = $(entries[i]); + if (!entry) { + continue; + } + + var host = entry.attr('host'), + user = entry.attr('user'), + password = entry.attr('password'); + + ciphers.push({ + type: constants.cipherType.login, + favorite: false, + notes: null, + name: getNameFromHost(host), + login: { + uri: host && host !== '' ? trimUri(host) : null, + username: user && user !== '' ? user : null, + password: password && password !== '' ? password : null, + } + }); + } + + success(folders, ciphers, []); + } + + if (file.type && file.type === 'text/xml') { + getXmlFileContents(file, parseXml, error); + } + else { + error('Only .xml exports are supported.'); + return; + } + } + + 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: { + uri: value[3] && value[3] !== '' ? trimUri(value[3]) : null, + 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: { + uri: value[4] && value[4] !== '' ? trimUri(value[4]) : null, + 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: { + uri: url && url.text() !== '' ? trimUri(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: { + uri: 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.uri) { + cipher.login.uri = trimUri(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: { + uri: url && url.text() !== '' ? trimUri(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: { + uri: null, + password: null, + username: null + } + }; + + if (row.length === 2) { + cipher.login.uri = fixUri(row[1]); + } + else if (row.length === 3) { + cipher.login.uri = fixUri(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.uri = fixUri(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.uri = fixUri(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.uri = fixUri(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; + } + if (cipher.login.uri === '') { + cipher.login.uri = 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: { + uri: linkText && linkText !== '' ? trimUri(linkText) : null, + 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: { + uri: null, + username: null, + password: null + } + }; + + if (value[1] === 'Web Logins') { + cipher.login.uri = value[4] && value[4] !== '' ? trimUri(value[4]) : null; + 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: { + uri: value.url && value.url !== '' ? trimUri(value.url) : null, + 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: { + uri: 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.uri = trimUri(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: { + uri: account.domain && account.domain !== '' ? fixUri(account.domain) : null, + 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: { + uri: null, + username: null, + password: null + }, + fields: null + }; + + var url = outterTable.find('.subcaption').text(); + if (url && url !== '') { + cipher.login.uri = fixUri(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) { + function urlDomain(data) { + var a = document.createElement('a'); + a.href = data; + return a.hostname.startsWith('www.') ? a.hostname.replace('www.', '') : a.hostname; + } + + 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 !== '' ? urlDomain(value.url) : '--', + login: { + uri: value.url && value.url !== '' ? trimUri(value.url) : null, + 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: { + uri: 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.uri && isField(field, _uriFieldNames)) { + cipher.login.uri = fixUri(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: { + uri: item.login_url && item.login_url !== '' ? fixUri(item.login_url) : null, + 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: { + uri: value['Secret URL'] && value['Secret URL'] !== '' ? fixUri(value['Secret URL']) : null, + 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: { + uri: null, + username: null, + password: null + } + }; + + if (type === 'Web Logins' || type === 'Servers' || type === 'Email Accounts') { + cipher.login.uri = value[4] && value[4] !== '' ? fixUri(value[4]) : null; + 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: { + uri: row.Url && row.Url !== '' ? fixUri(row.Url) : null, + 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: { + uri: !!getValue('site', value) ? fixUri(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: { + uri: fixUri(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.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 = constants.twoFactorProviderInfo; + $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; } + } + }); + + 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", function ($scope, apiService, $uibModalInstance, cryptoService, + toastr, $analytics, constants, $timeout) { + $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; + return apiService.twoFactor.getDuo({}, { + masterPasswordHash: _masterPasswordHash + }).$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; + } + + $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) { + $scope.submitPromise = apiService.twoFactor.putDuo({}, { + integrationKey: model.ikey, + secretKey: model.skey, + host: model.host, + masterPasswordHash: _masterPasswordHash + }, 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.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 + }; + + if (decCiphers[i].fields) { + for (var 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 = decCiphers[i].login.uri; + cipher.login_username = decCiphers[i].login.username; + cipher.login_password = decCiphers[i].login.password; + cipher.login_totp = decCiphers[i].login.totp; + 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: 'firefoxpasswordexportercsvxml', + name: 'Firefox Password Exporter (xml)', + featured: true, + sort: 4, + instructions: $sce.trustAsHtml('Use the ' + + '' + + 'Password Exporter addon for FireFox to export your passwords to a XML file. After installing ' + + 'the addon, type about:addons in your FireFox navigation bar. Locate the Password Exporter ' + + 'addon and click the "Options" button. In the dialog that pops up, click the "Export Passwords" button ' + + 'to save the XML 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'); + }; + }]); + +angular + .module('bit.vault') + + .controller('vaultAddCipherController', ["$scope", "apiService", "$uibModalInstance", "cryptoService", "cipherService", "passwordService", "selectedFolder", "$analytics", "checkedFavorite", "$rootScope", "authService", "$uibModal", "constants", "$filter", function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, + passwordService, selectedFolder, $analytics, checkedFavorite, $rootScope, authService, $uibModal, constants, $filter) { + $analytics.eventTrack('vaultAddCipherController', { category: 'Modal' }); + $scope.folders = $filter('filter')($rootScope.vaultGroupings, { folder: true }); + $scope.constants = constants; + $scope.selectedType = constants.cipherType.login.toString(); + $scope.cipher = { + folderId: selectedFolder ? selectedFolder.id : null, + favorite: checkedFavorite === true, + type: constants.cipherType.login, + login: {}, + 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 () { + 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.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.loading = true; + $scope.ciphers = []; + $scope.folderCount = 0; + $scope.collectionCount = 0; + $scope.firstCollectionId = null; + $scope.constants = constants; + $scope.favoriteCollapsed = $localStorage.collapsedFolders && 'favorite' in $localStorage.collapsedFolders; + $scope.groupingIdFilter = undefined; + $scope.typeFilter = undefined; + + if ($state.params.refreshFromServer) { + $rootScope.vaultGroupings = $rootScope.vaultCiphers = null; + } + + $scope.$on('$viewContentLoaded', function () { + $("#search").focus(); + + if ($rootScope.vaultGroupings && $rootScope.vaultCiphers) { + $scope.loading = false; + loadGroupingData($rootScope.vaultGroupings); + loadCipherData($rootScope.vaultCiphers); + return; + } + + loadDataFromServer(); + }); + + function loadDataFromServer() { + var decGroupings = [{ + id: null, + name: 'No Folder', + folder: true + }]; + + 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); + decCollection.collection = true; + decGroupings.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]); + decFolder.folder = true; + decGroupings.push(decFolder); + } + }).$promise; + + var groupingPromise = $q.all([collectionPromise, folderPromise]).then(function () { + loadGroupingData(decGroupings); + }); + + var cipherPromise = 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); + }); + }).$promise; + + $q.all([cipherPromise, groupingPromise]).then(function () { + $scope.loading = false; + }); + } + + function loadGroupingData(decGroupings) { + $rootScope.vaultGroupings = $filter('orderBy')(decGroupings, ['folder', groupingSort]); + var collections = $filter('filter')($rootScope.vaultGroupings, { collection: true }); + $scope.collectionCount = collections.length; + $scope.folderCount = decGroupings.length - collections.length - 1; + if (collections && collections.length) { + $scope.firstCollectionId = collections[0].id; + } + } + + function loadCipherData(decCiphers) { + angular.forEach($rootScope.vaultGroupings, function (grouping, groupingIndex) { + grouping.collapsed = $localStorage.collapsedFolders && + (grouping.id || 'none') in $localStorage.collapsedFolders; + + angular.forEach(decCiphers, function (cipherValue) { + if (cipherValue.favorite) { + cipherValue.sort = -1; + } + else if (grouping.folder && cipherValue.folderId == grouping.id) { + cipherValue.sort = groupingIndex; + } + else if (grouping.collection && cipherValue.collectionIds.indexOf(grouping.id) > -1) { + cipherValue.sort = groupingIndex; + } + }); + }); + + $rootScope.vaultCiphers = $scope.ciphers = $filter('orderBy')(decCiphers, ['sort', 'name', 'subTitle']); + + var chunks = chunk($rootScope.vaultCiphers, 400); + 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); + } + }); + } + } + + 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; + } + + function groupingSort(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.collapseExpand = function (grouping, favorite) { + if (!$localStorage.collapsedFolders) { + $localStorage.collapsedFolders = {}; + } + + var id = favorite ? 'favorite' : (grouping.id || 'none'); + if (id in $localStorage.collapsedFolders) { + delete $localStorage.collapsedFolders[id]; + } + else { + $localStorage.collapsedFolders[id] = true; + } + }; + + $scope.collapseAll = function () { + if (!$localStorage.collapsedFolders) { + $localStorage.collapsedFolders = {}; + } + + $localStorage.collapsedFolders.none = true; + $localStorage.collapsedFolders.favorite = true; + + if ($rootScope.vaultGroupings) { + for (var i = 0; i < $rootScope.vaultGroupings.length; i++) { + $localStorage.collapsedFolders[$rootScope.vaultGroupings[i].id] = true; + } + } + + $('.box').addClass('collapsed-box'); + $('.box .box-header button i.fa-minus').removeClass('fa-minus').addClass('fa-plus'); + }; + + $scope.expandAll = function () { + if ($localStorage.collapsedFolders) { + delete $localStorage.collapsedFolders; + } + + $('.box').removeClass('collapsed-box'); + $('.box-body').show(); + $('.box .box-header button i.fa-plus').removeClass('fa-plus').addClass('fa-minus'); + }; + + $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) { + $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 (grouping, favorite) { + var addModel = $uibModal.open({ + animation: true, + templateUrl: 'app/vault/views/vaultAddCipher.html', + controller: 'vaultAddCipherController', + resolve: { + selectedFolder: function () { return grouping && grouping.folder ? grouping : null; }, + checkedFavorite: function () { return favorite; } + } + }); + + 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) { + 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) { + addedFolder.folder = true; + $rootScope.vaultGroupings.push(addedFolder); + loadGroupingData($rootScope.vaultGroupings); + }); + }; + + $scope.deleteFolder = function (folder) { + if (!confirm('Are you sure you want to delete this folder (' + folder.name + ')?')) { + return; + } + + apiService.folders.del({ id: folder.id }, function () { + $analytics.eventTrack('Deleted Folder'); + var index = $rootScope.vaultGroupings.indexOf(folder); + if (index > -1) { + $rootScope.vaultGroupings.splice(index, 1); + $scope.folderCount--; + } + }); + }; + + $scope.canDeleteFolder = function (folder) { + if (!folder || !folder.id || !$rootScope.vaultCiphers) { + return false; + } + + var ciphers = $filter('filter')($rootScope.vaultCiphers, { folderId: folder.id }); + return ciphers && ciphers.length === 0; + }; + + $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 (orgId) { + cipher.organizationId = orgId; + }); + }; + + $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.filterGrouping = function (grouping) { + $scope.groupingIdFilter = grouping.id; + + if ($.AdminLTE && $.AdminLTE.layout) { + $timeout(function () { + $.AdminLTE.layout.fix(); + }, 0); + } + }; + + $scope.filterType = function (type) { + $scope.typeFilter = type; + + if ($.AdminLTE && $.AdminLTE.layout) { + $timeout(function () { + $.AdminLTE.layout.fix(); + }, 0); + } + }; + + $scope.clearFilters = function () { + $scope.groupingIdFilter = undefined; + $scope.typeFilter = undefined; + + if ($.AdminLTE && $.AdminLTE.layout) { + $timeout(function () { + $.AdminLTE.layout.fix(); + }, 0); + } + }; + + $scope.groupingFilter = function (grouping) { + return $scope.groupingIdFilter === undefined || grouping.id === $scope.groupingIdFilter; + }; + + $scope.cipherFilter = function (grouping) { + return function (cipher) { + var matchesGrouping = grouping === null; + if (!matchesGrouping && grouping.folder && cipher.folderId === grouping.id) { + matchesGrouping = true; + } + else if (!matchesGrouping && grouping.collection && cipher.collectionIds.indexOf(grouping.id) > -1) { + matchesGrouping = true; + } + + return matchesGrouping && ($scope.typeFilter === undefined || cipher.type === $scope.typeFilter); + }; + }; + + $scope.unselectAll = function () { + selectAll(false); + }; + + $scope.selectFolder = function (folder, $event) { + var checkbox = $($event.currentTarget).closest('.box').find('input[name="cipherSelection"]'); + checkbox.prop('checked', 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 = $filter('filter')($rootScope.vaultGroupings, { folder: true }); + $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; + }); + + $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.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' + }); + }; + }]); + +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 = $filter('filter')($rootScope.vaultGroupings, { folder: true }); + $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 */ + } + } + + $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); + } + } + + return apiService.ciphers.putShare({ id: cipherId }, request).$promise; + }).then(function (response) { + $analytics.eventTrack('Shared Cipher'); + toastr.success('Item has been shared.'); + $uibModalInstance.close(model.organizationId); + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + + $scope.createOrg = function () { + $state.go('backend.user.settingsCreateOrg').then(function () { + $uibModalInstance.dismiss('cancel'); + }); + }; + }]); diff --git a/js/bw.min.js b/js/bw.min.js index ff51b2c2..f71bfb0b 100644 --- a/js/bw.min.js +++ b/js/bw.min.js @@ -1 +1,1397 @@ -!function(e,t,r,n,o,a,s){e.GoogleAnalyticsObject=o,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,a=t.createElement("script"),s=t.getElementsByTagName("script")[0],a.async=1,a.src="https://www.google-analytics.com/analytics.js",s.parentNode.insertBefore(a,s)}(window,document,0,0,"ga"),ga("create","UA-81915606-3","auto");var AdminLTEOptions={controlSidebarOptions:{selector:"#adminlte-fakeselector"}};!function(e){var t=-1!==navigator.userAgent.indexOf("Firefox")||-1!==navigator.userAgent.indexOf("Gecko/"),r=!(void 0===e.u2f||!e.u2f.register);if(t&&r)e.u2f.isSupported=!0;else{var n=e.u2f||{};n.isSupported=!!(void 0!==n&&n.register||"undefined"!=typeof chrome&&chrome.runtime);var o;n.EXTENSION_ID="kmendfapggjehodndflmmgagdbamhnfd",n.MessageTypes={U2F_REGISTER_REQUEST:"u2f_register_request",U2F_REGISTER_RESPONSE:"u2f_register_response",U2F_SIGN_REQUEST:"u2f_sign_request",U2F_SIGN_RESPONSE:"u2f_sign_response",U2F_GET_API_VERSION_REQUEST:"u2f_get_api_version_request",U2F_GET_API_VERSION_RESPONSE:"u2f_get_api_version_response"},n.ErrorCodes={OK:0,OTHER_ERROR:1,BAD_REQUEST:2,CONFIGURATION_UNSUPPORTED:3,DEVICE_INELIGIBLE:4,TIMEOUT:5},n.U2fRequest,n.U2fResponse,n.Error,n.Transport,n.Transports,n.SignRequest,n.SignResponse,n.RegisterRequest,n.RegisterResponse,n.RegisteredKey,n.GetJsApiVersionResponse,n.getMessagePort=function(e){if("undefined"!=typeof chrome&&chrome.runtime){var t={type:n.MessageTypes.U2F_SIGN_REQUEST,signRequests:[]};chrome.runtime.sendMessage(n.EXTENSION_ID,t,function(){chrome.runtime.lastError?n.getIframePort_(e):n.getChromeRuntimePort_(e)})}else n.isAndroidChrome_()?n.getAuthenticatorPort_(e):n.isIosChrome_()?n.getIosPort_(e):n.getIframePort_(e)},n.isAndroidChrome_=function(){var e=navigator.userAgent;return-1!=e.indexOf("Chrome")&&-1!=e.indexOf("Android")},n.isIosChrome_=function(){return["iPhone","iPad","iPod"].indexOf(navigator.platform)>-1},n.getChromeRuntimePort_=function(e){var t=chrome.runtime.connect(n.EXTENSION_ID,{includeTlsChannelId:!0});setTimeout(function(){e(new n.WrappedChromeRuntimePort_(t))},0)},n.getAuthenticatorPort_=function(e){setTimeout(function(){e(new n.WrappedAuthenticatorPort_)},0)},n.getIosPort_=function(e){setTimeout(function(){e(new n.WrappedIosPort_)},0)},n.WrappedChromeRuntimePort_=function(e){this.port_=e},n.formatSignRequest_=function(e,t,r,a,s){if(void 0===o||o<1.1){for(var i=[],p=0;p>3)),u,m,v);if(s&&"generateKey"===e&&"RSASSA-PKCS1-v1_5"===u.name&&(!u.modulusLength||u.modulusLength>=2048))return(o=d(o)).name="RSAES-PKCS1-v1_5",delete o.hash,r.generateKey(o,!0,["encrypt","decrypt"]).then(function(e){return Promise.all([r.exportKey("jwk",e.publicKey),r.exportKey("jwk",e.privateKey)])}).then(function(e){return e[0].alg=e[1].alg=y(u),e[0].key_ops=v.filter(E),e[1].key_ops=v.filter(A),Promise.all([r.importKey("jwk",e[0],u,!0,e[0].key_ops),r.importKey("jwk",e[1],u,m,e[1].key_ops)])}).then(function(e){return{publicKey:e[0],privateKey:e[1]}});if((s||a&&"SHA-1"===(u.hash||{}).name)&&"importKey"===e&&"jwk"===o&&"HMAC"===u.name&&"oct"===i.kty)return r.importKey("raw",f(l(i.k)),p,w[3],w[4]);if(s&&"importKey"===e&&("spki"===o||"pkcs8"===o))return r.importKey("jwk",function(e){var t=_(e),r=!1;t.length>2&&(r=!0,t.shift());var n={ext:!0};switch(t[0][0]){case"1.2.840.113549.1.1.1":var o=["n","e","d","p","q","dp","dq","qi"],a=_(t[1]);r&&a.shift();for(var s=0;s2&&(n=!0,a.unshift(new Uint8Array([0]))),r[0][0]="1.2.840.113549.1.1.1",t=a;break;default:throw new TypeError("Unsupported key type")}return r.push(new Uint8Array(m(t)).buffer),n?r.unshift(new Uint8Array([0])):r[1]={tag:3,value:r[1]},new Uint8Array(m(r)).buffer}(h(e))})),u}}),["encrypt","decrypt","sign","verify"].forEach(function(e){var t=r[e];r[e]=function(n,o,s,i){if(a&&(!s.byteLength||i&&!i.byteLength))throw new Error("Empy input is not allowed");var p=[].slice.call(arguments),u=d(n);if(a&&"decrypt"===e&&"AES-GCM"===u.name){var c=n.tagLength>>3;p[2]=(s.buffer||s).slice(0,s.byteLength-c),n.tag=(s.buffer||s).slice(s.byteLength-c)}p[1]=o._key;var l;try{l=t.apply(r,p)}catch(e){return Promise.reject(e)}return a&&(l=new Promise(function(t,r){l.onabort=l.onerror=function(e){r(e)},l.oncomplete=function(r){r=r.target.result;if("encrypt"===e&&r instanceof AesGcmEncryptResult){var n=r.ciphertext,o=r.tag;(r=new Uint8Array(n.byteLength+o.byteLength)).set(new Uint8Array(n),0),r.set(new Uint8Array(o),n.byteLength),r=r.buffer}t(r)}})),l}}),a){var u=r.digest;r.digest=function(e,t){if(!t.byteLength)throw new Error("Empy input is not allowed");var n;try{n=u.call(r,e,t)}catch(e){return Promise.reject(e)}return n=new Promise(function(e,t){n.onabort=n.onerror=function(e){t(e)},n.oncomplete=function(t){e(t.target.result)}})},e.crypto=Object.create(t,{getRandomValues:{value:function(e){return t.getRandomValues(e)}},subtle:{value:r}}),e.CryptoKey=S}s&&(t.subtle=r,e.Crypto=n,e.SubtleCrypto=o,e.CryptoKey=S)}}}function c(e){return btoa(e).replace(/\=+$/,"").replace(/\+/g,"-").replace(/\//g,"_")}function l(e){return e=(e+="===").slice(0,-e.length%4),atob(e.replace(/-/g,"+").replace(/_/g,"/"))}function f(e){for(var t=new Uint8Array(e.length),r=0;re.length)throw new RangeError("Malformed DER");var r=e[t.pos++],n=e[t.pos++];if(n>=128){if(n&=127,t.end-t.pos=128){var i=n;n=4;for(t.splice(o,0,i>>24&255,i>>16&255,i>>8&255,255&i);n>1&&!(i>>24);)i<<=8,n--;n<4&&t.splice(o,4-n),n|=128}return t.splice(o-2,2,r,n),t}function S(e,t,r,n){Object.defineProperties(this,{_key:{value:e},type:{value:e.type,enumerable:!0},extractable:{value:void 0===r?e.extractable:r,enumerable:!0},algorithm:{value:void 0===t?e.algorithm:t,enumerable:!0},usages:{value:void 0===n?e.usages:n,enumerable:!0}})}function E(e){return"verify"===e||"encrypt"===e||"wrapKey"===e}function A(e){return"sign"===e||"decrypt"===e||"unwrapKey"===e}}("undefined"==typeof window?"undefined"==typeof self?this:self:window); \ No newline at end of file +(function (i, s, o, g, r, a, m) { + i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { + (i[r].q = i[r].q || []).push(arguments) + }, i[r].l = 1 * new Date(); a = s.createElement(o), + m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) +})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); + +ga('create', 'UA-81915606-3', 'auto'); + +var AdminLTEOptions = { + controlSidebarOptions: { + selector: '#adminlte-fakeselector' + } +}; + +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +// ref: https://github.com/google/u2f-ref-code/blob/master/u2f-gae-demo/war/js/u2f-api.js + +/** + * @fileoverview The U2F api. + */ +'use strict'; + +/** + * Modification: + * Wrap implementation so that we can exit if window.u2f is already supplied by the browser (see below). + */ +(function (root) { + /** + * Modification: + * Only continue load this library if window.u2f is not already supplied by the browser. + */ + var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1; + var browserImplementsU2f = !!((typeof root.u2f !== 'undefined') && root.u2f.register); + + if (isFirefox && browserImplementsU2f) { + root.u2f.isSupported = true; + return; + } + + /** + * Namespace for the U2F api. + * @type {Object} + */ + var u2f = root.u2f || {}; + + /** + * Modification: + * Check if browser supports U2F API before this wrapper was added. + */ + u2f.isSupported = !!(((typeof u2f !== 'undefined') && u2f.register) || ((typeof chrome !== 'undefined') && chrome.runtime)); + + /** + * FIDO U2F Javascript API Version + * @number + */ + var js_api_version; + + /** + * The U2F extension id + * @const {string} + */ + // The Chrome packaged app extension ID. + // Uncomment this if you want to deploy a server instance that uses + // the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; + // The U2F Chrome extension ID. + // Uncomment this if you want to deploy a server instance that uses + // the U2F Chrome extension to authenticate. + // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + + /** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ + u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' + }; + + + /** + * Response status codes + * @const + * @enum {number} + */ + u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 + }; + + + /** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ + u2f.U2fRequest; + + + /** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ + u2f.U2fResponse; + + + /** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ + u2f.Error; + + /** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ + u2f.Transport; + + + /** + * Data object for a single sign request. + * @typedef {Array} + */ + u2f.Transports; + + /** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ + u2f.SignRequest; + + + /** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ + u2f.SignResponse; + + + /** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ + u2f.RegisterRequest; + + + /** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ + u2f.RegisterResponse; + + + /** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ + u2f.RegisteredKey; + + + /** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ + u2f.GetJsApiVersionResponse; + + + //Low level MessagePort API support + + /** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ + u2f.getMessagePort = function (callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } + }; + + /** + * Detect chrome running on android based on the browser's useragent. + * @private + */ + u2f.isAndroidChrome_ = function () { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; + }; + + /** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ + u2f.isIosChrome_ = function () { + return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; + }; + + /** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ + u2f.getChromeRuntimePort_ = function (callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + { 'includeTlsChannelId': true }); + setTimeout(function () { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); + }; + + /** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ + u2f.getAuthenticatorPort_ = function (callback) { + setTimeout(function () { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); + }; + + /** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ + u2f.getIosPort_ = function (callback) { + setTimeout(function () { + callback(new u2f.WrappedIosPort_()); + }, 0); + }; + + /** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ + u2f.WrappedChromeRuntimePort_ = function (port) { + this.port_ = port; + }; + + /** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ + u2f.formatSignRequest_ = + function (appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + + /** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ + u2f.formatRegisterRequest_ = + function (appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + + + /** + * Posts a message on the underlying channel. + * @param {Object} message + */ + u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) { + this.port_.postMessage(message); + }; + + + /** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ + u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function (eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function (message) { + // Emulate a minimal MessageEvent object + handler({ 'data': message }); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + + /** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ + u2f.WrappedAuthenticatorPort_ = function () { + this.requestId_ = -1; + this.requestObject_ = null; + } + + /** + * Launch the Authenticator intent. + * @param {Object} message + */ + u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; + }; + + /** + * Tells what type of port this is. + * @return {String} port type + */ + u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () { + return "WrappedAuthenticatorPort_"; + }; + + + /** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ + u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } + }; + + /** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ + u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function (callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({ 'data': responseObject }); + }; + + /** + * Base URL for intents to Authenticator. + * @const + * @private + */ + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + + /** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ + u2f.WrappedIosPort_ = function () { }; + + /** + * Launch the iOS client app request + * @param {Object} message + */ + u2f.WrappedIosPort_.prototype.postMessage = function (message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); + }; + + /** + * Tells what type of port this is. + * @return {String} port type + */ + u2f.WrappedIosPort_.prototype.getPortType = function () { + return "WrappedIosPort_"; + }; + + /** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ + u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } + }; + + /** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ + u2f.getIframePort_ = function (callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function (message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function () { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); + }; + + + //High-level JS API + + /** + * Default extension response timeout in seconds. + * @const + */ + u2f.EXTENSION_TIMEOUT_SEC = 30; + + /** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ + u2f.port_ = null; + + /** + * Callbacks waiting for a port + * @type {Array} + * @private + */ + u2f.waitingForPort_ = []; + + /** + * A counter for requestIds. + * @type {number} + * @private + */ + u2f.reqCounter_ = 0; + + /** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ + u2f.callbackMap_ = {}; + + /** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ + u2f.getPortSingleton_ = function (callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function (port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */(u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } + }; + + /** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ + u2f.responseHandler_ = function (message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); + }; + + /** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.sign = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } + }; + + /** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.sendSignRequest = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function (port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); + }; + + /** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.register = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } + }; + + /** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array} registerRequests + * @param {Array} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.sendRegisterRequest = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function (port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); + }; + + + /** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ + u2f.getApiVersion = function (callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function (port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); + }; + + /** + * Modification: + * Assign u2f back to window (root) scope. + */ + root.u2f = u2f; +}(this)); + +/** + * @file Web Cryptography API shim + * @author Artem S Vybornov + * @license MIT + */ +!function (global) { + 'use strict'; + + // We are using an angular promise polyfill which is loaded after this script + //if (typeof Promise !== 'function') + // throw "Promise support required"; + + var _crypto = global.crypto || global.msCrypto; + if (!_crypto) return; + + var _subtle = _crypto.subtle || _crypto.webkitSubtle; + if (!_subtle) return; + + var _Crypto = global.Crypto || _crypto.constructor || Object, + _SubtleCrypto = global.SubtleCrypto || _subtle.constructor || Object, + _CryptoKey = global.CryptoKey || global.Key || Object; + + var isIE = !!global.msCrypto, + // ref PR: https://github.com/vibornoff/webcrypto-shim/pull/15 + isWebkit = !_crypto.subtle && !!_crypto.webkitSubtle; + if (!isIE && !isWebkit) return; + + // Added + global.cryptoShimmed = true; + + function s2a(s) { + return btoa(s).replace(/\=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); + } + + function a2s(s) { + s += '===', s = s.slice(0, -s.length % 4); + return atob(s.replace(/-/g, '+').replace(/_/g, '/')); + } + + function s2b(s) { + var b = new Uint8Array(s.length); + for (var i = 0; i < s.length; i++) b[i] = s.charCodeAt(i); + return b; + } + + function b2s(b) { + if (b instanceof ArrayBuffer) b = new Uint8Array(b); + return String.fromCharCode.apply(String, b); + } + + function alg(a) { + var r = { 'name': (a.name || a || '').toUpperCase().replace('V', 'v') }; + switch (r.name) { + case 'SHA-1': + case 'SHA-256': + case 'SHA-384': + case 'SHA-512': + break; + case 'AES-CBC': + case 'AES-GCM': + case 'AES-KW': + if (a.length) r['length'] = a.length; + break; + case 'HMAC': + if (a.hash) r['hash'] = alg(a.hash); + if (a.length) r['length'] = a.length; + break; + case 'RSAES-PKCS1-v1_5': + if (a.publicExponent) r['publicExponent'] = new Uint8Array(a.publicExponent); + if (a.modulusLength) r['modulusLength'] = a.modulusLength; + break; + case 'RSASSA-PKCS1-v1_5': + case 'RSA-OAEP': + if (a.hash) r['hash'] = alg(a.hash); + if (a.publicExponent) r['publicExponent'] = new Uint8Array(a.publicExponent); + if (a.modulusLength) r['modulusLength'] = a.modulusLength; + break; + default: + throw new SyntaxError("Bad algorithm name"); + } + return r; + }; + + function jwkAlg(a) { + return { + 'HMAC': { + 'SHA-1': 'HS1', + 'SHA-256': 'HS256', + 'SHA-384': 'HS384', + 'SHA-512': 'HS512', + }, + 'RSASSA-PKCS1-v1_5': { + 'SHA-1': 'RS1', + 'SHA-256': 'RS256', + 'SHA-384': 'RS384', + 'SHA-512': 'RS512', + }, + 'RSAES-PKCS1-v1_5': { + '': 'RSA1_5', + }, + 'RSA-OAEP': { + 'SHA-1': 'RSA-OAEP', + 'SHA-256': 'RSA-OAEP-256', + }, + 'AES-KW': { + '128': 'A128KW', + '192': 'A192KW', + '256': 'A256KW', + }, + 'AES-GCM': { + '128': 'A128GCM', + '192': 'A192GCM', + '256': 'A256GCM', + }, + 'AES-CBC': { + '128': 'A128CBC', + '192': 'A192CBC', + '256': 'A256CBC', + }, + }[a.name][(a.hash || {}).name || a.length || '']; + } + + function b2jwk(k) { + if (k instanceof ArrayBuffer || k instanceof Uint8Array) k = JSON.parse(decodeURIComponent(escape(b2s(k)))); + var jwk = { 'kty': k.kty, 'alg': k.alg, 'ext': k.ext || k.extractable }; + switch (jwk.kty) { + case 'oct': + jwk.k = k.k; + case 'RSA': + ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi', 'oth'].forEach(function (x) { if (x in k) jwk[x] = k[x] }); + break; + default: + throw new TypeError("Unsupported key type"); + } + return jwk; + } + + function jwk2b(k) { + var jwk = b2jwk(k); + if (isIE) jwk['extractable'] = jwk.ext, delete jwk.ext; + return s2b(unescape(encodeURIComponent(JSON.stringify(jwk)))).buffer; + } + + function pkcs2jwk(k) { + var info = b2der(k), prv = false; + if (info.length > 2) prv = true, info.shift(); // remove version from PKCS#8 PrivateKeyInfo structure + var jwk = { 'ext': true }; + switch (info[0][0]) { + case '1.2.840.113549.1.1.1': + var rsaComp = ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'], + rsaKey = b2der(info[1]); + if (prv) rsaKey.shift(); // remove version from PKCS#1 RSAPrivateKey structure + for (var i = 0; i < rsaKey.length; i++) { + if (!rsaKey[i][0]) rsaKey[i] = rsaKey[i].subarray(1); + jwk[rsaComp[i]] = s2a(b2s(rsaKey[i])); + } + jwk['kty'] = 'RSA'; + break; + default: + throw new TypeError("Unsupported key type"); + } + return jwk; + } + + function jwk2pkcs(k) { + var key, info = [['', null]], prv = false; + switch (k.kty) { + case 'RSA': + var rsaComp = ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'], + rsaKey = []; + for (var i = 0; i < rsaComp.length; i++) { + if (!(rsaComp[i] in k)) break; + var b = rsaKey[i] = s2b(a2s(k[rsaComp[i]])); + if (b[0] & 0x80) rsaKey[i] = new Uint8Array(b.length + 1), rsaKey[i].set(b, 1); + } + if (rsaKey.length > 2) prv = true, rsaKey.unshift(new Uint8Array([0])); // add version to PKCS#1 RSAPrivateKey structure + info[0][0] = '1.2.840.113549.1.1.1'; + key = rsaKey; + break; + default: + throw new TypeError("Unsupported key type"); + } + info.push(new Uint8Array(der2b(key)).buffer); + if (!prv) info[1] = { 'tag': 0x03, 'value': info[1] }; + else info.unshift(new Uint8Array([0])); // add version to PKCS#8 PrivateKeyInfo structure + return new Uint8Array(der2b(info)).buffer; + } + + var oid2str = { 'KoZIhvcNAQEB': '1.2.840.113549.1.1.1' }, + str2oid = { '1.2.840.113549.1.1.1': 'KoZIhvcNAQEB' }; + + function b2der(buf, ctx) { + if (buf instanceof ArrayBuffer) buf = new Uint8Array(buf); + if (!ctx) ctx = { pos: 0, end: buf.length }; + + if (ctx.end - ctx.pos < 2 || ctx.end > buf.length) throw new RangeError("Malformed DER"); + + var tag = buf[ctx.pos++], + len = buf[ctx.pos++]; + + if (len >= 0x80) { + len &= 0x7f; + if (ctx.end - ctx.pos < len) throw new RangeError("Malformed DER"); + for (var xlen = 0; len--;) xlen <<= 8, xlen |= buf[ctx.pos++]; + len = xlen; + } + + if (ctx.end - ctx.pos < len) throw new RangeError("Malformed DER"); + + var rv; + + switch (tag) { + case 0x02: // Universal Primitive INTEGER + rv = buf.subarray(ctx.pos, ctx.pos += len); + break; + case 0x03: // Universal Primitive BIT STRING + if (buf[ctx.pos++]) throw new Error("Unsupported bit string"); + len--; + case 0x04: // Universal Primitive OCTET STRING + rv = new Uint8Array(buf.subarray(ctx.pos, ctx.pos += len)).buffer; + break; + case 0x05: // Universal Primitive NULL + rv = null; + break; + case 0x06: // Universal Primitive OBJECT IDENTIFIER + var oid = btoa(b2s(buf.subarray(ctx.pos, ctx.pos += len))); + if (!(oid in oid2str)) throw new Error("Unsupported OBJECT ID " + oid); + rv = oid2str[oid]; + break; + case 0x30: // Universal Constructed SEQUENCE + rv = []; + for (var end = ctx.pos + len; ctx.pos < end;) rv.push(b2der(buf, ctx)); + break; + default: + throw new Error("Unsupported DER tag 0x" + tag.toString(16)); + } + + return rv; + } + + function der2b(val, buf) { + if (!buf) buf = []; + + var tag = 0, len = 0, + pos = buf.length + 2; + + buf.push(0, 0); // placeholder + + if (val instanceof Uint8Array) { // Universal Primitive INTEGER + tag = 0x02, len = val.length; + for (var i = 0; i < len; i++) buf.push(val[i]); + } + else if (val instanceof ArrayBuffer) { // Universal Primitive OCTET STRING + tag = 0x04, len = val.byteLength, val = new Uint8Array(val); + for (var i = 0; i < len; i++) buf.push(val[i]); + } + else if (val === null) { // Universal Primitive NULL + tag = 0x05, len = 0; + } + else if (typeof val === 'string' && val in str2oid) { // Universal Primitive OBJECT IDENTIFIER + var oid = s2b(atob(str2oid[val])); + tag = 0x06, len = oid.length; + for (var i = 0; i < len; i++) buf.push(oid[i]); + } + else if (val instanceof Array) { // Universal Constructed SEQUENCE + for (var i = 0; i < val.length; i++) der2b(val[i], buf); + tag = 0x30, len = buf.length - pos; + } + else if (typeof val === 'object' && val.tag === 0x03 && val.value instanceof ArrayBuffer) { // Tag hint + val = new Uint8Array(val.value), tag = 0x03, len = val.byteLength; + buf.push(0); for (var i = 0; i < len; i++) buf.push(val[i]); + len++; + } + else { + throw new Error("Unsupported DER value " + val); + } + + if (len >= 0x80) { + var xlen = len, len = 4; + buf.splice(pos, 0, (xlen >> 24) & 0xff, (xlen >> 16) & 0xff, (xlen >> 8) & 0xff, xlen & 0xff); + while (len > 1 && !(xlen >> 24)) xlen <<= 8, len--; + if (len < 4) buf.splice(pos, 4 - len); + len |= 0x80; + } + + buf.splice(pos - 2, 2, tag, len); + + return buf; + } + + function CryptoKey(key, alg, ext, use) { + Object.defineProperties(this, { + _key: { + value: key + }, + type: { + value: key.type, + enumerable: true, + }, + extractable: { + value: (ext === undefined) ? key.extractable : ext, + enumerable: true, + }, + algorithm: { + value: (alg === undefined) ? key.algorithm : alg, + enumerable: true, + }, + usages: { + value: (use === undefined) ? key.usages : use, + enumerable: true, + }, + }); + } + + function isPubKeyUse(u) { + return u === 'verify' || u === 'encrypt' || u === 'wrapKey'; + } + + function isPrvKeyUse(u) { + return u === 'sign' || u === 'decrypt' || u === 'unwrapKey'; + } + + ['generateKey', 'importKey', 'unwrapKey'] + .forEach(function (m) { + var _fn = _subtle[m]; + + _subtle[m] = function (a, b, c) { + var args = [].slice.call(arguments), + ka, kx, ku; + + switch (m) { + case 'generateKey': + ka = alg(a), kx = b, ku = c; + break; + case 'importKey': + ka = alg(c), kx = args[3], ku = args[4]; + if (a === 'jwk') { + b = b2jwk(b); + if (!b.alg) b.alg = jwkAlg(ka); + if (!b.key_ops) b.key_ops = (b.kty !== 'oct') ? ('d' in b) ? ku.filter(isPrvKeyUse) : ku.filter(isPubKeyUse) : ku.slice(); + args[1] = jwk2b(b); + } + break; + case 'unwrapKey': + ka = args[4], kx = args[5], ku = args[6]; + args[2] = c._key; + break; + } + + if (m === 'generateKey' && ka.name === 'HMAC' && ka.hash) { + ka.length = ka.length || { 'SHA-1': 512, 'SHA-256': 512, 'SHA-384': 1024, 'SHA-512': 1024 }[ka.hash.name]; + return _subtle.importKey('raw', _crypto.getRandomValues(new Uint8Array((ka.length + 7) >> 3)), ka, kx, ku); + } + + if (isWebkit && m === 'generateKey' && ka.name === 'RSASSA-PKCS1-v1_5' && (!ka.modulusLength || ka.modulusLength >= 2048)) { + a = alg(a), a.name = 'RSAES-PKCS1-v1_5', delete a.hash; + return _subtle.generateKey(a, true, ['encrypt', 'decrypt']) + .then(function (k) { + return Promise.all([ + _subtle.exportKey('jwk', k.publicKey), + _subtle.exportKey('jwk', k.privateKey), + ]); + }) + .then(function (keys) { + keys[0].alg = keys[1].alg = jwkAlg(ka); + keys[0].key_ops = ku.filter(isPubKeyUse), keys[1].key_ops = ku.filter(isPrvKeyUse); + return Promise.all([ + _subtle.importKey('jwk', keys[0], ka, true, keys[0].key_ops), + _subtle.importKey('jwk', keys[1], ka, kx, keys[1].key_ops), + ]); + }) + .then(function (keys) { + return { + publicKey: keys[0], + privateKey: keys[1], + }; + }); + } + + if ((isWebkit || (isIE && (ka.hash || {}).name === 'SHA-1')) + && m === 'importKey' && a === 'jwk' && ka.name === 'HMAC' && b.kty === 'oct') { + return _subtle.importKey('raw', s2b(a2s(b.k)), c, args[3], args[4]); + } + + if (isWebkit && m === 'importKey' && (a === 'spki' || a === 'pkcs8')) { + return _subtle.importKey('jwk', pkcs2jwk(b), c, args[3], args[4]); + } + + if (isIE && m === 'unwrapKey') { + return _subtle.decrypt(args[3], c, b) + .then(function (k) { + return _subtle.importKey(a, k, args[4], args[5], args[6]); + }); + } + + var op; + try { + op = _fn.apply(_subtle, args); + } + catch (e) { + return Promise.reject(e); + } + + if (isIE) { + op = new Promise(function (res, rej) { + op.onabort = + op.onerror = function (e) { rej(e) }; + op.oncomplete = function (r) { res(r.target.result) }; + }); + } + + op = op.then(function (k) { + if (ka.name === 'HMAC') { + if (!ka.length) ka.length = 8 * k.algorithm.length; + } + if (ka.name.search('RSA') == 0) { + if (!ka.modulusLength) ka.modulusLength = (k.publicKey || k).algorithm.modulusLength; + if (!ka.publicExponent) ka.publicExponent = (k.publicKey || k).algorithm.publicExponent; + } + if (k.publicKey && k.privateKey) { + k = { + publicKey: new CryptoKey(k.publicKey, ka, kx, ku.filter(isPubKeyUse)), + privateKey: new CryptoKey(k.privateKey, ka, kx, ku.filter(isPrvKeyUse)), + }; + } + else { + k = new CryptoKey(k, ka, kx, ku); + } + return k; + }); + + return op; + } + }); + + ['exportKey', 'wrapKey'] + .forEach(function (m) { + var _fn = _subtle[m]; + + _subtle[m] = function (a, b, c) { + var args = [].slice.call(arguments); + + switch (m) { + case 'exportKey': + args[1] = b._key; + break; + case 'wrapKey': + args[1] = b._key, args[2] = c._key; + break; + } + + if ((isWebkit || (isIE && (b.algorithm.hash || {}).name === 'SHA-1')) + && m === 'exportKey' && a === 'jwk' && b.algorithm.name === 'HMAC') { + args[0] = 'raw'; + } + + if (isWebkit && m === 'exportKey' && (a === 'spki' || a === 'pkcs8')) { + args[0] = 'jwk'; + } + + if (isIE && m === 'wrapKey') { + return _subtle.exportKey(a, b) + .then(function (k) { + if (a === 'jwk') k = s2b(unescape(encodeURIComponent(JSON.stringify(b2jwk(k))))); + return _subtle.encrypt(args[3], c, k); + }); + } + + var op; + try { + op = _fn.apply(_subtle, args); + } + catch (e) { + return Promise.reject(e); + } + + if (isIE) { + op = new Promise(function (res, rej) { + op.onabort = + op.onerror = function (e) { rej(e) }; + op.oncomplete = function (r) { res(r.target.result) }; + }); + } + + if (m === 'exportKey' && a === 'jwk') { + op = op.then(function (k) { + if ((isWebkit || (isIE && (b.algorithm.hash || {}).name === 'SHA-1')) + && b.algorithm.name === 'HMAC') { + return { 'kty': 'oct', 'alg': jwkAlg(b.algorithm), 'key_ops': b.usages.slice(), 'ext': true, 'k': s2a(b2s(k)) }; + } + k = b2jwk(k); + if (!k.alg) k['alg'] = jwkAlg(b.algorithm); + if (!k.key_ops) k['key_ops'] = (b.type === 'public') ? b.usages.filter(isPubKeyUse) : (b.type === 'private') ? b.usages.filter(isPrvKeyUse) : b.usages.slice(); + return k; + }); + } + + if (isWebkit && m === 'exportKey' && (a === 'spki' || a === 'pkcs8')) { + op = op.then(function (k) { + k = jwk2pkcs(b2jwk(k)); + return k; + }); + } + + return op; + } + }); + + ['encrypt', 'decrypt', 'sign', 'verify'] + .forEach(function (m) { + var _fn = _subtle[m]; + + _subtle[m] = function (a, b, c, d) { + if (isIE && (!c.byteLength || (d && !d.byteLength))) + throw new Error("Empy input is not allowed"); + + var args = [].slice.call(arguments), + ka = alg(a); + + if (isIE && m === 'decrypt' && ka.name === 'AES-GCM') { + var tl = a.tagLength >> 3; + args[2] = (c.buffer || c).slice(0, c.byteLength - tl), + a.tag = (c.buffer || c).slice(c.byteLength - tl); + } + + args[1] = b._key; + + var op; + try { + op = _fn.apply(_subtle, args); + } + catch (e) { + return Promise.reject(e); + } + + if (isIE) { + op = new Promise(function (res, rej) { + op.onabort = + op.onerror = function (e) { + rej(e); + }; + + op.oncomplete = function (r) { + var r = r.target.result; + + if (m === 'encrypt' && r instanceof AesGcmEncryptResult) { + var c = r.ciphertext, t = r.tag; + r = new Uint8Array(c.byteLength + t.byteLength); + r.set(new Uint8Array(c), 0); + r.set(new Uint8Array(t), c.byteLength); + r = r.buffer; + } + + res(r); + }; + }); + } + + return op; + } + }); + + if (isIE) { + var _digest = _subtle.digest; + + _subtle['digest'] = function (a, b) { + if (!b.byteLength) + throw new Error("Empy input is not allowed"); + + var op; + try { + op = _digest.call(_subtle, a, b); + } + catch (e) { + return Promise.reject(e); + } + + op = new Promise(function (res, rej) { + op.onabort = + op.onerror = function (e) { rej(e) }; + op.oncomplete = function (r) { res(r.target.result) }; + }); + + return op; + }; + + global.crypto = Object.create(_crypto, { + getRandomValues: { value: function (a) { return _crypto.getRandomValues(a) } }, + subtle: { value: _subtle }, + }); + + global.CryptoKey = CryptoKey; + } + + if (isWebkit) { + _crypto.subtle = _subtle; + + global.Crypto = _Crypto; + global.SubtleCrypto = _SubtleCrypto; + global.CryptoKey = CryptoKey; + } +}(typeof window === 'undefined' ? typeof self === 'undefined' ? this : self : window); diff --git a/js/fallback-scripts.min.js b/js/fallback-scripts.min.js index 09fa53f3..2760de64 100644 --- a/js/fallback-scripts.min.js +++ b/js/fallback-scripts.min.js @@ -1 +1,9 @@ -function loadScriptIfMissing(i,n){i||document.write(' +