1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

Compare commits

..

112 Commits

Author SHA1 Message Date
Kyle Spearrin
3ed69d887f utf8 encode params for key derivation 2017-02-15 19:03:56 -05:00
Kyle Spearrin
f0d440d204 Move tools from side nav into tools page boxes 2017-02-14 00:41:23 -05:00
Kyle Spearrin
3e18f812db lint errors 2017-02-11 18:22:13 -05:00
Kyle Spearrin
8cf02fd59a version bump 2017-02-11 17:12:50 -05:00
Kyle Spearrin
06bfab3afa version bump 2017-02-11 17:12:09 -05:00
Kyle Spearrin
71e4697562 two factor edits 2017-02-11 17:08:06 -05:00
Kyle Spearrin
cf1bffe2f1 change email button 2017-02-11 16:48:52 -05:00
Kyle Spearrin
55a5fd49dc Moved domain rules page out from modal into it's own page 2017-02-11 16:46:24 -05:00
Kyle Spearrin
3f6637eb8f move many account settings into main settings page instead of nav menu 2017-02-11 15:44:22 -05:00
Kyle Spearrin
7373e281ac print recovery code. changed vault and login route 2017-02-11 14:21:21 -05:00
Kyle Spearrin
52b89455d7 replace sjcl cryptoservice implementation with forge 2017-02-11 13:03:48 -05:00
Kyle Spearrin
bca7592c77 production by default settings 2017-02-01 22:56:39 -05:00
Kyle Spearrin
f6ab0bfe82 version bump 2017-02-01 22:53:38 -05:00
Kyle Spearrin
012a5c491d version bump 2017-02-01 22:53:15 -05:00
Kyle Spearrin
7666d6136d updated truekey importer to their new csv format 2017-02-01 22:52:36 -05:00
Kyle Spearrin
7bdda34f14 remove old auth endpoints from apiservice 2017-01-29 21:39:38 -05:00
Kyle Spearrin
df21f89fcb lint fix 2017-01-29 21:24:32 -05:00
Kyle Spearrin
f3b4cdca8a back to enum ints for 2fa providers 2017-01-28 17:27:37 -05:00
Kyle Spearrin
52460bf47b version bump 2017-01-28 17:00:26 -05:00
Kyle Spearrin
e674e7287e token refresh 2017-01-28 16:09:38 -05:00
Kyle Spearrin
a20e8b6228 fix string split bug on 1password 1pif importer 2017-01-28 02:03:49 -05:00
Kyle Spearrin
8d50e96dab splashid importer 2017-01-28 01:57:36 -05:00
Kyle Spearrin
1fe673951b WIP convert web vault to new identity server 2017-01-28 01:19:43 -05:00
Kyle Spearrin
3df5a9454e 1password importer naming adjustments 2017-01-21 02:08:14 -05:00
Kyle Spearrin
79fecd6b03 fix some linter complaints 2017-01-20 23:39:43 -05:00
Kyle Spearrin
f3445c24b9 added example placeholder text 2017-01-10 21:54:32 -05:00
Kyle Spearrin
a3150d8505 cleanup domain add/edit submission 2017-01-10 21:53:03 -05:00
Kyle Spearrin
828b5d8703 Add/edit equivalent domains 2017-01-10 21:38:53 -05:00
Kyle Spearrin
39559e203a react to model restructure on API 2017-01-10 17:01:38 -05:00
Kyle Spearrin
605bdd0ea0 domain rules page setup with new APIs 2017-01-09 22:26:20 -05:00
Kyle Spearrin
74945e03ce linter fixes 2017-01-06 00:03:33 -05:00
Kyle Spearrin
c772502af5 version bump fix for settings 2017-01-05 23:57:45 -05:00
Kyle Spearrin
401d9db0f2 version bump 2017-01-05 23:56:59 -05:00
Kyle Spearrin
2306da94fe More instructions for firefox password exporter. 2017-01-04 22:43:22 -05:00
Kyle Spearrin
9f7ed11082 fix bug from site rename 2017-01-04 22:32:47 -05:00
Kyle Spearrin
fff0efb095 append tooltips to body 2017-01-04 22:23:21 -05:00
Kyle Spearrin
9aa61f4bca complete import options array 2017-01-04 20:39:55 -05:00
Kyle Spearrin
bd70dc5966 Update README.md 2017-01-04 00:17:24 -05:00
Kyle Spearrin
ac6a3caa8f importer instructions 2017-01-04 00:11:27 -05:00
Kyle Spearrin
0914776152 re-ordered fields on site add/edit. marked name as required with asterisk 2017-01-03 22:09:26 -05:00
Kyle Spearrin
45d0f43e90 If searching, only show folder if it has filtered logins 2017-01-03 19:19:54 -05:00
Kyle Spearrin
7264367fa3 Collapse/Expand #32 2017-01-03 19:06:50 -05:00
Kyle Spearrin
022fa34478 switch back to sites enpoint until API is updated 2017-01-03 00:35:04 -05:00
Kyle Spearrin
f7fd28fded refactored naming Site => Login 2017-01-02 22:26:32 -05:00
Kyle Spearrin
e01a22de48 importer fixes 2017-01-02 21:37:20 -05:00
Kyle Spearrin
711c8e63c1 fixes to 1password4 1pif. new uri formatter. added importers for 1password6 csv, zoho vault csv, password boss json, keepassx csv, and ascendo data vault csv. 2017-01-02 18:20:42 -05:00
Kyle Spearrin
f186ec160a saferpass csv importer 2016-12-31 16:24:11 -05:00
Kyle Spearrin
53fcfd13ee folder and site count to vault list 2016-12-31 15:18:40 -05:00
Kyle Spearrin
c684d66ec0 use reference field names on clipperz importer 2016-12-31 14:52:04 -05:00
Kyle Spearrin
6496f750b0 roboform html importer 2016-12-31 14:48:56 -05:00
Kyle Spearrin
6aaa47cccd avira json importer 2016-12-31 01:00:51 -05:00
Kyle Spearrin
1f6677d610 clipperz html importer 2016-12-31 00:38:12 -05:00
Kyle Spearrin
54b659aff0 true key json importer 2016-12-29 15:33:37 -05:00
Kyle Spearrin
b9db21309e split newline for msecure notes 2016-12-29 11:41:13 -05:00
Kyle Spearrin
8649c3b2b1 msecure csv importer 2016-12-29 02:33:37 -05:00
Kyle Spearrin
6bf6cc365b sticky password importer (#1) 2016-12-28 10:36:44 -05:00
Kyle Spearrin
7c2d5448e8 catch bad data on all importers 2016-12-27 10:36:02 -05:00
Kyle Spearrin
a9f2ef7c10 version bump 2016-12-22 01:38:43 -05:00
Kyle Spearrin
f86bce970e dashlane csv importer (#1) 2016-12-22 01:33:47 -05:00
Kyle Spearrin
6cbd618fb8 password safe importer (#1) 2016-12-21 23:30:47 -05:00
Kyle Spearrin
11787193ed Enpass csv importer (#1) 2016-12-21 22:15:37 -05:00
Kyle Spearrin
45aae6810c added opera extension link 2016-12-19 11:06:29 -05:00
Kyle Spearrin
7f6d571ef1 Clear _aesWithMac too with key clear 2016-12-10 10:34:42 -05:00
Kyle Spearrin
908dc4727c encrypt-then-mac support 2016-12-08 22:21:46 -05:00
Kyle Spearrin
264759cfa0 Version bump and CNAME dist fix 2016-12-03 00:56:41 -05:00
Kyle Spearrin
b5d265526a space between badges 2016-12-01 00:15:35 -05:00
Kyle Spearrin
22290eafb8 update readme with appveyor build badge 2016-12-01 00:14:52 -05:00
Kyle Spearrin
3101e57c36 Reorganization a bit more. Updated readme with build/run instructions. 2016-12-01 00:07:03 -05:00
Kyle Spearrin
b72a52232d reorganize project folder structure and remove asp.net dependency 2016-11-30 23:50:00 -05:00
Kyle Spearrin
a5b8e703fc added node server via gulp serve task 2016-11-30 23:30:08 -05:00
Kyle Spearrin
fb26425f17 tweaks to two factor modal 2016-11-30 23:22:25 -05:00
Kyle Spearrin
3114e20aef remove any spaces from authenticator code input 2016-11-26 18:52:46 -05:00
Kyle Spearrin
a52d2f4b7a added account recovery page 2016-11-14 23:31:54 -05:00
Kyle Spearrin
34e484c377 null check recovery code 2016-11-14 22:45:01 -05:00
Kyle Spearrin
08e8e9ff64 Add recovery code information to two-step login modal, also make verification code required for all actions 2016-11-14 22:38:02 -05:00
Kyle Spearrin
ebf55390eb Added password dragon xml importer #1 2016-11-11 19:21:12 -05:00
Kyle Spearrin
c328144a58 keeper csv import (#1) 2016-11-11 00:10:16 -05:00
Kyle Spearrin
4b583bea9b version bump - 1.4.0 2016-11-09 21:50:04 -05:00
Kyle Spearrin
3f0ca412c6 index checking 2016-11-09 21:49:15 -05:00
Kyle Spearrin
0050b570b4 Added delete option to edit site modal #25 2016-11-09 19:26:02 -05:00
Kyle Spearrin
9405be03b0 Added importer for universal password manager csv format 2016-11-09 18:18:46 -05:00
Kyle Spearrin
b5b706fe06 check badDataLength 2016-11-09 00:42:19 -05:00
Kyle Spearrin
8313d9fa90 Better error handling for imports. Check for bad CSV data on lastpass (when no header row is included). 2016-11-09 00:41:17 -05:00
Kyle Spearrin
93b96b3be7 adjusted add/edit site sort functions for folders 2016-10-22 09:08:17 -04:00
blindly
50e6818f2b Sdd site button to each folder (#17) 2016-10-22 08:43:50 -04:00
Kyle Spearrin
104fb57bd8 Added support for Firefox Password Exporter Addon import. ref #1 2016-10-21 00:10:17 -04:00
Kyle Spearrin
7ba6b2f00a version bump 2016-10-20 08:04:40 -04:00
Kyle Spearrin
d8e8939eca remove old code from 1Password 1pif importer 2016-10-20 08:03:20 -04:00
Kyle Spearrin
26c5f4049d link to help site 2016-10-19 22:52:53 -04:00
Kyle Spearrin
b6c9dba0fc Added Chrome csv importer. Adjustments to 1Password importer in preparation for detecting different formats. 2016-10-19 18:22:18 -04:00
blindly
8badc1a354 Folders should be sorted in alphabetical order when adding or editing a site. 2016-10-19 06:07:10 -04:00
Kyle Spearrin
15097eb1f0 version bump 2016-10-18 22:11:54 -04:00
Kyle Spearrin
986306f811 fix imports error 2016-10-18 21:34:08 -04:00
Kyle Spearrin
c5de290dd4 version bump 2016-10-17 23:26:20 -04:00
Kyle Spearrin
21e9e083f5 nothing was imported error 2016-10-17 23:24:16 -04:00
Kyle Spearrin
517ea65bc9 import validation 2016-10-17 23:18:19 -04:00
Kyle Spearrin
e7a9699226 show import errors as console warning to assist in debugging issues 2016-10-17 23:11:39 -04:00
Kyle Spearrin
52b3dfd0e3 set encoding for to UTF-8 for csv parser 2016-10-17 23:00:44 -04:00
Kyle Spearrin
4c0bde9d87 1.2.0 version bump 2016-10-15 12:30:20 -04:00
Kyle Spearrin
f29bbe4316 adjust password generator to use cryptographically secure RNG 2016-10-15 00:41:03 -04:00
Kyle Spearrin
3c03ed8636 trim url for safeincloud xml import 2016-10-14 23:55:02 -04:00
Kyle Spearrin
71ca4eb84a fix safeincloudcsv value in select option 2016-10-14 23:52:31 -04:00
Kyle Spearrin
30d19b4ee1 import for safeincloud xml 2016-10-14 23:50:39 -04:00
Kyle Spearrin
7b88d30aa8 lowered uri trim to 1000 2016-10-14 23:17:20 -04:00
Kyle Spearrin
0ae11cc40c import for 1password 1pif files 2016-10-14 23:16:37 -04:00
Kyle Spearrin
dd5cda867d trim ridiculously large URLs on import 2016-10-14 21:43:00 -04:00
Kyle Spearrin
2d11bef262 padlock csv import 2016-10-13 23:10:14 -04:00
Kyle Spearrin
5d5d0bfb66 gitter badge 2016-10-13 22:02:46 -04:00
Kyle Spearrin
1b169f368b version bump to 1.1.1 2016-10-13 20:06:55 -04:00
Kyle Spearrin
3375bda789 cache tag for local assets on preprocess build 2016-10-13 19:57:56 -04:00
Kyle Spearrin
6569fbe6aa adjust importers to not require Uri and Passwords 2016-10-13 19:05:53 -04:00
Kyle Spearrin
1d2b82a302 lint fixes for import service. made passwird not required for sites 2016-10-13 18:40:42 -04:00
111 changed files with 4265 additions and 1569 deletions

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

View File

@@ -1,7 +1,27 @@
[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/web?branch=master&svg=true)] (https://ci.appveyor.com/project/bitwarden/web) [![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
# bitwarden Web
The bitwarden Web project is an AngularJS application that powers the web vault (https://vault.bitwarden.com/).
# Build/Run
**Requirements**
- Node.js
- Gulp
Unless you are running the [Core](https://github.com/bitwarden/core) API locally, you'll probably need to switch the
application to target the production API. Open `package.json` and set `production` to `true`.
Then run the following commands:
- `npm install`
- `gulp build`
- `gulp serve`
You can now access the web vault at `http://localhost:4001`.
# Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch.

View File

@@ -3,34 +3,37 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{860863C9-0436-43D4-840D-FE919C9F6FFC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{14FE7221-D377-4AD5-9A9E-4541577CF05A}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
CNAME = CNAME
global.json = global.json
NuGet.Config = NuGet.Config
README.md = README.md
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-web", ".", "{25BEDEF4-2CAF-445A-807D-63C17FF85694}"
ProjectSection(WebsiteProperties) = preProject
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.6.1"
Debug.AspNetCompiler.VirtualPath = "/localhost_15509"
Debug.AspNetCompiler.PhysicalPath = "."
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_15509\"
Debug.AspNetCompiler.Updateable = "true"
Debug.AspNetCompiler.ForceOverwrite = "true"
Debug.AspNetCompiler.FixedNames = "false"
Debug.AspNetCompiler.Debug = "True"
Release.AspNetCompiler.VirtualPath = "/localhost_15509"
Release.AspNetCompiler.PhysicalPath = "."
Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_15509\"
Release.AspNetCompiler.Updateable = "true"
Release.AspNetCompiler.ForceOverwrite = "true"
Release.AspNetCompiler.FixedNames = "false"
Release.AspNetCompiler.Debug = "False"
VWDPort = "15509"
SlnRelativePath = "."
DefaultWebSiteLanguage = "Visual C#"
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Web", "src\Web\Web.xproj", "{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5}.Release|Any CPU.Build.0 = Release|Any CPU
{25BEDEF4-2CAF-445A-807D-63C17FF85694}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25BEDEF4-2CAF-445A-807D-63C17FF85694}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0BEBF47C-BA0B-48AC-B48C-718F94084AD5} = {860863C9-0436-43D4-840D-FE919C9F6FFC}
EndGlobalSection
EndGlobal

View File

@@ -1,6 +0,0 @@
{
"projects": [ "src", "test" ],
"sdk": {
"version": "1.0.0-preview2-003121"
}
}

View File

@@ -8,28 +8,32 @@ var gulp = require('gulp'),
uglify = require('gulp-uglify'),
ghPages = require('gulp-gh-pages'),
less = require('gulp-less'),
connect = require('gulp-connect'),
ngAnnotate = require('gulp-ng-annotate'),
preprocess = require('gulp-preprocess'),
runSequence = require('run-sequence'),
merge = require('merge-stream'),
ngConfig = require('gulp-ng-config'),
settings = require('./settings.json'),
project = require('./project.json'),
project = require('./package.json'),
jshint = require('gulp-jshint'),
_ = require('lodash');
_ = require('lodash'),
webpack = require('webpack-stream');
var paths = {};
paths.dist = '../../dist/';
paths.webroot = './wwwroot/'
paths.dist = './dist/';
paths.webroot = './src/'
paths.js = paths.webroot + 'js/**/*.js';
paths.minJs = paths.webroot + 'js/**/*.min.js';
paths.concatJsDest = paths.webroot + 'js/bw.min.js';
paths.libDir = paths.webroot + 'lib/';
paths.npmDir = 'node_modules/';
paths.lessDir = 'less/';
paths.lessDir = paths.webroot + 'less/';
paths.cssDir = paths.webroot + 'css/';
paths.jsDir = paths.webroot + 'js/';
var randomString = Math.random().toString(36).substring(7);
gulp.task('lint', function () {
return gulp.src(paths.webroot + 'app/**/*.js')
.pipe(jshint())
@@ -39,7 +43,7 @@ gulp.task('lint', function () {
gulp.task('build', function (cb) {
return runSequence(
'clean',
['lib', 'less', 'settings', 'lint'],
['lib', 'webpack', 'less', 'settings', 'lint'],
cb);
});
@@ -139,10 +143,6 @@ gulp.task('lib', ['clean:lib'], function () {
src: paths.npmDir + 'angular-messages/*messages*.js',
dest: paths.libDir + 'angular-messages'
},
{
src: [paths.npmDir + 'sjcl/core/cbc.js', paths.npmDir + 'sjcl/core/bitArray.js', paths.npmDir + 'sjcl/sjcl.js'],
dest: paths.libDir + 'sjcl'
},
{
src: paths.npmDir + 'ngstorage/*.js',
dest: paths.libDir + 'ngstorage'
@@ -175,6 +175,33 @@ gulp.task('lib', ['clean:lib'], function () {
return merge(tasks);
});
gulp.task('webpack', ['webpack:forge']);
gulp.task('webpack:forge', function () {
var forgeDir = paths.npmDir + '/node-forge/lib/';
return gulp.src([
forgeDir + 'pbkdf2.js',
forgeDir + 'aes.js',
forgeDir + 'hmac.js',
forgeDir + 'sha256.js',
forgeDir + 'random.js',
forgeDir + 'forge.js'
]).pipe(webpack({
output: {
filename: 'forge.js',
library: 'forge',
libraryTarget: 'umd'
},
node: {
Buffer: false,
process: false,
crypto: false,
setImmediate: false
}
})).pipe(gulp.dest(paths.libDir + 'forge'));
});
gulp.task('settings', function () {
return config()
.pipe(gulp.dest(paths.webroot + 'app'));
@@ -187,9 +214,9 @@ function config() {
constants: _.merge({}, {
appSettings: {
version: project.version,
environment: project.environment
environment: project.production ? 'Production' : 'Development'
}
}, require('./settings.' + project.environment + '.json') || {})
}, require('./settings' + (project.production ? '.Production' : '') + '.json') || {})
}));
}
@@ -211,7 +238,7 @@ gulp.task('dist:clean', function (cb) {
gulp.task('dist:move', function () {
var moves = [
{
src: '../../CNAME',
src: './CNAME',
dest: paths.dist
},
{
@@ -261,7 +288,7 @@ gulp.task('dist:css', function () {
paths.cssDir + '**/*.css',
'!' + paths.cssDir + '**/*.min.css'
])
.pipe(preprocess({ context: settings }))
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(cssmin())
.pipe(rename({ suffix: '.min' }))
.pipe(gulp.dest(paths.dist + 'css'));
@@ -277,7 +304,7 @@ gulp.task('dist:js:app', function () {
]);
merge(mainStream, config())
.pipe(preprocess({ context: settings }))
.pipe(preprocess({ context: { cacheTag: randomString } }))
.pipe(concat(paths.dist + '/js/app.min.js'))
.pipe(ngAnnotate())
.pipe(uglify())
@@ -287,8 +314,6 @@ gulp.task('dist:js:app', function () {
gulp.task('dist:js:lib', function () {
return gulp
.src([
paths.libDir + 'sjcl/sjcl.js',
paths.libDir + 'sjcl/*.js',
paths.libDir + 'angulartics/angulartics.js',
paths.libDir + '**/*.js',
'!' + paths.libDir + '**/*.min.js',
@@ -306,7 +331,7 @@ gulp.task('dist:preprocess', function () {
.src([
paths.dist + '/**/*.html'
], { base: '.' })
.pipe(preprocess({ context: settings }))
.pipe(preprocess({ context: { cacheTag: randomString }}))
.pipe(gulp.dest('.'));
});
@@ -322,3 +347,10 @@ gulp.task('deploy', ['dist'], function () {
return gulp.src(paths.dist + '**/*')
.pipe(ghPages({ cacheDir: paths.dist + '.publish' }));
});
gulp.task('serve', function () {
connect.server({
port: 4001,
root: ['src']
});
});

View File

@@ -1,6 +1,7 @@
{
"name": "bitwarden",
"version": "0.0.0",
"version": "1.9.1",
"production": true,
"devDependencies": {
"connect": "3.4.1",
"lodash": "4.13.1",
@@ -14,6 +15,7 @@
"gulp-preprocess": "2.0.0",
"gulp-ng-annotate": "2.0.0",
"gulp-ng-config": "1.3.1",
"gulp-connect": "5.0.0",
"jshint": "2.9.2",
"gulp-jshint": "2.0.1",
"rimraf": "2.5.2",
@@ -22,7 +24,6 @@
"jquery": "2.2.4",
"font-awesome": "4.6.3",
"bootstrap": "3.3.6",
"sjcl": "1.0.3",
"angular": "1.5.6",
"angular-resource": "1.5.6",
"angular-bootstrap-npm": "0.14.3",
@@ -40,6 +41,8 @@
"clipboard": "1.5.12",
"ngclipboard": "1.1.1",
"angulartics": "1.1.2",
"angulartics-google-analytics": "0.2.1"
"angulartics-google-analytics": "0.2.1",
"node-forge": "0.7.0",
"webpack-stream": "3.2.0"
}
}

6
settings.json Normal file
View File

@@ -0,0 +1,6 @@
{
"appSettings": {
"rememberedEmailCookieName": "bit.rememberedEmail",
"apiUri": "http://localhost:4000"
}
}

View File

@@ -1,20 +0,0 @@
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace Bit.Web
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
}
}

View File

@@ -1,23 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Bit.Web")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("bitwarden Web")]
[assembly: AssemblyProduct("bitwarden Web")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("0bebf47c-ba0b-48ac-b48c-718f94084ad5")]

View File

@@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:4001/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Web": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,16 +0,0 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Web
{
public class Startup
{
public void ConfigureServices(IServiceCollection services) { }
public void Configure(IApplicationBuilder app)
{
app.UseFileServer();
}
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>0bebf47c-ba0b-48ac-b48c-718f94084ad5</ProjectGuid>
<RootNamespace>Bit.Vault</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>4001</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -1,47 +0,0 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,300italic,400italic,600italic);
@import "../node_modules/toastr/toastr.less";
/* Start AdminLTE */
//Bootstrap Variables & Mixins
//The core bootstrap code have not been modified. These files
//are included only for reference.
@import (reference) "../node_modules/admin-lte/build/bootstrap-less/mixins.less";
@import (reference) "../node_modules/admin-lte/build/bootstrap-less/variables.less";
//MISC
//----
@import "../node_modules/admin-lte/build/less/core.less";
@import "../node_modules/admin-lte/build/less/variables.less";
@import "../node_modules/admin-lte/build/less/mixins.less";
//COMPONENTS
//-----------
@import "../node_modules/admin-lte/build/less/header.less";
@import "../node_modules/admin-lte/build/less/sidebar.less";
@import "../node_modules/admin-lte/build/less/sidebar-mini.less";
@import "../node_modules/admin-lte/build/less/control-sidebar.less";
@import "../node_modules/admin-lte/build/less/dropdown.less";
@import "../node_modules/admin-lte/build/less/forms.less";
@import "../node_modules/admin-lte/build/less/progress-bars.less";
@import "../node_modules/admin-lte/build/less/small-box.less";
@import "../node_modules/admin-lte/build/less/boxes.less";
@import "../node_modules/admin-lte/build/less/info-box.less";
@import "../node_modules/admin-lte/build/less/timeline.less";
@import "../node_modules/admin-lte/build/less/buttons.less";
@import "../node_modules/admin-lte/build/less/callout.less";
@import "../node_modules/admin-lte/build/less/alerts.less";
@import "../node_modules/admin-lte/build/less/navs.less";
@import "../node_modules/admin-lte/build/less/table.less";
@import "../node_modules/admin-lte/build/less/labels.less";
@import "../node_modules/admin-lte/build/less/modal.less";
//PAGES
//------
@import "../node_modules/admin-lte/build/less/login_and_register.less";
@import "../node_modules/admin-lte/build/less/404_500_errors.less";
//Miscellaneous
//-------------
@import "../node_modules/admin-lte/build/less/miscellaneous.less";
@import "../node_modules/admin-lte/build/less/print.less";
/* End AdminLTE */
@import "../node_modules/admin-lte/build/less/skins/skin-blue.less";

View File

@@ -1,56 +0,0 @@
{
"version": "1.1.0",
"environment": "Development",
"dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": "1.0.0"
},
"tools": {
"Microsoft.AspNetCore.Server.IISIntegration.Tools": {
"version": "1.0.0-preview2-final",
"imports": "portable-net45+win8+dnxcore50"
}
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
}
}
}
},
"buildOptions": {
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"runtimeOptions": {
"gcServer": false,
"gcConcurrent": true
},
"publishOptions": {
"include": [
"wwwroot",
"Views",
"settings.json",
"settings.Development.json",
"settings.Production.json",
"settings.Staging.json",
"web.config"
]
},
"scripts": {
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
},
"userSecretsId": "aspnet-Vault-20160519103145"
}

View File

@@ -1,5 +0,0 @@
{
"appSettings": {
"apiUri": "http://localhost:4000"
}
}

View File

@@ -1,5 +0,0 @@
{
"appSettings": {
"apiUri": "https://api.bitwarden.com"
}
}

View File

@@ -1,5 +0,0 @@
{
"appSettings": {
"rememberedEmailCookieName": "bit.rememberedEmail"
}
}

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
</system.webServer>
</configuration>

View File

@@ -1,70 +0,0 @@
/// <autosync enabled="true" />
/// <reference path="../gulpfile.js" />
/// <reference path="app/accounts/accountsLoginController.js" />
/// <reference path="app/accounts/accountsLogoutController.js" />
/// <reference path="app/accounts/accountsmodule.js" />
/// <reference path="app/accounts/accountspasswordhintcontroller.js" />
/// <reference path="app/accounts/accountsRegisterController.js" />
/// <reference path="app/apiInterceptor.js" />
/// <reference path="app/app.js" />
/// <reference path="app/config.js" />
/// <reference path="app/directives/apiFieldDirective.js" />
/// <reference path="app/directives/apiFormDirective.js" />
/// <reference path="app/directives/directivesModule.js" />
/// <reference path="app/directives/masterPasswordDirective.js" />
/// <reference path="app/directives/pageTitleDirective.js" />
/// <reference path="app/directives/passwordmeterdirective.js" />
/// <reference path="app/directives/passwordviewerdirective.js" />
/// <reference path="app/global/globalModule.js" />
/// <reference path="app/global/mainController.js" />
/// <reference path="app/global/sideNavController.js" />
/// <reference path="app/global/topNavController.js" />
/// <reference path="app/services/apiService.js" />
/// <reference path="app/services/authService.js" />
/// <reference path="app/services/cipherService.js" />
/// <reference path="app/services/cryptoService.js" />
/// <reference path="app/services/importservice.js" />
/// <reference path="app/services/passwordservice.js" />
/// <reference path="app/services/servicesModule.js" />
/// <reference path="app/services/tokenService.js" />
/// <reference path="app/services/validationservice.js" />
/// <reference path="app/settings.js" />
/// <reference path="app/settings/settingsChangeEmailController.js" />
/// <reference path="app/settings/settingsChangePasswordController.js" />
/// <reference path="app/settings/settingsController.js" />
/// <reference path="app/settings/settingsdeletecontroller.js" />
/// <reference path="app/settings/settingsmodule.js" />
/// <reference path="app/settings/settingsSessionsController.js" />
/// <reference path="app/settings/settingsTwoFactorController.js" />
/// <reference path="app/tools/toolsAuditsController.js" />
/// <reference path="app/tools/toolsController.js" />
/// <reference path="app/tools/toolsExportController.js" />
/// <reference path="app/tools/toolsImportController.js" />
/// <reference path="app/tools/toolsmodule.js" />
/// <reference path="app/vault/vaultAddFolderController.js" />
/// <reference path="app/vault/vaultAddSiteController.js" />
/// <reference path="app/vault/vaultController.js" />
/// <reference path="app/vault/vaultEditFolderController.js" />
/// <reference path="app/vault/vaultEditSiteController.js" />
/// <reference path="app/vault/vaultmodule.js" />
/// <reference path="lib/admin-lte/js/app.js" />
/// <reference path="lib/angular/angular.js" />
/// <reference path="lib/angular-bootstrap/angular-bootstrap-tpls.js" />
/// <reference path="lib/angular-bootstrap-show-errors/showErrors.js" />
/// <reference path="lib/angular-cookies/angular-cookies.js" />
/// <reference path="lib/angular-jwt/angular-jwt.js" />
/// <reference path="lib/angular-md5/angular-md5.js" />
/// <reference path="lib/angular-messages/angular-messages.js" />
/// <reference path="lib/angular-resource/angular-resource.js" />
/// <reference path="lib/angular-toastr/angular-toastr.js" />
/// <reference path="lib/angular-toastr/angular-toastr.tpls.js" />
/// <reference path="lib/angular-ui-router/angular-ui-router.js" />
/// <reference path="lib/bootstrap/js/bootstrap.min.js" />
/// <reference path="lib/clipboard/clipboard.js" />
/// <reference path="lib/jquery/jquery.js" />
/// <reference path="lib/ngclipboard/ngclipboard.js" />
/// <reference path="lib/ngstorage/ngStorage.js" />
/// <reference path="lib/papaparse/papaparse.js" />
/// <reference path="lib/sjcl/bitArray.js" />
/// <reference path="lib/sjcl/cbc.js" />
/// <reference path="lib/sjcl/sjcl.js" />

View File

@@ -1,112 +0,0 @@
angular
.module('bit.services')
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) {
var _service = {},
_userProfile = null;
_service.logIn = function (email, masterPassword) {
email = email.toLowerCase();
var key = cryptoService.makeKey(masterPassword, email);
var request = {
email: email,
masterPasswordHash: cryptoService.hashPassword(masterPassword, key)
};
var deferred = $q.defer();
apiService.auth.token(request, function (response) {
if (!response || !response.Token) {
return;
}
tokenService.setToken(response.Token);
cryptoService.setKey(key);
_service.setUserProfile(response.Profile);
deferred.resolve(response);
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
};
_service.logInTwoFactor = function (code, provider) {
var request = {
code: code,
provider: provider
};
var deferred = $q.defer();
apiService.auth.tokenTwoFactor(request, function (response) {
if (!response || !response.Token) {
return;
}
tokenService.setToken(response.Token);
_service.setUserProfile(response.Profile);
deferred.resolve(response);
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
};
_service.logOut = function () {
tokenService.clearToken();
cryptoService.clearKey();
_userProfile = null;
};
_service.getUserProfile = function () {
if (!_userProfile) {
_service.setUserProfile();
}
return _userProfile;
};
_service.setUserProfile = function (profile) {
var token = tokenService.getToken();
if (!token) {
return;
}
var decodedToken = jwtHelper.decodeToken(token);
var twoFactor = decodedToken.authmethod === "TwoFactor";
_userProfile = {
id: decodedToken.nameid,
email: decodedToken.email,
twoFactor: twoFactor
};
if (!twoFactor && profile) {
loadProfile(profile);
}
else if (!twoFactor && !profile) {
apiService.accounts.getProfile({}, loadProfile);
}
};
function loadProfile(profile) {
_userProfile.extended = {
name: profile.Name,
twoFactorEnabled: profile.TwoFactorEnabled,
culture: profile.Culture
};
}
_service.isAuthenticated = function () {
return _service.getUserProfile() !== null && !_service.getUserProfile().twoFactor;
};
_service.isTwoFactorAuthenticated = function () {
return _service.getUserProfile() !== null && _service.getUserProfile().twoFactor;
};
return _service;
});

View File

@@ -1,112 +0,0 @@
angular
.module('bit.services')
.factory('cipherService', function (cryptoService, apiService) {
var _service = {};
_service.decryptSites = function (encryptedSites) {
if (!encryptedSites) throw "encryptedSites is undefined or null";
var unencryptedSites = [];
for (var i = 0; i < encryptedSites.length; i++) {
unencryptedSites.push(_service.decryptSite(encryptedSites[i]));
}
return unencryptedSites;
};
_service.decryptSite = function (encryptedSite) {
if (!encryptedSite) throw "encryptedSite is undefined or null";
var site = {
id: encryptedSite.Id,
'type': 1,
folderId: encryptedSite.FolderId,
favorite: encryptedSite.Favorite,
name: cryptoService.decrypt(encryptedSite.Name),
uri: encryptedSite.Uri && encryptedSite.Uri !== '' ? cryptoService.decrypt(encryptedSite.Uri) : null,
username: encryptedSite.Username && encryptedSite.Username !== '' ? cryptoService.decrypt(encryptedSite.Username) : null,
password: encryptedSite.Password && encryptedSite.Password !== '' ? cryptoService.decrypt(encryptedSite.Password) : null,
notes: encryptedSite.Notes && encryptedSite.Notes !== '' ? cryptoService.decrypt(encryptedSite.Notes) : null
};
if (encryptedSite.Folder) {
site.folder = {
name: cryptoService.decrypt(encryptedSite.Folder.Name)
};
}
return site;
};
_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,
'type': 0,
name: cryptoService.decrypt(encryptedFolder.Name)
};
};
_service.encryptSites = function (unencryptedSites, key) {
if (!unencryptedSites) throw "unencryptedSites is undefined or null";
var encryptedSites = [];
for (var i = 0; i < unencryptedSites.length; i++) {
encryptedSites.push(_service.encryptSite(unencryptedSites[i], key));
}
return encryptedSites;
};
_service.encryptSite = function (unencryptedSite, key) {
if (!unencryptedSite) throw "unencryptedSite is undefined or null";
return {
id: unencryptedSite.id,
'type': 1,
folderId: unencryptedSite.folderId === '' ? null : unencryptedSite.folderId,
favorite: unencryptedSite.favorite !== null ? unencryptedSite.favorite : false,
uri: !unencryptedSite.uri || unencryptedSite.uri === '' ? null : cryptoService.encrypt(unencryptedSite.uri, key),
name: cryptoService.encrypt(unencryptedSite.name, key),
username: !unencryptedSite.username || unencryptedSite.username === '' ? null : cryptoService.encrypt(unencryptedSite.username, key),
password: !unencryptedSite.password || unencryptedSite.password === '' ? null : cryptoService.encrypt(unencryptedSite.password, key),
notes: !unencryptedSite.notes || unencryptedSite.notes === '' ? null : cryptoService.encrypt(unencryptedSite.notes, key)
};
};
_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,
'type': 0,
name: cryptoService.encrypt(unencryptedFolder.name, key)
};
};
return _service;
});

View File

@@ -1,114 +0,0 @@
angular
.module('bit.services')
.factory('cryptoService', function ($sessionStorage) {
var _service = {},
_key,
_b64Key,
_aes;
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]();
_service.setKey = function (key) {
_key = key;
$sessionStorage.key = sjcl.codec.base64.fromBits(key);
};
_service.getKey = function (b64) {
if (b64 && b64 === true && _b64Key) {
return _b64Key;
}
else if (!b64 && _key) {
return _key;
}
if ($sessionStorage.key) {
_key = sjcl.codec.base64.toBits($sessionStorage.key);
}
if (b64 && b64 === true) {
_b64Key = sjcl.codec.base64.fromBits(_key);
return _b64Key;
}
return _key;
};
_service.clearKey = function () {
_key = _b64Key = _aes = null;
delete $sessionStorage.key;
};
_service.makeKey = function (password, salt, b64) {
var key = sjcl.misc.pbkdf2(password, salt, 5000, 256, null);
if (b64 && b64 === true) {
return sjcl.codec.base64.fromBits(key);
}
return key;
};
_service.hashPassword = function (password, key) {
if (!key) {
key = _service.getKey();
}
if (!password || !key) {
throw 'Invalid parameters.';
}
var hashBits = sjcl.misc.pbkdf2(key, password, 1, 256, null);
return sjcl.codec.base64.fromBits(hashBits);
};
_service.getAes = function () {
if (!_aes && _service.getKey()) {
_aes = new sjcl.cipher.aes(_service.getKey());
}
return _aes;
};
_service.encrypt = function (plaintextValue, key) {
if (!_service.getKey() && !key) {
throw 'Encryption key unavailable.';
}
if (!key) {
key = _service.getKey();
}
var response = {};
var params = {
mode: "cbc",
iv: sjcl.random.randomWords(4, 10)
};
var ctJson = sjcl.encrypt(key, plaintextValue, params, response);
var ct = ctJson.match(/"ct":"([^"]*)"/)[1];
var iv = sjcl.codec.base64.fromBits(response.iv);
return iv + "|" + ct;
};
_service.decrypt = function (encValue) {
if (!_service.getAes()) {
throw 'AES encryption unavailable.';
}
var encPieces = encValue.split('|');
if (encPieces.length !== 2) {
return '';
}
var ivBits = sjcl.codec.base64.toBits(encPieces[0]);
var ctBits = sjcl.codec.base64.toBits(encPieces[1]);
var decBits = sjcl.mode.cbc.decrypt(_service.getAes(), ctBits, ivBits, null);
return sjcl.codec.utf8String.fromBits(decBits);
};
return _service;
});

View File

@@ -1,321 +0,0 @@
angular
.module('bit.services')
.factory('importService', function () {
var _service = {};
_service.import = function (source, file, success, error) {
switch (source) {
case 'local':
importLocal(file, success, error);
break;
case 'lastpass':
importLastPass(file, success, error);
break;
case 'safeincloudcsv':
importSafeInCloudCsv(file, success, error);
break;
case 'keypassxml':
importKeyPassXml(file, success, error);
break;
default:
error();
break;
}
};
function importLocal(file, success, error) {
Papa.parse(file, {
header: true,
complete: function (results) {
var folders = [],
sites = [],
folderRelationships = [];
angular.forEach(results.data, function (value, key) {
if (!value.uri || value.uri === '') {
return;
}
var folderIndex = folders.length,
siteIndex = sites.length,
hasFolder = value.folder && value.folder !== '',
addFolder = hasFolder;
if (hasFolder) {
for (var i = 0; i < folders.length; i++) {
if (folders[i].name === value.folder) {
addFolder = false;
folderIndex = i;
break;
}
}
}
sites.push({
favorite: value.favorite !== null ? value.favorite : false,
uri: value.uri,
username: value.username && value.username !== '' ? value.username : null,
password: value.password,
notes: value.notes && value.notes !== '' ? value.notes : null,
name: value.name
});
if (addFolder) {
folders.push({
name: value.folder
});
}
if (hasFolder) {
var relationship = {
key: siteIndex,
value: folderIndex
};
folderRelationships.push(relationship);
}
});
success(folders, sites, folderRelationships);
}
});
}
function importLastPass(file, success, error) {
if (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');
if (pre.length === 1) {
var csv = pre.text().trim();
var results = Papa.parse(csv, { header: true });
parseData(results.data);
}
else {
var foundPre = false;
for (var i = 0; i < doc.length; i++) {
if (doc[i].tagName === 'PRE') {
foundPre = true;
var csv = doc[i].outerText.trim();
var results = Papa.parse(csv, { header: true });
parseData(results.data);
break;
}
}
if (!foundPre) {
error();
}
}
};
reader.onerror = function (evt) {
error();
};
}
else {
Papa.parse(file, {
header: true,
complete: function (results) {
parseData(results.data);
}
});
}
function parseData(data) {
var folders = [],
sites = [],
siteRelationships = [];
angular.forEach(data, function (value, key) {
if (!value.url || value.url === '') {
return;
}
var folderIndex = folders.length,
siteIndex = sites.length,
hasFolder = value.grouping && value.grouping !== '' && value.grouping !== '(none)',
addFolder = hasFolder;
if (hasFolder) {
for (var i = 0; i < folders.length; i++) {
if (folders[i].name === value.grouping) {
addFolder = false;
folderIndex = i;
break;
}
}
}
sites.push({
favorite: value.fav === '1',
uri: value.url,
username: value.username && value.username !== '' ? value.username : null,
password: value.password,
notes: value.extra && value.extra !== '' ? value.extra : null,
name: value.name
});
if (addFolder) {
folders.push({
name: value.grouping
});
}
if (hasFolder) {
var relationship = {
key: siteIndex,
value: folderIndex
};
siteRelationships.push(relationship);
}
});
success(folders, sites, siteRelationships);
}
}
function importSafeInCloudCsv(file, success, error) {
Papa.parse(file, {
header: true,
complete: function (results) {
var folders = [],
sites = [],
siteRelationships = [];
angular.forEach(results.data, function (value, key) {
sites.push({
favorite: false,
uri: value.URL && value.URL !== '' ? value.URL : null,
username: value.Login && value.Login !== '' ? value.Login : null,
password: value.Password,
notes: value.Notes && value.Notes !== '' ? value.Notes : null,
name: value.Title
});
});
success(folders, sites, siteRelationships);
}
});
}
function importKeyPassXml(file, success, error) {
var folders = [],
sites = [],
siteRelationships = [];
var reader = new FileReader();
reader.readAsText(file, 'utf-8');
reader.onload = function (evt) {
var xmlDoc = $.parseXML(evt.target.result),
xml = $(xmlDoc);
var root = xml.find('Root');
if (root.length) {
var group = root.find('> Group');
if (group.length) {
traverse($(group[0]), true, '');
success(folders, sites, siteRelationships);
}
}
else {
error();
}
};
reader.onerror = function (evt) {
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 siteIndex = sites.length;
var site = {
favorite: false,
uri: '',
username: '',
password: '',
notes: '',
name: ''
};
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':
site.uri = value;
break;
case 'UserName':
site.username = value;
break;
case 'Password':
site.password = value;
break;
case 'Title':
site.name = value;
break;
case 'Notes':
site.notes = site.notes === null ? value + '\n' : site.notes + value + '\n';
break;
default:
// other custom fields
site.notes = site.notes === null ? key + ': ' + value + '\n'
: site.notes + key + ': ' + value + '\n';
break;
}
}
if (site.name === '') {
site.name = '--';
}
sites.push(site);
if (!isRootNode) {
siteRelationships.push({
key: siteIndex,
value: folderIndex
});
}
}
}
var groups = node.find('> Group');
if (groups.length) {
for (var k = 0; k < groups.length; k++) {
traverse($(groups[k]), false, groupName);
}
}
}
}
return _service;
});

View File

@@ -1,27 +0,0 @@
angular
.module('bit.services')
.factory('tokenService', function ($sessionStorage) {
var _service = {},
_token;
_service.setToken = function (token) {
$sessionStorage.authBearer = token;
_token = token;
};
_service.getToken = function () {
if (!_token) {
_token = $sessionStorage.authBearer;
}
return _token;
};
_service.clearToken = function () {
_token = null;
delete $sessionStorage.authBearer;
};
return _service;
});

View File

@@ -1,2 +0,0 @@
angular.module("bit")
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","version":"1.1.0","environment":"Development","apiUri":"http://localhost:4000"});

View File

@@ -1,60 +0,0 @@
<section class="content-header">
<h1>
Settings
<small>manage your account</small>
</h1>
</section>
<section class="content">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="profileForm" ng-submit="profileForm.$valid && save(model)" api-form="savePromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="profileForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in profileForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<input type="text" id="name" name="Name" ng-model="model.name" class="form-control"
required api-field />
</div>
<div class="form-group">
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
</div>
<div class="form-group" show-errors>
<label for="hint">Master Password Hint</label>
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.masterPasswordHint"
class="form-control" api-field />
</div>
<div class="form-group" show-errors>
<label for="culture">Language/Culture</label>
<select id="culture" name="Culture" ng-model="model.culture" class="form-control" api-field>
<option value="en-US">English (US)</option>
</select>
</div>
</div>
<div class="col-sm-3 settings-photo">
<a href="http://www.gravatar.com/" target="_blank">
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=150&d=mm"
class="img-rounded img-responsive" alt="User Image">
</a>
<a href="http://www.gravatar.com/" target="_blank" class="btn btn-link"
analytics-on="click" analytics-event="Clicked Update Photo">Update Photo</a>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="profileForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="profileForm.$loading"></i>Save
</button>
</div>
</form>
</div>
</section>

View File

@@ -1,36 +0,0 @@
angular
.module('bit.tools')
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics) {
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
$scope.model = { source: 'local' };
$scope.import = function (model) {
$scope.processing = true;
var file = document.getElementById('file').files[0];
importService.import(model.source, file, importSuccess, importError);
};
function importSuccess(folders, sites, folderRelationships) {
apiService.ciphers.import({
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
sites: cipherService.encryptSites(sites, cryptoService.getKey()),
folderRelationships: folderRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
$state.go('backend.vault').then(function () {
$analytics.eventTrack('Imported Data', { label: $scope.model.source });
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
});
}, importError);
}
function importError() {
$uibModalInstance.dismiss('cancel');
toastr.error('Something went wrong. Try again.', 'Oh No!');
}
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,9 +0,0 @@
<section class="content-header">
<h1>
Tools
<small>helpful utilities</small>
</h1>
</section>
<section class="content">
Several tools are available in the menu to the left. More tools coming soon...
</section>

View File

@@ -1,51 +0,0 @@
angular
.module('bit.vault')
.controller('vaultEditSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, siteId, folders, $analytics) {
$analytics.eventTrack('vaultEditSiteController', { category: 'Modal' });
$scope.folders = folders;
$scope.site = {};
apiService.sites.get({ id: siteId }, function (site) {
$scope.site = cipherService.decryptSite(site);
});
$scope.save = function (model) {
var site = cipherService.encryptSite(model);
$scope.savePromise = apiService.sites.put({ id: siteId }, site, function (siteResponse) {
$analytics.eventTrack('Edited Site');
var decSite = cipherService.decryptSite(siteResponse);
$uibModalInstance.close(decSite);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.site.password = passwordService.generatePassword({ length: 10, special: true });
}
};
$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('cancel');
};
});

View File

@@ -1,7 +1,8 @@
angular
.module('bit.accounts')
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService, $state, appSettings, $analytics) {
.controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService,
$state, appSettings, $analytics) {
var rememberedEmail = $cookies.get(appSettings.rememberedEmailCookieName);
if (rememberedEmail) {
$scope.model = {
@@ -10,10 +11,13 @@ angular
};
}
var email,
masterPassword;
$scope.login = function (model) {
$scope.loginPromise = authService.logIn(model.email, model.masterPassword);
$scope.loginPromise.then(function () {
$scope.loginPromise.then(function (twoFactorProviders) {
if (model.rememberEmail) {
var cookieExpiration = new Date();
cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10);
@@ -27,8 +31,10 @@ angular
$cookies.remove(appSettings.rememberedEmailCookieName);
}
var profile = authService.getUserProfile();
if (profile.twoFactor) {
if (twoFactorProviders && twoFactorProviders.length > 0) {
email = model.email;
masterPassword = model.masterPassword;
$analytics.eventTrack('Logged In To Two-step');
$state.go('frontend.login.twoFactor');
}
@@ -40,8 +46,8 @@ angular
};
$scope.twoFactor = function (model) {
// Only supporting Authenticator provider for now
$scope.twoFactorPromise = authService.logInTwoFactor(model.code, "Authenticator");
// Only supporting Authenticator (0) provider for now
$scope.twoFactorPromise = authService.logIn(email, masterPassword, model.code, 0);
$scope.twoFactorPromise.then(function () {
$analytics.eventTrack('Logged In From Two-step');

View File

@@ -0,0 +1,21 @@
angular
.module('bit.accounts')
.controller('accountsRecoverController', function ($scope, apiService, cryptoService) {
$scope.success = false;
$scope.submit = function (model) {
var email = model.email.toLowerCase();
var key = cryptoService.makeKey(model.masterPassword, email);
var request = {
email: email,
masterPasswordHash: cryptoService.hashPassword(model.masterPassword, key),
recoveryCode: model.code.replace(/\s/g, '').toLowerCase()
};
$scope.submitPromise = apiService.accounts.postTwoFactorRecover(request, function () {
$scope.success = true;
}).$promise;
};
});

View File

@@ -13,7 +13,10 @@
<span class="fa fa-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-offset-7 col-xs-5">
<div class="col-xs-7">
<a ui-sref="frontend.recover">Lost authenticator app?</a>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="twoFactorForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="twoFactorForm.$loading"></i>Log In
</button>

View File

@@ -0,0 +1,52 @@
<div class="login-box">
<div class="login-logo">
<i class="fa fa-shield"></i> <b>bit</b>warden
</div>
<div class="login-box-body">
<p class="login-box-msg">Lost your authenticator app?</p>
<div class="text-center" ng-show="success">
<div class="callout callout-success">
Two-step login has been successfully disabled on your account.
</div>
<a ui-sref="frontend.login.info">Ready to log in?</a>
</div>
<form name="recoverForm" ng-submit="recoverForm.$valid && submit(model)" ng-show="!success"
api-form="submitPromise">
<div class="callout callout-danger validation-errors" ng-show="recoverForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in recoverForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group has-feedback" show-errors>
<label for="email" class="sr-only">Email</label>
<input type="email" id="email" name="Email" class="form-control" placeholder="Email" ng-model="model.email"
required api-field />
<span class="fa fa-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback" show-errors>
<label for="masterPassword" class="sr-only">Master Password</label>
<input type="password" id="masterPassword" name="MasterPasswordHash" class="form-control" placeholder="Master Password"
ng-model="model.masterPassword"
required api-field />
<span class="fa fa-lock form-control-feedback"></span>
</div>
<div class="form-group has-feedback" show-errors>
<label for="code" class="sr-only">Recovery code</label>
<input type="text" id="code" name="RecoveryCode" class="form-control" placeholder="Recovery code"
ng-model="model.code" required api-field />
<span class="fa fa-key form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-7">
<a ui-sref="frontend.login.info">Ready to log in?</a>
</div>
<div class="col-xs-5">
<button type="submit" class="btn btn-primary btn-block btn-flat" ng-disabled="recoverForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="recoverForm.$loading"></i>Submit
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -2,11 +2,44 @@ angular
.module('bit')
.config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, $uibTooltipProvider, toastrConfig) {
jwtInterceptorProvider.urlParam = 'access_token';
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService) {
if (config.url.indexOf(appSettings.apiUri) === 0) {
return tokenService.getToken();
jwtInterceptorProvider.urlParam = 'access_token2';
var refreshPromise;
jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService, apiService, jwtHelper, $q) {
if (config.url.indexOf(appSettings.apiUri) !== 0) {
return;
}
if (refreshPromise) {
return refreshPromise;
}
var token = tokenService.getToken();
if (!token) {
return;
}
if (!tokenService.tokenNeedsRefresh(token)) {
return token;
}
var refreshToken = tokenService.getRefreshToken();
if (!refreshToken) {
return;
}
var deferred = $q.defer();
apiService.identity.token({
grant_type: 'refresh_token',
client_id: 'web',
refresh_token: refreshToken
}, function (response) {
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
refreshPromise = null;
deferred.resolve(response.access_token);
});
refreshPromise = deferred.promise;
return refreshPromise;
};
angular.extend(toastrConfig, {
@@ -17,7 +50,9 @@ angular
});
$uibTooltipProvider.options({
popupDelay: 600
popupDelay: 600,
appendToBody: true
});
if ($httpProvider.defaults.headers.post) {
@@ -41,7 +76,7 @@ angular
}
})
.state('backend.vault', {
url: '^/',
url: '^/vault',
templateUrl: 'app/vault/views/vault.html',
controller: 'vaultController',
data: { pageTitle: 'My Vault' }
@@ -52,6 +87,12 @@ angular
controller: 'settingsController',
data: { pageTitle: 'Settings' }
})
.state('backend.settingsDomains', {
url: '^/settings/domains',
templateUrl: 'app/settings/views/settingsDomains.html',
controller: 'settingsDomainsController',
data: { pageTitle: 'Domain Settings' }
})
.state('backend.tools', {
url: '^/tools',
templateUrl: 'app/tools/views/tools.html',
@@ -75,14 +116,14 @@ angular
}
})
.state('frontend.login.info', {
url: '^/login',
url: '^/',
templateUrl: 'app/accounts/views/accountsLoginInfo.html',
data: {
pageTitle: 'Log In'
}
})
.state('frontend.login.twoFactor', {
url: '^/login/two-factor',
url: '^/two-factor',
templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html',
data: {
pageTitle: 'Log In (Two Factor)',
@@ -105,6 +146,15 @@ angular
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.register', {
url: '^/register',
templateUrl: 'app/accounts/views/accountsRegister.html',
@@ -118,7 +168,7 @@ angular
.run(function ($rootScope, authService, jwtHelper, tokenService, $state) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if (!toState.data || !toState.data.authorize) {
if (authService.isAuthenticated() && !jwtHelper.isTokenExpired(tokenService.getToken())) {
if (authService.isAuthenticated()) {
event.preventDefault();
$state.go('backend.vault');
}
@@ -126,7 +176,7 @@ angular
return;
}
if (!authService.isAuthenticated() || jwtHelper.isTokenExpired(tokenService.getToken())) {
if (!authService.isAuthenticated()) {
event.preventDefault();
authService.logOut();
$state.go('frontend.login.info');

View File

@@ -40,43 +40,11 @@ angular
$state.go('backend.vault');
};
$scope.addSite = function () {
$scope.$broadcast('vaultAddSite');
$scope.addLogin = function () {
$scope.$broadcast('vaultAddLogin');
};
$scope.addFolder = function () {
$scope.$broadcast('vaultAddFolder');
};
$scope.changeEmail = function () {
$scope.$broadcast('settingsChangeEmail');
};
$scope.changePassword = function () {
$scope.$broadcast('settingsChangePassword');
};
$scope.sessions = function () {
$scope.$broadcast('settingsSessions');
};
$scope.delete = function () {
$scope.$broadcast('settingsDelete');
};
$scope.twoFactor = function () {
$scope.$broadcast('settingsTwoFactor');
};
$scope.import = function () {
$scope.$broadcast('toolsImport');
};
$scope.export = function () {
$scope.$broadcast('toolsExport');
};
$scope.audits = function () {
$scope.$broadcast('toolsAudits');
};
});

View File

@@ -1,11 +1,11 @@
angular
.module('bit.services')
.factory('apiService', function ($resource, tokenService, appSettings) {
.factory('apiService', function ($resource, tokenService, appSettings, $httpParamSerializer) {
var _service = {},
_apiUri = appSettings.apiUri;
_service.sites = $resource(_apiUri + '/sites/:id', {}, {
_service.logins = $resource(_apiUri + '/sites/:id', {}, {
get: { method: 'GET', params: { id: '@id' } },
list: { method: 'GET', params: {} },
post: { method: 'POST', params: {} },
@@ -36,18 +36,36 @@
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: {} },
getTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'GET', params: {} },
putTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'POST', params: {} },
postTwoFactorRecover: { url: _apiUri + '/accounts/two-factor-recover', method: 'POST', params: {} },
postPasswordHint: { url: _apiUri + '/accounts/password-hint', method: 'POST', params: {} },
putSecurityStamp: { url: _apiUri + '/accounts/security-stamp', method: 'POST', params: {} },
'import': { url: _apiUri + '/accounts/import', method: 'POST', params: {} },
postDelete: { url: _apiUri + '/accounts/delete', method: 'POST', params: {} }
});
_service.auth = $resource(_apiUri + '/auth', {}, {
token: { url: _apiUri + '/auth/token', method: 'POST', params: {} },
tokenTwoFactor: { url: _apiUri + '/auth/token/two-factor', 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.identity = $resource(_apiUri + '/connect', {}, {
token: {
url: _apiUri + '/connect/token',
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
transformRequest: transformUrlEncoded,
skipAuthorization: true,
params: {}
}
});
function transformUrlEncoded(data) {
return $httpParamSerializer(data);
}
return _service;
});

View File

@@ -0,0 +1,93 @@
angular
.module('bit.services')
.factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) {
var _service = {},
_userProfile = null;
_service.logIn = function (email, masterPassword, token, provider) {
email = email.toLowerCase();
var key = cryptoService.makeKey(masterPassword, email);
var request = {
username: email,
password: cryptoService.hashPassword(masterPassword, key),
grant_type: 'password',
scope: 'api offline_access',
client_id: 'web'
};
if (token && typeof (provider) !== 'undefined' && provider !== null) {
request.twoFactorToken = token.replace(' ', '');
request.twoFactorProvider = provider;
}
// TODO: device information one day?
var deferred = $q.defer();
apiService.identity.token(request, function (response) {
if (!response || !response.access_token) {
return;
}
tokenService.setToken(response.access_token);
tokenService.setRefreshToken(response.refresh_token);
cryptoService.setKey(key);
deferred.resolve();
}, function (error) {
if (error.status === 400 && error.data.TwoFactorProviders && error.data.TwoFactorProviders.length) {
deferred.resolve(error.data.TwoFactorProviders);
}
else {
deferred.reject(error);
}
});
return deferred.promise;
};
_service.logOut = function () {
tokenService.clearToken();
tokenService.clearRefreshToken();
cryptoService.clearKey();
_userProfile = null;
};
_service.getUserProfile = function () {
if (!_userProfile) {
_service.setUserProfile();
}
return _userProfile;
};
_service.setUserProfile = function () {
var token = tokenService.getToken();
if (!token) {
return;
}
var decodedToken = jwtHelper.decodeToken(token);
_userProfile = {
id: decodedToken.name,
email: decodedToken.email
};
apiService.accounts.getProfile({}, loadProfile);
};
function loadProfile(profile) {
_userProfile.extended = {
name: profile.Name,
twoFactorEnabled: profile.TwoFactorEnabled,
culture: profile.Culture
};
}
_service.isAuthenticated = function () {
return tokenService.getToken() !== null;
};
return _service;
});

View File

@@ -0,0 +1,112 @@
angular
.module('bit.services')
.factory('cipherService', function (cryptoService, apiService) {
var _service = {};
_service.decryptLogins = function (encryptedLogins) {
if (!encryptedLogins) throw "encryptedLogins is undefined or null";
var unencryptedLogins = [];
for (var i = 0; i < encryptedLogins.length; i++) {
unencryptedLogins.push(_service.decryptLogin(encryptedLogins[i]));
}
return unencryptedLogins;
};
_service.decryptLogin = function (encryptedLogin) {
if (!encryptedLogin) throw "encryptedLogin is undefined or null";
var login = {
id: encryptedLogin.Id,
'type': 1,
folderId: encryptedLogin.FolderId,
favorite: encryptedLogin.Favorite,
name: cryptoService.decrypt(encryptedLogin.Name),
uri: encryptedLogin.Uri && encryptedLogin.Uri !== '' ? cryptoService.decrypt(encryptedLogin.Uri) : null,
username: encryptedLogin.Username && encryptedLogin.Username !== '' ? cryptoService.decrypt(encryptedLogin.Username) : null,
password: encryptedLogin.Password && encryptedLogin.Password !== '' ? cryptoService.decrypt(encryptedLogin.Password) : null,
notes: encryptedLogin.Notes && encryptedLogin.Notes !== '' ? cryptoService.decrypt(encryptedLogin.Notes) : null
};
if (encryptedLogin.Folder) {
login.folder = {
name: cryptoService.decrypt(encryptedLogin.Folder.Name)
};
}
return login;
};
_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,
'type': 0,
name: cryptoService.decrypt(encryptedFolder.Name)
};
};
_service.encryptLogins = function (unencryptedLogins, key) {
if (!unencryptedLogins) throw "unencryptedLogins is undefined or null";
var encryptedLogins = [];
for (var i = 0; i < unencryptedLogins.length; i++) {
encryptedLogins.push(_service.encryptLogin(unencryptedLogins[i], key));
}
return encryptedLogins;
};
_service.encryptLogin = function (unencryptedLogin, key) {
if (!unencryptedLogin) throw "unencryptedLogin is undefined or null";
return {
id: unencryptedLogin.id,
'type': 1,
folderId: unencryptedLogin.folderId === '' ? null : unencryptedLogin.folderId,
favorite: unencryptedLogin.favorite !== null ? unencryptedLogin.favorite : false,
uri: !unencryptedLogin.uri || unencryptedLogin.uri === '' ? null : cryptoService.encrypt(unencryptedLogin.uri, key),
name: cryptoService.encrypt(unencryptedLogin.name, key),
username: !unencryptedLogin.username || unencryptedLogin.username === '' ? null : cryptoService.encrypt(unencryptedLogin.username, key),
password: !unencryptedLogin.password || unencryptedLogin.password === '' ? null : cryptoService.encrypt(unencryptedLogin.password, key),
notes: !unencryptedLogin.notes || unencryptedLogin.notes === '' ? null : cryptoService.encrypt(unencryptedLogin.notes, key)
};
};
_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,
'type': 0,
name: cryptoService.encrypt(unencryptedFolder.name, key)
};
};
return _service;
});

View File

@@ -0,0 +1,153 @@
angular
.module('bit.services')
.factory('cryptoService', function ($sessionStorage) {
var _service = {},
_key,
_b64Key;
_service.setKey = function (key) {
_key = key;
$sessionStorage.key = forge.util.encode64(key);
};
_service.getKey = function (b64) {
if (b64 && b64 === true && _b64Key) {
return _b64Key;
}
else if (!b64 && _key) {
return _key;
}
if ($sessionStorage.key) {
_key = forge.util.decode64($sessionStorage.key);
}
if (b64 && b64 === true) {
_b64Key = forge.util.encode64(_key);
return _b64Key;
}
return _key;
};
_service.getEncKey = function (key) {
key = key || _service.getKey();
var buffer = forge.util.createBuffer(key);
return buffer.getBytes(16);
};
_service.getMacKey = function (key) {
key = key || _service.getKey();
var buffer = forge.util.createBuffer(key);
buffer.getBytes(16); // skip first half
return buffer.getBytes(16);
};
_service.clearKey = function () {
_key = _b64Key = null;
delete $sessionStorage.key;
};
_service.makeKey = function (password, salt, b64) {
var key = forge.pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt),
5000, 256 / 8, 'sha256');
if (b64 && b64 === true) {
return forge.util.encode64(key);
}
return key;
};
_service.hashPassword = function (password, key) {
if (!key) {
key = _service.getKey();
}
if (!password || !key) {
throw 'Invalid parameters.';
}
var hashBits = forge.pbkdf2(key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256');
return forge.util.encode64(hashBits);
};
_service.encrypt = function (plaintextValue, key) {
if (!_service.getKey() && !key) {
throw 'Encryption key unavailable.';
}
// TODO: Turn on whenever ready to support encrypt-then-mac
var encKey = null;
if (false) {
encKey = _service.getEncKey(key);
}
else {
encKey = key || _service.getKey();
}
var buffer = forge.util.createBuffer(plaintextValue, 'utf8');
var ivBytes = forge.random.getBytesSync(16);
var cipher = forge.cipher.createCipher('AES-CBC', encKey);
cipher.start({ iv: ivBytes });
cipher.update(buffer);
cipher.finish();
var iv = forge.util.encode64(ivBytes);
var ctBytes = cipher.output.getBytes();
var ct = forge.util.encode64(ctBytes);
var cipherString = iv + '|' + ct;
// TODO: Turn on whenever ready to support encrypt-then-mac
if (false) {
var mac = computeMac(ctBytes, ivBytes);
cipherString = cipherString + '|' + mac;
}
return cipherString;
};
_service.decrypt = function (encValue) {
if (!_service.getKey()) {
throw 'AES encryption unavailable.';
}
var encPieces = encValue.split('|');
if (encPieces.length !== 2 && encPieces.length !== 3) {
return '';
}
var ivBytes = forge.util.decode64(encPieces[0]);
var ctBytes = forge.util.decode64(encPieces[1]);
var computedMac = null;
if (encPieces.length === 3) {
computedMac = computeMac(ctBytes, ivBytes);
if (computedMac !== encPieces[2]) {
console.error('MAC failed.');
return '';
}
}
var ctBuffer = forge.util.createBuffer(ctBytes);
var decipher = forge.cipher.createDecipher('AES-CBC', computedMac ? _service.getEncKey() : _service.getKey());
decipher.start({ iv: ivBytes });
decipher.update(ctBuffer);
decipher.finish();
return decipher.output.toString('utf8');
};
function computeMac(ct, iv, macKey) {
var hmac = forge.hmac.create();
hmac.start('sha256', macKey || _service.getMacKey());
hmac.update(iv + ct);
var mac = hmac.digest();
return forge.util.encode64(mac.getBytes());
}
return _service;
});

File diff suppressed because it is too large Load Diff

View File

@@ -97,8 +97,39 @@ angular
return password;
};
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
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;

View File

@@ -0,0 +1,63 @@
angular
.module('bit.services')
.factory('tokenService', function ($sessionStorage, 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.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;
});

View File

@@ -14,6 +14,10 @@
return;
}
if (data && data.ErrorModel) {
data = data.ErrorModel;
}
if (!data.ValidationErrors) {
if (data.Message) {
form.$errors.push(data.Message);

2
src/app/settings.js Normal file
View File

@@ -0,0 +1,2 @@
angular.module("bit")
.constant("appSettings", {"rememberedEmailCookieName":"bit.rememberedEmail","apiUri":"https://api.bitwarden.com","version":"1.9.1","environment":"Production"});

View File

@@ -0,0 +1,18 @@
angular
.module('bit.vault')
.controller('settingsAddEditEquivalentDomainController', 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');
};
});

View File

@@ -27,10 +27,10 @@
$scope.confirm = function (model) {
$scope.processing = true;
var reencryptedSites = [];
var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) {
var unencryptedSites = cipherService.decryptSites(encryptedSites.Data);
reencryptedSites = cipherService.encryptSites(unencryptedSites, _newKey);
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, _newKey);
}).$promise;
var reencryptedFolders = [];
@@ -39,13 +39,13 @@
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, _newKey);
}).$promise;
$q.all([sitesPromise, foldersPromise]).then(function () {
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
token: model.token,
newEmail: model.newEmail.toLowerCase(),
masterPasswordHash: _masterPasswordHash,
newMasterPasswordHash: _newMasterPasswordHash,
ciphers: reencryptedSites.concat(reencryptedFolders)
ciphers: reencryptedLogins.concat(reencryptedFolders)
};
$scope.confirmPromise = apiService.accounts.email(request, function () {

View File

@@ -27,10 +27,10 @@
var profile = authService.getUserProfile();
var newKey = cryptoService.makeKey(model.newMasterPassword, profile.email.toLowerCase());
var reencryptedSites = [];
var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) {
var unencryptedSites = cipherService.decryptSites(encryptedSites.Data);
reencryptedSites = cipherService.encryptSites(unencryptedSites, newKey);
var reencryptedLogins = [];
var loginsPromise = apiService.logins.list({ dirty: false }, function (encryptedLogins) {
var unencryptedLogins = cipherService.decryptLogins(encryptedLogins.Data);
reencryptedLogins = cipherService.encryptLogins(unencryptedLogins, newKey);
}).$promise;
var reencryptedFolders = [];
@@ -39,11 +39,11 @@
reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey);
}).$promise;
$q.all([sitesPromise, foldersPromise]).then(function () {
$q.all([loginsPromise, foldersPromise]).then(function () {
var request = {
masterPasswordHash: cryptoService.hashPassword(model.masterPassword),
newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey),
ciphers: reencryptedSites.concat(reencryptedFolders)
ciphers: reencryptedLogins.concat(reencryptedFolders)
};
$scope.savePromise = apiService.accounts.putPassword(request, function () {

View File

@@ -2,20 +2,33 @@
.module('bit.settings')
.controller('settingsController', function ($scope, $uibModal, apiService, toastr, authService) {
$scope.model = {};
$scope.model = {
profile: {},
twoFactorEnabled: false,
email: null
};
apiService.accounts.getProfile({}, function (user) {
$scope.model = {
name: user.Name,
profile: {
name: user.Name,
masterPasswordHint: user.MasterPasswordHint,
culture: user.Culture
},
email: user.Email,
masterPasswordHint: user.MasterPasswordHint,
culture: user.Culture,
twoFactorEnabled: user.TwoFactorEnabled
};
});
$scope.save = function (model) {
$scope.savePromise = apiService.accounts.putProfile({}, model, function (profile) {
$scope.generalSave = function () {
$scope.generalPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
authService.setUserProfile(profile);
toastr.success('Account has been updated.', 'Success!');
}).$promise;
};
$scope.passwordHintSave = function () {
$scope.passwordHintPromise = apiService.accounts.putProfile({}, $scope.model.profile, function (profile) {
authService.setUserProfile(profile);
toastr.success('Account has been updated.', 'Success!');
}).$promise;
@@ -29,34 +42,29 @@
});
};
$scope.$on('settingsChangePassword', function (event, args) {
$scope.changePassword();
});
$scope.changeEmail = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsChangeEmail.html',
controller: 'settingsChangeEmailController',
size: 'sm'
controller: 'settingsChangeEmailController'
});
};
$scope.$on('settingsChangeEmail', function (event, args) {
$scope.changeEmail();
});
$scope.twoFactor = function () {
$uibModal.open({
var twoFactorModal = $uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsTwoFactor.html',
controller: 'settingsTwoFactorController'
});
};
$scope.$on('settingsTwoFactor', function (event, args) {
$scope.twoFactor();
});
twoFactorModal.result.then(function (enabled) {
if (enabled === null) {
return;
}
$scope.model.twoFactorEnabled = enabled;
});
};
$scope.sessions = function () {
$uibModal.open({
@@ -66,20 +74,11 @@
});
};
$scope.$on('settingsSessions', function (event, args) {
$scope.sessions();
});
$scope.delete = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/settings/views/settingsDelete.html',
controller: 'settingsDeleteController',
size: 'sm'
controller: 'settingsDeleteController'
});
};
$scope.$on('settingsDelete', function (event, args) {
$scope.delete();
});
});

View File

@@ -0,0 +1,101 @@
angular
.module('bit.settings')
.controller('settingsDomainsController', 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.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) {
toastr.success('Domains have been updated.', 'Success!');
}).$promise;
};
});

View File

@@ -19,18 +19,31 @@
masterPasswordHash: _masterPasswordHash,
provider: 0 /* Only authenticator provider for now. */
}, function (response) {
var key = response.AuthenticatorKey;
$scope.twoFactorModel = {
enabled: response.TwoFactorEnabled,
key: key.replace(/(.{4})/g, '$1 ').trim(),
qr: 'https://chart.googleapis.com/chart?chs=120x120&chld=L|0&cht=qr&chl=otpauth://totp/' +
_issuer + ':' + encodeURIComponent(_profile.email) +
'%3Fsecret=' + encodeURIComponent(key) +
'%26issuer=' + _issuer
};
processResponse(response);
}).$promise;
};
function formatString(s) {
if (!s) {
return null;
}
return s.replace(/(.{4})/g, '$1 ').trim().toUpperCase();
}
function processResponse(response) {
var key = response.AuthenticatorKey;
$scope.twoFactorModel = {
enabled: response.TwoFactorEnabled,
key: formatString(key),
recovery: formatString(response.TwoFactorRecoveryCode),
qr: 'https://chart.googleapis.com/chart?chs=120x120&chld=L|0&cht=qr&chl=otpauth://totp/' +
_issuer + ':' + encodeURIComponent(_profile.email) +
'%3Fsecret=' + encodeURIComponent(key) +
'%26issuer=' + _issuer
};
}
$scope.update = function (model) {
var currentlyEnabled = $scope.twoFactorModel.enabled;
if (currentlyEnabled && !confirm('Are you sure you want to disable two-step login?')) {
@@ -39,7 +52,7 @@
var request = {
enabled: !currentlyEnabled,
token: model ? model.token : null,
token: model.token.replace(' ', ''),
masterPasswordHash: _masterPasswordHash
};
@@ -48,18 +61,28 @@
$analytics.eventTrack('Enabled Two-step Login');
toastr.success('Two-step login has been enabled.');
if (_profile.extended) _profile.extended.twoFactorEnabled = true;
processResponse(response);
$('#token').blur();
model.token = null;
}
else {
$analytics.eventTrack('Disabled Two-step Login');
toastr.success('Two-step login has been disabled.');
if (_profile.extended) _profile.extended.twoFactorEnabled = false;
$scope.close();
}
$scope.close();
}).$promise;
};
$scope.print = function (printContent) {
var w = window.open();
w.document.write('<div style="font-size: 18px; text-align: center;"><p>bitwarden two-step login recovery code:</p>' +
'<pre>' + printContent + '</pre>');
w.print();
w.close();
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
$uibModalInstance.close(!_profile.extended ? null : _profile.extended.twoFactorEnabled);
};
});

View File

@@ -0,0 +1,128 @@
<section class="content-header">
<h1>
Settings
<small>manage your account</small>
</h1>
</section>
<section class="content">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">General</h3>
</div>
<form role="form" name="generalForm" ng-submit="generalForm.$valid && generalSave()" api-form="generalPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="generalForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in generalForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<input type="text" id="name" name="Name" ng-model="model.profile.name" class="form-control"
required api-field />
</div>
<div class="form-group">
<label for="email">Email - <a href="javascript:void(0)" ng-click="changeEmail()">change</a></label>
<input type="text" id="email" ng-model="model.email" class="form-control" readonly />
</div>
<div class="form-group" show-errors>
<label for="culture">Language/Culture</label>
<select id="culture" name="Culture" ng-model="model.profile.culture" class="form-control" api-field>
<option value="en-US">English (US)</option>
</select>
</div>
</div>
<div class="col-sm-3 settings-photo">
<a href="http://www.gravatar.com/" target="_blank">
<img src="//www.gravatar.com/avatar/{{ main.userProfile.email | gravatar }}.jpg?s=150&d=mm"
class="img-rounded img-responsive" alt="User Image">
</a>
<a href="http://www.gravatar.com/" target="_blank" class="btn btn-link"
analytics-on="click" analytics-event="Clicked Update Photo">Update Photo</a>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="generalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="generalForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="changeEmail()">
Change Email
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Master Password</h3>
</div>
<form role="form" name="masterPasswordForm" ng-submit="masterPasswordForm.$valid && passwordHintSave()"
api-form="passwordHintPromise">
<div class="box-body">
<div class="row">
<div class="col-sm-9">
<div class="callout callout-danger validation-errors" ng-show="masterPasswordForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in masterPasswordForm.$errors">{{e}}</li>
</ul>
</div>
<div class="form-group" show-errors>
<label for="hint">Master Password Hint</label>
<input type="text" id="hint" name="MasterPasswordHint" ng-model="model.profile.masterPasswordHint"
class="form-control" api-field />
</div>
</div>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="masterPasswordForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="masterPasswordForm.$loading"></i>Save
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="changePassword()">
Change Master Password
</button>
</div>
</form>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Two-step Log In</h3>
</div>
<div class="box-body">
<p>
Current Status:
<span class="label bg-green" ng-show="model.twoFactorEnabled">ENABLED</span>
<span class="label bg-gray" ng-show="!model.twoFactorEnabled">DISABLED</span>
</p>
<p>
Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device
while logging in (in addition to your master password).
</p>
</div>
<div class="box-footer">
<button type="button" class="btn btn-default btn-flat" ng-click="twoFactor()">
Manage Two-step Log In
</button>
</div>
</div>
<div class="box box-danger">
<div class="box-header with-border">
<h3 class="box-title">Danger Zone</h3>
</div>
<div class="box-body">
Careful, these actions are not reversible!
</div>
<div class="box-footer">
<button type="submit" class="btn btn-default btn-flat" ng-click="sessions()">
Deauthorize Sessions
</button>
<button type="submit" class="btn btn-default btn-flat" ng-click="delete()">
Delete Account
</button>
</div>
</div>
</section>

View File

@@ -0,0 +1,35 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><i class="fa fa-globe"></i> {{index ? 'Edit Equivalent Domain' : 'Add Equivalent Domain'}}</h4>
</div>
<form name="domainAddEditForm" ng-submit="domainAddEditForm.$valid && submit(domainAddEditForm)">
<div class="modal-body">
<div class="callout callout-danger validation-errors" ng-show="domainAddEditForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in domainAddEditForm.$errors">{{e}}</li>
</ul>
</div>
<p>
Enter a list of domains separated by commas.
</p>
<div class="form-group" show-errors>
<label for="name">Domains</label> <span>*</span>
<textarea id="domains" name="Domains" ng-model="domains" class="form-control" placeholder="ex. google.com, gmail.com"
style="height: 100px;" required></textarea>
<p class="help-block">
Only "base" domains are allowed. Do not enter subdomains. For example, enter "google.com" instead of
"www.google.com".
</p>
<p class="help-block">
You can also enter "androidapp://package.name" to associate an android app with other website domains.
</p>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat">
Submit
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>
</form>

View File

@@ -0,0 +1,94 @@
<section class="content-header">
<h1>Domain Rules</h1>
</section>
<section class="content">
<p>
If you have the same login across multiple different website domains, you can mark the website as "equivalent".
"Global" domains are ones already created for you by bitwarden.
</p>
<form name="customForm" ng-submit="customForm.$valid && saveCustom()" api-form="customPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Custom Equivalent Domains</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" ng-click="addEdit(null)">
<i class="fa fa-plus"></i> Add New
</button>
</div>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover">
<tbody ng-if="equivalentDomains.length">
<tr ng-repeat="customDomain in equivalentDomains track by $index">
<td style="width: 80px; min-width: 80px;">
<button type="button" class="btn btn-link btn-table" uib-tooltip="Edit" ng-click="addEdit($index)">
<i class="fa fa-lg fa-pencil"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Delete" ng-click="delete($index)">
<i class="fa fa-lg fa-trash"></i>
</button>
</td>
<td>{{customDomain}}</td>
</tr>
</tbody>
<tbody ng-if="!equivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="customForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="customForm.$loading"></i>Save
</button>
</div>
</div>
</form>
<form name="globalForm" ng-submit="globalForm.$valid && saveGlobal()" api-form="globalPromise">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Global Equivalent Domains</h3>
</div>
<div class="box-body no-padding">
<div class="table-responsive">
<table class="table table-striped table-hover">
<tbody ng-if="globalEquivalentDomains.length">
<tr ng-repeat="globalDomain in globalEquivalentDomains">
<td style="width: 80px; min-width: 80px;">
<button type="button" class="btn btn-link btn-table" uib-tooltip="Exclude"
ng-if="!globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
<i class="fa fa-lg fa-ban"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Include"
ng-if="globalDomain.excluded" ng-click="toggleExclude(globalDomain)">
<i class="fa fa-lg fa-plus"></i>
</button>
<button type="button" class="btn btn-link btn-table" uib-tooltip="Customize"
ng-click="customize(globalDomain)">
<i class="fa fa-lg fa-cut"></i>
</button>
</td>
<td ng-class="{strike: globalDomain.excluded}">{{globalDomain.domains}}</td>
</tr>
</tbody>
<tbody ng-if="!globalEquivalentDomains.length">
<tr>
<td>No domains to list.</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="globalForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="globalForm.$loading"></i>Save
</button>
</div>
</div>
</form>
</section>

View File

@@ -1,13 +1,11 @@
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Login</h4>
<h4 class="modal-title" id="twoFactorModelLabel"><i class="fa fa-key"></i> Two-step Log In</h4>
</div>
<form name="authTwoStepForm" ng-submit="authTwoStepForm.$valid && auth(authModel)" api-form="authPromise" ng-if="!twoFactorModel">
<div class="modal-body">
<p>Current Status: <span class="label bg-green" ng-show="enabled()">ENABLED</span><span class="label bg-gray" ng-show="!enabled()">DISABLED</span></p>
<p>Two-step login helps keep your account more secure by requiring a code provided by an app on your mobile device while logging in (in addition to your master password).</p>
<p ng-show="enabled()">Two-step login is already enabled on your account. To access your two-step settings enter your master password below.</p>
<p ng-show="!enabled()">To get started with two-step login enter your master password below.</p>
<p ng-show="enabled()">Two-step log in is already enabled on your account. To access your two-step settings enter your master password below.</p>
<p ng-show="!enabled()">To get started with two-step log in enter your master password below.</p>
<div class="callout callout-danger validation-errors" ng-show="authTwoStepForm.$errors">
<h4>Errors have occured</h4>
<ul>
@@ -29,7 +27,7 @@
<form name="updateTwoStepForm" ng-submit="updateTwoStepForm.$valid && update(updateModel)" api-form="updatePromise" ng-if="twoFactorModel">
<div class="modal-body">
<div ng-show="enabled()">
<p>Two-step login is enabled on your account. Below is the code required by your verification app.</p>
<p>Two-step log in is <strong class="text-green">enabled</strong> on your account. Below is the code required by your verification app.</p>
<p>Need a two-step verification app? Download one of the following:</p>
</div>
<div ng-show="!enabled()">
@@ -37,7 +35,7 @@
<h4>1. Download a two-step verification app</h4>
</div>
<ul class="fa-ul">
<li><i class="fa-li fa fa-apple"></i> iOS devices: <a href="https://itunes.apple.com/en/app/authy/id494168017" target="_blank">Authy for iOS</a></li>
<li><i class="fa-li fa fa-apple"></i> iOS devices: <a href="https://itunes.apple.com/us/app/authy/id494168017?mt=8" target="_blank">Authy for iOS</a></li>
<li><i class="fa-li fa fa-android"></i> Android devices: <a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank">Authy for Android</a></li>
<li><i class="fa-li fa fa-windows"></i> Windows devices: <a href="https://www.microsoft.com/en-us/store/apps/authenticator/9wzdncrfj3rj" target="_blank">Microsoft Authenticator </a></li>
</ul>
@@ -47,35 +45,49 @@
<div class="col-md-4 text-center">
<p><img ng-src="{{twoFactorModel.qr}}" alt="QR" class="img-thumbnail" /></p>
</div>
<div class="col-sm-8">
<div class="col-md-8">
<p><strong>Can't scan the code?</strong> You can add the code to your application manually using the following details:</p>
<ul class="list-unstyled">
<li><strong>Key:</strong> <samp>{{twoFactorModel.key}}</samp></li>
<li><strong>Key:</strong> <code>{{twoFactorModel.key}}</code></li>
<li><strong>Account:</strong> {{account}}</li>
<li><strong>Time based:</strong> Yes</li>
</ul>
</div>
</div>
<div ng-show="!enabled()">
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in updateTwoStepForm.$errors">{{e}}</li>
</ul>
<div ng-show="enabled()">
<hr />
<div class="callout callout-danger">
<h4><i class="fa fa-warning"></i> Recovery Code <i class="fa fa-warning"></i></h4>
<p>
The recovery code allows you to access your account in the event that you lose your authenticator app.
bitwarden support won't be able to assist you if you lose access to your account. We recommend you write down or
print the recovery code below and keep it in a safe place.
</p>
<p><strong>Recovery Code:</strong> <code>{{twoFactorModel.recovery}}</code></p>
<button type="button" class="btn btn-default" ng-click="print(twoFactorModel.recovery)">
<i class="fa fa-print"></i> Print
</button>
</div>
<h4 style="margin-top: 30px;">3. Enter the resulting verification code from the app</h4>
<div class="form-group" show-errors ng-show="!twoFactorModel.enabled">
<label for="token" class="sr-only">Verification Code</label>
<input type="number" id="token" name="Token" placeholder="Verification Code" ng-model="updateModel.token" class="form-control" ng-required="!twoFactorModel.enabled" api-field />
</div>
<p>NOTE: After enabling two-step login, you will be required to enter the current code generated by your verification app each time you log in.</p>
</div>
<div class="callout callout-danger validation-errors" ng-show="updateTwoStepForm.$errors">
<h4>Errors have occured</h4>
<ul>
<li ng-repeat="e in updateTwoStepForm.$errors">{{e}}</li>
</ul>
</div>
<hr ng-show="enabled()" />
<h4 style="margin-top: 30px;"><span ng-show="enabled()">Want to disable? </span><span ng-show="!enabled()">3. </span>Enter the resulting verification code from the app</h4>
<div class="form-group" show-errors>
<label for="token" class="sr-only">Verification Code</label>
<input type="text" id="token" name="Token" placeholder="Verification Code" ng-model="updateModel.token" class="form-control" required api-field />
</div>
<p ng-show="!enabled()">NOTE: After enabling two-step log in, you will be required to enter the current code generated by your verification app each time you log in.</p>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-flat" ng-disabled="updateTwoStepForm.$loading">
<i class="fa fa-refresh fa-spin loading-icon" ng-show="updateTwoStepForm.$loading"></i>
<span ng-show="twoFactorModel.enabled">Disable Two-step</span>
<span ng-show="!twoFactorModel.enabled">Enable Two-step</span>
<span ng-show="enabled()">Disable Two-step</span>
<span ng-show="!enabled()">Enable Two-step</span>
</button>
<button type="button" class="btn btn-default btn-flat" ng-click="close()">Close</button>
</div>

View File

@@ -6,28 +6,18 @@
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsImport.html',
controller: 'toolsImportController',
size: 'sm'
controller: 'toolsImportController'
});
};
$scope.$on('toolsImport', function (event, args) {
$scope.import();
});
$scope.export = function () {
$uibModal.open({
animation: true,
templateUrl: 'app/tools/views/toolsExport.html',
controller: 'toolsExportController',
size: 'sm'
controller: 'toolsExportController'
});
};
$scope.$on('toolsExport', function (event, args) {
$scope.export();
});
$scope.audits = function () {
$uibModal.open({
animation: true,
@@ -35,8 +25,4 @@
controller: 'toolsAuditsController'
});
};
$scope.$on('toolsAudits', function (event, args) {
$scope.audits();
});
});

View File

@@ -5,25 +5,25 @@
$analytics.eventTrack('toolsExportController', { category: 'Modal' });
$scope.export = function (model) {
$scope.startedExport = true;
apiService.sites.list({ expand: ['folder'] }, function (sites) {
apiService.logins.list({ expand: ['folder'] }, function (logins) {
try {
var decSites = cipherService.decryptSites(sites.Data);
var decLogins = cipherService.decryptLogins(logins.Data);
var exportSites = [];
for (var i = 0; i < decSites.length; i++) {
var site = {
name: decSites[i].name,
uri: decSites[i].uri,
username: decSites[i].username,
password: decSites[i].password,
notes: decSites[i].notes,
folder: decSites[i].folder ? decSites[i].folder.name : null
var exportLogins = [];
for (var i = 0; i < decLogins.length; i++) {
var login = {
name: decLogins[i].name,
uri: decLogins[i].uri,
username: decLogins[i].username,
password: decLogins[i].password,
notes: decLogins[i].notes,
folder: decLogins[i].folder ? decLogins[i].folder.name : null
};
exportSites.push(site);
exportLogins.push(login);
}
var csvString = Papa.unparse(exportSites);
var csvString = Papa.unparse(exportLogins);
var csvBlob = new Blob([csvString]);
if (window.navigator.msSaveOrOpenBlob) { // IE hack. ref http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
window.navigator.msSaveBlob(csvBlob, makeFileName());

View File

@@ -0,0 +1,281 @@
angular
.module('bit.tools')
.controller('toolsImportController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, toastr, importService, $analytics, $sce) {
$analytics.eventTrack('toolsImportController', { category: 'Modal' });
$scope.model = { source: 'bitwardencsv' };
$scope.source = {};
$scope.options = [
{
id: 'bitwardencsv',
name: 'bitwarden (csv)',
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)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-lastpass/">' +
'https://help.bitwarden.com/getting-started/import-from-lastpass/</a>')
},
{
id: 'chromecsv',
name: 'Chrome (csv)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-chrome/">' +
'https://help.bitwarden.com/getting-started/import-from-chrome/</a>')
},
{
id: 'firefoxpasswordexportercsvxml',
name: 'Firefox Password Exporter (xml)',
instructions: $sce.trustAsHtml('Use the ' +
'<a target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/password-exporter/">' +
'Password Exporter</a> addon for FireFox to export your passwords to a XML file. After installing ' +
'the addon, type <code>about:addons</code> 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)',
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)',
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)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
},
{
id: '1password6wincsv',
name: '1Password 6 Windows (csv)',
instructions: $sce.trustAsHtml('See detailed instructions on our help site at ' +
'<a target="_blank" href="https://help.bitwarden.com/getting-started/import-from-1password/">' +
'https://help.bitwarden.com/getting-started/import-from-1password/</a>')
},
{
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.')
},
{
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 <code>zoho_export.csv</code>.')
},
{
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 ' +
'<code>padlock_export.csv</code>.')
},
{
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.')
}
];
$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) {
$scope.processing = true;
var file = document.getElementById('file').files[0];
importService.import(model.source, file, importSuccess, importError);
};
function importSuccess(folders, logins, folderRelationships) {
if (!folders.length && !logins.length) {
importError('Nothing was imported.');
return;
}
else if (logins.length) {
var halfway = Math.floor(logins.length / 2);
var last = logins.length - 1;
if (loginIsBadData(logins[0]) && loginIsBadData(logins[halfway]) && loginIsBadData(logins[last])) {
importError('CSV data is not formatted correctly. Please check your import file and try again.');
return;
}
}
apiService.ciphers.import({
folders: cipherService.encryptFolders(folders, cryptoService.getKey()),
logins: cipherService.encryptLogins(logins, cryptoService.getKey()),
folderRelationships: folderRelationships
}, function () {
$uibModalInstance.dismiss('cancel');
$state.go('backend.vault').then(function () {
$analytics.eventTrack('Imported Data', { label: $scope.model.source });
toastr.success('Data has been successfully imported into your vault.', 'Import Success');
});
}, importError);
}
function loginIsBadData(login) {
return (login.name === null || login.name === '--') && (login.password === null || 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');
};
});

View File

@@ -0,0 +1,47 @@
<section class="content-header">
<h1>
Tools
<small>helpful utilities</small>
</h1>
</section>
<section class="content">
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Import</h3>
</div>
<div class="box-body">
Quickly import your logins and other data from a previous bitwarden export or from another
password manager application.
</div>
<div class="box-footer">
<button class="btn btn-default btn-flat" type="button" ng-click="import()">Import Data</button>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Export</h3>
</div>
<div class="box-body">
Export all of your vault data in <code>.csv</code> format.
</div>
<div class="box-footer">
<button class="btn btn-default btn-flat" type="button" ng-click="export()">Export Data</button>
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Password Generator</h3>
</div>
<div class="box-body">
Coming soon!
</div>
</div>
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title">Audits</h3>
</div>
<div class="box-body">
Coming soon!
</div>
</div>
</section>

View File

@@ -4,7 +4,12 @@
</div>
<form name="exportForm" ng-submit="exportForm.$valid && export(model)" api-form="exportPromise" ng-show="!startedExport">
<div class="modal-body">
<p>Export all of your vault data in <code>.csv</code> format. Enter your master password to continue.</p>
<p>Enter your master password to continue.</p>
<div class="callout callout-warning">
<h4><i class="fa fa-warning"></i> Warning</h4>
This export contains your <u>unencrypted</u> data in <code>.csv</code> format. You should not store or send it
over unsecure channels (such as email). Delete it immediately after your are done using it.
</div>
<div class="callout callout-danger validation-errors" ng-show="exportForm.$errors">
<h4>Errors have occured</h4>
<ul>

View File

@@ -5,17 +5,18 @@
<form name="importForm" ng-submit="importForm.$valid && import(model)" ng-show="!processing">
<div class="modal-body">
<div class="form-group">
<label for="source">Source</label>
<select id="source" name="source" class="form-control" ng-model="model.source">
<option value="local">bitwarden (csv)</option>
<option value="lastpass">LastPass (csv)</option>
<option value="safeincloudcsv">SafeInCloud (csv)</option>
<option value="keypassxml">KeyPass (xml)</option>
<label for="source">1. Select the source of this import file</label>
<select id="source" name="source" class="form-control" ng-model="model.source" ng-change="setSource()">
<option ng-repeat="option in options" value="{{option.id}}">{{option.name}}</option>
</select>
</div>
<div class="callout callout-default">
<h4><i class="fa fa-info-circle"></i> {{source.name}} Instructions</h4>
<div ng-bind-html="source.instructions"></div>
</div>
<div class="form-group">
<label for="file">File</label>
<input type="file" id="file" name="file" />
<label for="file">2. Select the import file</label>
<input type="file" id="file" name="file" required />
</div>
</div>
<div class="modal-footer">

View File

@@ -1,27 +1,27 @@
angular
.module('bit.vault')
.controller('vaultAddSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
$analytics.eventTrack('vaultAddSiteController', { category: 'Modal' });
.controller('vaultAddLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder, $analytics) {
$analytics.eventTrack('vaultAddLoginController', { category: 'Modal' });
$scope.folders = folders;
$scope.site = {
$scope.login = {
folderId: selectedFolder ? selectedFolder.id : null
};
$scope.savePromise = null;
$scope.save = function (model) {
var site = cipherService.encryptSite(model);
$scope.savePromise = apiService.sites.post(site, function (siteResponse) {
$analytics.eventTrack('Created Site');
var decSite = cipherService.decryptSite(siteResponse);
$uibModalInstance.close(decSite);
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.logins.post(login, function (loginResponse) {
$analytics.eventTrack('Created Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close(decLogin);
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.site.password || confirm('Are you sure you want to overwrite the current password?')) {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Add');
$scope.site.password = passwordService.generatePassword({ length: 10, special: true });
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
@@ -37,6 +37,10 @@
alert('Your web browser does not support easy clipboard copying. Copy it manually instead.');
};
$scope.folderSort = function (item) {
return item.name.toLowerCase();
};
function selectPassword(e) {
var target = $(e.trigger).parent().prev();
if (target.attr('type') === 'text') {

View File

@@ -2,35 +2,35 @@
.module('bit.vault')
.controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr, cipherService) {
$scope.sites = [];
$scope.logins = [];
$scope.folders = [];
$scope.loadingSites = true;
apiService.sites.list({}, function (sites) {
$scope.loadingSites = false;
$scope.loadingLogins = true;
apiService.logins.list({}, function (logins) {
$scope.loadingLogins = false;
var decSites = [];
for (var i = 0; i < sites.Data.length; i++) {
var decSite = {
id: sites.Data[i].Id,
folderId: sites.Data[i].FolderId,
favorite: sites.Data[i].Favorite
var decLogins = [];
for (var i = 0; i < logins.Data.length; i++) {
var decLogin = {
id: logins.Data[i].Id,
folderId: logins.Data[i].FolderId,
favorite: logins.Data[i].Favorite
};
try { decSite.name = cryptoService.decrypt(sites.Data[i].Name); }
catch (err) { decSite.name = '[error: cannot decrypt]'; }
try { decLogin.name = cryptoService.decrypt(logins.Data[i].Name); }
catch (err) { decLogin.name = '[error: cannot decrypt]'; }
if (sites.Data[i].Username) {
try { decSite.username = cryptoService.decrypt(sites.Data[i].Username); }
catch (err) { decSite.username = '[error: cannot decrypt]'; }
if (logins.Data[i].Username) {
try { decLogin.username = cryptoService.decrypt(logins.Data[i].Username); }
catch (err) { decLogin.username = '[error: cannot decrypt]'; }
}
decSites.push(decSite);
decLogins.push(decLogin);
}
$scope.sites = decSites;
$scope.logins = decLogins;
}, function () {
$scope.loadingSites = false;
$scope.loadingLogins = false;
});
$scope.loadingFolders = true;
@@ -66,56 +66,70 @@
return item.name.toLowerCase();
};
$scope.editSite = function (site) {
$scope.editLogin = function (login) {
var editModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultEditSite.html',
controller: 'vaultEditSiteController',
templateUrl: 'app/vault/views/vaultEditLogin.html',
controller: 'vaultEditLoginController',
resolve: {
siteId: function () { return site.id; },
loginId: function () { return login.id; },
folders: function () { return $scope.folders; }
}
});
editModel.result.then(function (editedSite) {
var site = $filter('filter')($scope.sites, { id: editedSite.id }, true);
if (site && site.length > 0) {
site[0].folderId = editedSite.folderId;
site[0].name = editedSite.name;
site[0].username = editedSite.username;
site[0].favorite = editedSite.favorite;
editModel.result.then(function (returnVal) {
if (returnVal.action === 'edit') {
var loginToUpdate = $filter('filter')($scope.logins, { id: returnVal.data.id }, true);
if (loginToUpdate && loginToUpdate.length > 0) {
loginToUpdate[0].folderId = returnVal.data.folderId;
loginToUpdate[0].name = returnVal.data.name;
loginToUpdate[0].username = returnVal.data.username;
loginToUpdate[0].favorite = returnVal.data.favorite;
}
}
else if (returnVal.action === 'delete') {
var loginToDelete = $filter('filter')($scope.logins, { id: returnVal.data }, true);
if (loginToDelete && loginToDelete.length > 0) {
var index = $scope.logins.indexOf(loginToDelete[0]);
if (index > -1) {
$scope.logins.splice(index, 1);
}
}
}
});
};
$scope.$on('vaultAddSite', function (event, args) {
$scope.addSite();
$scope.$on('vaultAddLogin', function (event, args) {
$scope.addLogin();
});
$scope.addSite = function (folder) {
$scope.addLogin = function (folder) {
var addModel = $uibModal.open({
animation: true,
templateUrl: 'app/vault/views/vaultAddSite.html',
controller: 'vaultAddSiteController',
templateUrl: 'app/vault/views/vaultAddLogin.html',
controller: 'vaultAddLoginController',
resolve: {
folders: function () { return $scope.folders; },
selectedFolder: function () { return folder; }
}
});
addModel.result.then(function (addedSite) {
$scope.sites.push(addedSite);
addModel.result.then(function (addedLogin) {
$scope.logins.push(addedLogin);
});
};
$scope.deleteSite = function (site) {
if (!confirm('Are you sure you want to delete this site (' + site.name + ')?')) {
$scope.deleteLogin = function (login) {
if (!confirm('Are you sure you want to delete this login (' + login.name + ')?')) {
return;
}
apiService.sites.del({ id: site.id }, function () {
var index = $scope.sites.indexOf(site);
$scope.sites.splice(index, 1);
apiService.logins.del({ id: login.id }, function () {
var index = $scope.logins.indexOf(login);
if (index > -1) {
$scope.logins.splice(index, 1);
}
});
};
@@ -162,7 +176,9 @@
apiService.folders.del({ id: folder.id }, function () {
var index = $scope.folders.indexOf(folder);
$scope.folders.splice(index, 1);
if (index > -1) {
$scope.folders.splice(index, 1);
}
});
};
@@ -171,7 +187,7 @@
return false;
}
var sites = $filter('filter')($scope.sites, { folderId: folder.id });
return sites.length === 0;
var logins = $filter('filter')($scope.logins, { folderId: folder.id });
return logins.length === 0;
};
});

View File

@@ -0,0 +1,71 @@
angular
.module('bit.vault')
.controller('vaultEditLoginController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, loginId, folders, $analytics) {
$analytics.eventTrack('vaultEditLoginController', { category: 'Modal' });
$scope.folders = folders;
$scope.login = {};
apiService.logins.get({ id: loginId }, function (login) {
$scope.login = cipherService.decryptLogin(login);
});
$scope.save = function (model) {
var login = cipherService.encryptLogin(model);
$scope.savePromise = apiService.logins.put({ id: loginId }, login, function (loginResponse) {
$analytics.eventTrack('Edited Login');
var decLogin = cipherService.decryptLogin(loginResponse);
$uibModalInstance.close({
action: 'edit',
data: decLogin
});
}).$promise;
};
$scope.generatePassword = function () {
if (!$scope.login.password || confirm('Are you sure you want to overwrite the current password?')) {
$analytics.eventTrack('Generated Password From Edit');
$scope.login.password = passwordService.generatePassword({ length: 12, special: true });
}
};
$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) {
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 login (' + $scope.login.name + ')?')) {
return;
}
apiService.logins.del({ id: $scope.login.id }, function () {
$uibModalInstance.close({
action: 'delete',
data: $scope.login.id
});
});
};
$scope.close = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@@ -1,57 +1,60 @@
<section class="content-header">
<h1>
My Vault
<small>safe and secure</small>
<small>{{folders.length > 0 ? folders.length - 1 : 0}} folders, {{logins.length}} logins</small>
</h1>
</section>
<section class="content">
<div ng-show="loadingFolders && !folders.length">
<p>Loading...</p>
</div>
<div class="box" ng-repeat="folder in folders | orderBy: folderSort" ng-show="folders.length">
<div class="box" ng-repeat="folder in folders | orderBy: folderSort" ng-show="folders.length && (!main.searchVaultText || folderLogins.length)">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}}</h3>
<h3 class="box-title"><i class="fa fa-folder-open"></i> {{folder.name}} <small>{{folderLogins.length}} logins</small></h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" ng-click="addLogin(folder)" uib-tooltip="Add Login">
<i class="fa fa-plus"></i>
</button>
<button type="button" class="btn btn-box-tool" ng-click="deleteFolder(folder)" ng-show="canDeleteFolder(folder)" uib-tooltip="Delete">
<i class="fa fa-trash"></i>
</button>
<button type="button" class="btn btn-box-tool" ng-click="editFolder(folder)" ng-show="folder.id" uib-tooltip="Edit">
<i class="fa fa-pencil"></i>
</button>
<button type="button" class="btn btn-box-tool" data-widget="collapse" uib-tooltip="Collapse">
<button type="button" class="btn btn-box-tool" data-widget="collapse" uib-tooltip="Collapse/Expand">
<i class="fa fa-minus"></i>
</button>
</div>
</div>
<div class="box-body" ng-class="{'no-padding': folderSites.length}">
<div ng-show="loadingSites && !folderSites.length">
<p>Loading sites...</p>
<div class="box-body" ng-class="{'no-padding': folderLogins.length}">
<div ng-show="loadingLogins && !folderLogins.length">
<p>Loading logins...</p>
</div>
<div ng-show="!loadingSites && !folderSites.length">
<p>No sites in this folder.</p>
<button type="button" ng-click="addSite(folder)" class="btn btn-default btn-flat">Add a Site</button>
<div ng-show="!loadingLogins && !folderLogins.length">
<p>No logins in this folder.</p>
<button type="button" ng-click="addLogin(folder)" class="btn btn-default btn-flat">Add a Login</button>
</div>
<div class="table-responsive" ng-show="folderSites.length">
<div class="table-responsive" ng-show="folderLogins.length">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 75px; min-width: 75px;"></th>
<th>Site</th>
<th>Name</th>
<th style="width: 300px;">Username</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="site in folderSites = (sites | filter: { folderId: folder.id } | filter: (main.searchVaultText || '') | orderBy: ['name', 'username'])">
<tr ng-repeat="login in folderLogins = (logins | filter: { folderId: folder.id } | filter: (main.searchVaultText || '') | orderBy: ['name', 'username'])">
<td>
<button type="button" ng-click="deleteSite(site)" class="btn btn-link btn-table" uib-tooltip="Delete"><i class="fa fa-lg fa-trash"></i></button>
<button type="button" ng-click="editSite(site)" class="btn btn-link btn-table" uib-tooltip="View/Edit"><i class="fa fa-lg fa-pencil"></i></button>
<button type="button" ng-click="deleteLogin(login)" class="btn btn-link btn-table" uib-tooltip="Delete"><i class="fa fa-lg fa-trash"></i></button>
<button type="button" ng-click="editLogin(login)" class="btn btn-link btn-table" uib-tooltip="View/Edit"><i class="fa fa-lg fa-pencil"></i></button>
</td>
<td>{{site.name}} <i class="fa fa-star text-muted" uib-tooltip="Favorite" ng-show="site.favorite"></i></td>
<td>{{site.username}}</td>
<td>{{login.name}} <i class="fa fa-star text-muted" uib-tooltip="Favorite" ng-show="login.favorite"></i></td>
<td>{{login.username}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
</section>

View File

@@ -11,7 +11,7 @@
</ul>
</div>
<div class="form-group" show-errors>
<label for="name">Name</label>
<label for="name">Name</label> <span>*</span>
<input type="text" id="name" name="Name" ng-model="folder.name" class="form-control" required api-field />
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More