diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..c91485a9f53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,202 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studo 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.[Cc]ache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +bower_components/ +lib/ +css/ +dist/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Other +project.lock.json \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 00000000000..c3c2967a3bf --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000000..cfd947407e0 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# bitwarden Vault diff --git a/bitwarden-vault.sln b/bitwarden-vault.sln new file mode 100644 index 00000000000..062b168418c --- /dev/null +++ b/bitwarden-vault.sln @@ -0,0 +1,35 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +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 + global.json = global.json + NuGet.Config = NuGet.Config + README.md = README.md + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Vault", "src\Vault\Vault.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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0BEBF47C-BA0B-48AC-B48C-718F94084AD5} = {860863C9-0436-43D4-840D-FE919C9F6FFC} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 00000000000..38c762a32d3 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-rc1-final" + } +} diff --git a/src/Vault/Properties/AssemblyInfo.cs b/src/Vault/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..4e5c734e3af --- /dev/null +++ b/src/Vault/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +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.Vault")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("bitwarden Vault")] +[assembly: AssemblyProduct("bitwarden Vault")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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")] diff --git a/src/Vault/Properties/launchSettings.json b/src/Vault/Properties/launchSettings.json new file mode 100644 index 00000000000..323b7c66775 --- /dev/null +++ b/src/Vault/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:4001/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "Hosting:Environment": "Development" + } + }, + "web": { + "commandName": "web", + "environmentVariables": { + "Hosting:Environment": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Vault/Startup.cs b/src/Vault/Startup.cs new file mode 100644 index 00000000000..ea5f2bb9647 --- /dev/null +++ b/src/Vault/Startup.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Vault +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) { } + + public void Configure(IApplicationBuilder app) + { + app.UseIISPlatformHandler(); + app.UseFileServer(); + } + + // Entry point for the application. + public static void Main(string[] args) => WebApplication.Run(args); + } +} diff --git a/src/Vault/Vault.xproj b/src/Vault/Vault.xproj new file mode 100644 index 00000000000..a890f7f5a51 --- /dev/null +++ b/src/Vault/Vault.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 0bebf47c-ba0b-48ac-b48c-718f94084ad5 + Bit.Vault + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 4001 + + + \ No newline at end of file diff --git a/src/Vault/gulpfile.js b/src/Vault/gulpfile.js new file mode 100644 index 00000000000..b028d4f1be7 --- /dev/null +++ b/src/Vault/gulpfile.js @@ -0,0 +1,312 @@ +/// + +var gulp = require('gulp'), + rimraf = require('rimraf'), + concat = require('gulp-concat'), + rename = require('gulp-rename'), + cssmin = require('gulp-cssmin'), + uglify = require('gulp-uglify'), + ghPages = require('gulp-gh-pages'), + less = require('gulp-less'), + 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'), + jshint = require('gulp-jshint'), + _ = require('lodash'); + +var paths = {}; +paths.dist = '../../dist/'; +paths.webroot = './wwwroot/' +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.cssDir = paths.webroot + 'css/'; +paths.jsDir = paths.webroot + 'js/'; + +gulp.task('lint', function () { + return gulp.src(paths.webroot + 'app/**/*.js') + .pipe(jshint()) + .pipe(jshint.reporter('default')); +}); + +gulp.task('build', function (cb) { + return runSequence( + 'clean', + ['lib', 'less', 'settings', 'lint'], + cb); +}); + +gulp.task('clean:js', function (cb) { + return rimraf(paths.concatJsDest, cb); +}); + +gulp.task('clean:css', function (cb) { + return rimraf(paths.cssDir, cb); +}); + +gulp.task('clean:lib', function (cb) { + return rimraf(paths.libDir, cb); +}); + +gulp.task('clean', ['clean:js', 'clean:css', 'clean:lib', 'dist:clean']); + +gulp.task('min:js', ['clean:js'], function () { + return gulp.src([paths.js, '!' + paths.minJs], { base: '.' }) + .pipe(concat(paths.concatJsDest)) + .pipe(uglify()) + .pipe(gulp.dest('.')); +}); + +gulp.task('min:css', [], function () { + return gulp.src([paths.cssDir + '**/*.css', '!' + paths.cssDir + '**/*.min.css'], { base: '.' }) + .pipe(cssmin()) + .pipe(rename({ suffix: '.min' })) + .pipe(gulp.dest('.')); +}); + +gulp.task('min', ['min:js', 'min:css']); + +gulp.task('lib', ['clean:lib'], function () { + var libs = [ + { + src: [ + paths.npmDir + 'bootstrap/dist/**/*', + '!' + paths.npmDir + 'bootstrap/dist/**/npm.js', + '!' + paths.npmDir + 'bootstrap/dist/**/css/*theme*' + ], + dest: paths.libDir + 'bootstrap' + }, + { + src: paths.npmDir + 'font-awesome/css/*', + dest: paths.libDir + 'font-awesome/css' + }, + { + src: paths.npmDir + 'font-awesome/fonts/*', + dest: paths.libDir + 'font-awesome/fonts' + }, + { + src: paths.npmDir + 'jquery/dist/*.js', + dest: paths.libDir + 'jquery' + }, + { + src: paths.npmDir + 'admin-lte/dist/js/app*.js', + dest: paths.libDir + 'admin-lte/js' + }, + { + src: paths.npmDir + 'angular/angular*.js', + dest: paths.libDir + 'angular' + }, + { + src: paths.npmDir + 'angular-bootstrap-npm/dist/*tpls*.js', + dest: paths.libDir + 'angular-bootstrap' + }, + { + src: paths.npmDir + 'angular-bootstrap-show-errors/src/*.js', + dest: paths.libDir + 'angular-bootstrap-show-errors' + }, + { + src: paths.npmDir + 'angular-cookies/*cookies*.js', + dest: paths.libDir + 'angular-cookies' + }, + { + src: paths.npmDir + 'angular-jwt/dist/*.js', + dest: paths.libDir + 'angular-jwt' + }, + { + src: paths.npmDir + 'angular-md5/angular-md5*.js', + dest: paths.libDir + 'angular-md5' + }, + { + src: paths.npmDir + 'angular-resource/*resource*.js', + dest: paths.libDir + 'angular-resource' + }, + { + src: [paths.npmDir + 'angular-toastr/dist/**/*.css', paths.npmDir + 'angular-toastr/dist/**/*.js'], + dest: paths.libDir + 'angular-toastr' + }, + { + src: paths.npmDir + 'angular-ui-router/release/*.js', + dest: paths.libDir + 'angular-ui-router' + }, + { + 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' + }, + { + src: paths.npmDir + 'papaparse/papaparse*.js', + dest: paths.libDir + 'papaparse' + }, + { + src: paths.npmDir + 'ngclipboard/dist/ngclipboard*.js', + dest: paths.libDir + 'ngclipboard' + }, + { + src: paths.npmDir + 'clipboard/dist/clipboard*.js', + dest: paths.libDir + 'clipboard' + } + ]; + + var tasks = libs.map(function (lib) { + return gulp.src(lib.src).pipe(gulp.dest(lib.dest)); + }); + + return merge(tasks); +}); + +gulp.task('settings', function () { + return config() + .pipe(gulp.dest(paths.webroot + 'app')); +}); + +function config() { + return gulp.src('./settings.json') + .pipe(ngConfig('bit', { + createModule: false, + constants: _.merge({}, { + appSettings: { + version: project.version, + environment: project.environment + } + }, require('./settings.' + project.environment + '.json') || {}) + })); +} + +gulp.task('less', function () { + return gulp.src(paths.lessDir + 'vault.less') + .pipe(less()) + .pipe(gulp.dest(paths.cssDir)); +}); + +gulp.task('watch', function () { + gulp.watch(paths.lessDir + '*.less', ['less']); + gulp.watch('./settings*.json', ['settings']); +}); + +gulp.task('dist:clean', function (cb) { + return rimraf(paths.dist, cb); +}); + +gulp.task('dist:move', function () { + var moves = [ + { + src: [ + paths.npmDir + 'bootstrap/dist/**/bootstrap.min.js', + paths.npmDir + 'bootstrap/dist/**/bootstrap.min.css', + paths.npmDir + 'bootstrap/dist/**/fonts/**/*', + ], + dest: paths.dist + 'lib/bootstrap' + }, + { + src: [ + paths.npmDir + 'font-awesome/**/font-awesome.min.css', + paths.npmDir + 'font-awesome/**/fonts/**/*' + ], + dest: paths.dist + 'lib/font-awesome' + }, + { + src: paths.npmDir + 'jquery/dist/jquery.min.js', + dest: paths.dist + 'lib/jquery' + }, + { + src: paths.npmDir + 'angular/angular.min.js', + dest: paths.dist + 'lib/angular' + }, + { + src: [ + paths.webroot + '**/app/**/*.html', + paths.webroot + '**/images/**/*', + paths.webroot + 'index.html', + paths.webroot + 'favicon.ico' + ], + dest: paths.dist + } + ]; + + var tasks = moves.map(function (move) { + return gulp.src(move.src).pipe(gulp.dest(move.dest)); + }); + + return merge(tasks); +}); + +gulp.task('dist:css', function () { + return gulp + .src([ + paths.cssDir + '**/*.css', + '!' + paths.cssDir + '**/*.min.css' + ]) + .pipe(preprocess({ context: settings })) + .pipe(cssmin()) + .pipe(rename({ suffix: '.min' })) + .pipe(gulp.dest(paths.dist + 'css')); +}); + +gulp.task('dist:js:app', function () { + var mainStream = gulp + .src([ + paths.webroot + 'app/app.js', + '!' + paths.webroot + 'app/settings.js', + paths.webroot + 'app/**/*Module.js', + paths.webroot + 'app/**/*.js' + ]); + + merge(mainStream, config()) + .pipe(preprocess({ context: settings })) + .pipe(concat(paths.dist + '/js/app.min.js')) + .pipe(ngAnnotate()) + .pipe(uglify()) + .pipe(gulp.dest('.')); +}); + +gulp.task('dist:js:lib', function () { + return gulp + .src([ + paths.libDir + 'sjcl/sjcl.js', + paths.libDir + 'sjcl/*.js', + paths.libDir + '**/*.js', + '!' + paths.libDir + '**/*.min.js', + '!' + paths.libDir + 'angular/**/*', + '!' + paths.libDir + 'bootstrap/**/*', + '!' + paths.libDir + 'jquery/**/*' + ]) + .pipe(concat(paths.dist + '/js/lib.min.js')) + .pipe(uglify()) + .pipe(gulp.dest('.')); +}); + +gulp.task('dist:preprocess', function () { + return gulp + .src([ + paths.dist + '/**/*.html' + ], { base: '.' }) + .pipe(preprocess({ context: settings })) + .pipe(gulp.dest('.')); +}); + +gulp.task('dist', ['build'], function (cb) { + return runSequence( + 'dist:clean', + ['dist:move', 'dist:css', 'dist:js:app', 'dist:js:lib'], + 'dist:preprocess', + cb); +}); + +gulp.task('deploy', ['dist'], function () { + return gulp.src(paths.dist + '**/*') + .pipe(ghPages({ cacheDir: paths.dist + '.publish' })); +}); diff --git a/src/Vault/less/theme.less b/src/Vault/less/theme.less new file mode 100644 index 00000000000..7b907ce677c --- /dev/null +++ b/src/Vault/less/theme.less @@ -0,0 +1,47 @@ +@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"; diff --git a/src/Vault/less/vault.less b/src/Vault/less/vault.less new file mode 100644 index 00000000000..36bd289bed7 --- /dev/null +++ b/src/Vault/less/vault.less @@ -0,0 +1,170 @@ +@import "theme.less"; + +/* Theme Adjustments */ + +@boxed-layout-bg-image-path: "../images/boxed-bg.png"; + +body { + background-color: @gray; + .img-retina(@boxed-layout-bg-image-path, "../images/boxed-bg-2x.png", auto, auto); +} + +body, +.main-header .logo, +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.box-body > .table-responsive { + > .table { + margin-bottom: 0; + } + + @media screen and (max-width: @screen-xs-max) { + border: none; + margin-bottom: 0; + } +} + +.sidebar-form .form-group { + margin-bottom: 0; + + input[type="text"], .form-control-feedback { + color: #999; + } +} + +form div.validation-errors ul { + margin-bottom: 0; + padding-left: 20px; + + li { + margin-left: 0; + } +} + +.login-page, +.register-page { + background-color: @gray; + background-repeat: repeat; + background-attachment: fixed; + .img-retina(@boxed-layout-bg-image-path, "../images/boxed-bg-2x.png", auto, auto); +} + +.login-box-body, +.register-box-body { + .boxShadow(0 0 8px rgba(0, 0, 0, 0.5)); +} + +.login-box, .register-box { + .checkbox { + margin-top: 0; + } + + ul { + margin-bottom: 0; + padding-left: 20px; + } +} + +.password-options { + float: right; + + i { + margin: 0 5px; + cursor: pointer; + } +} + +/* Buttons */ + +.btn-table { + padding: 1px 5px; + line-height: 1; +} + +.btn-box-tool { + font-size: 14px; +} + +form .btn .loading-icon { + margin-right: 8px; +} + +/* Modals */ + +.modal-footer { + text-align: left; +} + +/* Toastr */ + +#toast-container { + position: absolute; + + &.toast-top-right { + top: 65px; + right: 15px; + + @media (max-width: @screen-xs-max) { + top: initial; + bottom: 0; + right: 0; + width: 100%; + } + } + + > .toast { + background-image: none !important; + .border-radius(0); + .boxShadow(0 0 8px rgba(0, 0, 0, 0.5)); + + @media (max-width: @screen-xs-max) { + width: 100%; + } + + &.toast-danger, &.toast-error { + &:extend(.bg-red); + + &:before { + content: "\f0e7"; + } + } + + &.toast-warning { + &:extend(.bg-yellow); + + &:before { + content: "\f071"; + } + } + + &.toast-info { + &:extend(.bg-aqua); + + &:before { + content: "\f005"; + } + } + + &.toast-success { + &:extend(.bg-green); + + &:before { + content: "\f00C"; + } + } + + &:before { + position: fixed; + font-family: FontAwesome; + font-size: 24px; + line-height: 24px; + float: left; + color: #ffffff; + padding-right: 0.5em; + margin: auto 0.5em auto -1.5em; + } + } +} diff --git a/src/Vault/package.json b/src/Vault/package.json new file mode 100644 index 00000000000..695934ded27 --- /dev/null +++ b/src/Vault/package.json @@ -0,0 +1,43 @@ +{ + "name": "bitwarden", + "version": "0.0.1", + "devDependencies": { + "connect": "^3.4.0", + "lodash": "3.10.1", + "gulp": "3.9.0", + "gulp-concat": "2.6.0", + "gulp-cssmin": "0.1.7", + "gulp-less": "3.0.3", + "gulp-rename": "1.2.2", + "gulp-uglify": "1.4.0", + "gulp-gh-pages": "0.5.4", + "gulp-preprocess": "1.2.0", + "gulp-ng-annotate": "1.1.0", + "gulp-ng-config": "1.2.1", + "jshint": "2.9.1-rc1", + "gulp-jshint": "2.0.0", + "rimraf": "2.4.3", + "run-sequence": "1.1.3", + "merge-stream": "1.0.0", + "jquery": "2.1.4", + "font-awesome": "4.4.0", + "bootstrap": "3.3.5", + "sjcl": "1.0.3", + "angular": "1.4.7", + "angular-resource": "1.4.7", + "angular-bootstrap-npm": "0.14.3", + "angular-ui-router": "0.2.15", + "angular-jwt": "0.0.9", + "angular-cookies": "1.4.7", + "admin-lte": "2.3.2", + "angular-md5": "0.1.8", + "angular-toastr": "1.5.0", + "angular-bootstrap-show-errors": "2.3.0", + "angular-messages": "1.4.7", + "ngstorage": "0.3.10", + "papaparse": "4.1.2", + "toastr": "2.1.2", + "clipboard": "1.5.5", + "ngclipboard": "1.0.0" + } +} diff --git a/src/Vault/project.json b/src/Vault/project.json new file mode 100644 index 00000000000..26ec0c3b608 --- /dev/null +++ b/src/Vault/project.json @@ -0,0 +1,27 @@ +{ + "version": "0.0.1", + "environment": "Development", + + "dependencies": { + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final", + "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final" + }, + + "commands": { + "web": "Microsoft.AspNet.Server.Kestrel" + }, + + "frameworks": { + "dnx451": { } + }, + + "exclude": [ + "wwwroot", + "node_modules" + ], + "publishExclude": [ + "**.user", + "**.vspscc" + ] +} diff --git a/src/Vault/settings.Development.json b/src/Vault/settings.Development.json new file mode 100644 index 00000000000..d2bc61c9990 --- /dev/null +++ b/src/Vault/settings.Development.json @@ -0,0 +1,5 @@ +{ + "appSettings": { + "apiUri": "http://localhost:4000" + } +} diff --git a/src/Vault/settings.Production.json b/src/Vault/settings.Production.json new file mode 100644 index 00000000000..3be6a9c81dc --- /dev/null +++ b/src/Vault/settings.Production.json @@ -0,0 +1,5 @@ +{ + "appSettings": { + "apiUri": "https://api.bitwarden.com" + } +} diff --git a/src/Vault/settings.Staging.json b/src/Vault/settings.Staging.json new file mode 100644 index 00000000000..3be6a9c81dc --- /dev/null +++ b/src/Vault/settings.Staging.json @@ -0,0 +1,5 @@ +{ + "appSettings": { + "apiUri": "https://api.bitwarden.com" + } +} diff --git a/src/Vault/settings.json b/src/Vault/settings.json new file mode 100644 index 00000000000..2e50bde1f9c --- /dev/null +++ b/src/Vault/settings.json @@ -0,0 +1,5 @@ +{ + "appSettings": { + "rememberdEmailCookieName": "bit.rememberedEmail" + } +} diff --git a/src/Vault/wwwroot/_references.js b/src/Vault/wwwroot/_references.js new file mode 100644 index 00000000000..725d3ce1dc4 --- /dev/null +++ b/src/Vault/wwwroot/_references.js @@ -0,0 +1,48 @@ +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// diff --git a/src/Vault/wwwroot/app/accounts/accountsLoginController.js b/src/Vault/wwwroot/app/accounts/accountsLoginController.js new file mode 100644 index 00000000000..01a1883f79d --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/accountsLoginController.js @@ -0,0 +1,48 @@ +angular + .module('bit.accounts') + + .controller('accountsLoginController', function ($scope, $rootScope, $cookies, apiService, cryptoService, authService, $state, appSettings) { + var rememberedEmail = $cookies.get(appSettings.rememberdEmailCookieName); + if (rememberedEmail) { + $scope.model = { + email: rememberedEmail, + rememberEmail: true + }; + } + + $scope.login = function (model) { + $scope.loginPromise = authService.logIn(model.email, model.masterPassword); + + $scope.loginPromise.then(function () { + if (model.rememberEmail) { + var cookieExpiration = new Date(); + cookieExpiration.setFullYear(cookieExpiration.getFullYear() + 10); + + $cookies.put( + appSettings.rememberdEmailCookieName, + model.email, + { expires: cookieExpiration }); + } + else { + $cookies.remove(appSettings.rememberdEmailCookieName); + } + + var profile = authService.getUserProfile(); + if (profile.twoFactor) { + $state.go('frontend.login.twoFactor'); + } + else { + $state.go('backend.vault'); + } + }); + }; + + $scope.twoFactor = function (model) { + // Only supporting Authenticator provider for now + $scope.twoFactorPromise = authService.logInTwoFactor(model.code, "Authenticator"); + + $scope.twoFactorPromise.then(function () { + $state.go('backend.vault'); + }); + }; + }); diff --git a/src/Vault/wwwroot/app/accounts/accountsLogoutController.js b/src/Vault/wwwroot/app/accounts/accountsLogoutController.js new file mode 100644 index 00000000000..ff513bd4ddf --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/accountsLogoutController.js @@ -0,0 +1,7 @@ +angular + .module('bit.accounts') + + .controller('accountsLogoutController', function ($scope, authService, $state) { + authService.logOut(); + $state.go('frontend.login.info'); + }); diff --git a/src/Vault/wwwroot/app/accounts/accountsModule.js b/src/Vault/wwwroot/app/accounts/accountsModule.js new file mode 100644 index 00000000000..dfa7f48bce8 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/accountsModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.accounts', ['ui.bootstrap', 'ngCookies']); diff --git a/src/Vault/wwwroot/app/accounts/accountsPasswordHintController.js b/src/Vault/wwwroot/app/accounts/accountsPasswordHintController.js new file mode 100644 index 00000000000..45fe8fce99a --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/accountsPasswordHintController.js @@ -0,0 +1,12 @@ +angular + .module('bit.accounts') + + .controller('accountsPasswordHintController', function ($scope, $rootScope, apiService) { + $scope.success = false; + + $scope.submit = function (model) { + $scope.submitPromise = apiService.accounts.postPasswordHint({ email: model.email }, function () { + $scope.success = true; + }).$promise; + }; + }); diff --git a/src/Vault/wwwroot/app/accounts/accountsRegisterController.js b/src/Vault/wwwroot/app/accounts/accountsRegisterController.js new file mode 100644 index 00000000000..14563434cb0 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/accountsRegisterController.js @@ -0,0 +1,13 @@ +angular + .module('bit.accounts') + + .controller('accountsRegisterController', function ($scope, $rootScope, apiService) { + $scope.success = false; + + $scope.registerPromise = null; + $scope.register = function (model) { + $scope.registerPromise = apiService.accounts.registerToken({ email: model.email }, function () { + $scope.success = true; + }).$promise; + }; + }); diff --git a/src/Vault/wwwroot/app/accounts/accountsRegisterFinalizeController.js b/src/Vault/wwwroot/app/accounts/accountsRegisterFinalizeController.js new file mode 100644 index 00000000000..d0484d741dc --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/accountsRegisterFinalizeController.js @@ -0,0 +1,48 @@ +angular + .module('bit.accounts') + + .controller('accountsRegisterFinalizeController', function ($scope, $rootScope, $location, $state, apiService, cryptoService, validationService) { + var params = $location.search(); + + if (!params.token || !params.email) { + $state.go('frontend.login.info'); + return; + } + + $scope.success = false; + $scope.model = { + email: params.email, + token: params.token + }; + + $scope.info = function () { + $scope.model.confirmMasterPassword = null; + $state.go('frontend.registerFinalize.confirm'); + }; + + $scope.confirmPromise = null; + $scope.confirm = function (form) { + if ($scope.model.masterPassword != $scope.model.confirmMasterPassword) { + validationService.addError(form, 'ConfirmMasterPassword', 'Master password confirmation does not match.', true); + return; + } + + var key = cryptoService.makeKey($scope.model.masterPassword, $scope.model.email); + var request = { + token: $scope.model.token, + name: $scope.model.name, + email: $scope.model.email, + masterPasswordHash: cryptoService.hashPassword($scope.model.masterPassword, key), + masterPasswordHint: $scope.model.masterPasswordHint + }; + + $scope.confirmPromise = apiService.accounts.register(request, function () { + $scope.success = true; + }).$promise; + }; + + $scope.loadInfo = function () { + $scope.model.masterPassword = null; + window.history.back(); + }; + }); diff --git a/src/Vault/wwwroot/app/accounts/views/accountsLogin.html b/src/Vault/wwwroot/app/accounts/views/accountsLogin.html new file mode 100644 index 00000000000..5775b44a9c7 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsLogin.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/Vault/wwwroot/app/accounts/views/accountsLoginInfo.html b/src/Vault/wwwroot/app/accounts/views/accountsLoginInfo.html new file mode 100644 index 00000000000..7bf478aa1a1 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsLoginInfo.html @@ -0,0 +1,40 @@ + +
+
+

Errors have occured

+
    +
  • {{e}}
  • +
+
+
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+ +
+
+
+ +
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/accounts/views/accountsLoginTwoFactor.html b/src/Vault/wwwroot/app/accounts/views/accountsLoginTwoFactor.html new file mode 100644 index 00000000000..db7fed6e7df --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsLoginTwoFactor.html @@ -0,0 +1,22 @@ + +
+
+

Errors have occured

+
    +
  • {{e}}
  • +
+
+
+ + + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/accounts/views/accountsPasswordHint.html b/src/Vault/wwwroot/app/accounts/views/accountsPasswordHint.html new file mode 100644 index 00000000000..d4f7f05d0cd --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsPasswordHint.html @@ -0,0 +1,37 @@ + diff --git a/src/Vault/wwwroot/app/accounts/views/accountsRegister.html b/src/Vault/wwwroot/app/accounts/views/accountsRegister.html new file mode 100644 index 00000000000..f4091e3a5a2 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsRegister.html @@ -0,0 +1,37 @@ +
+ +
+ +
+
+

Almost done!

Check your email ({{model.email}}) to complete your registration. +
+ Ready to log in? +
+
+
+

Errors have occured

+
    +
  • {{e}}
  • +
+
+
+ + + +
+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalize.html b/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalize.html new file mode 100644 index 00000000000..dd5b4156794 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalize.html @@ -0,0 +1,7 @@ +
+ +
+
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalizeConfirm.html b/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalizeConfirm.html new file mode 100644 index 00000000000..b7bbffca246 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalizeConfirm.html @@ -0,0 +1,35 @@ + +
+
+

You're Registered!

+

You may now log in to your new account.

+
+ Ready to log in? +
+
+
+

Errors have occured

+
    +
  • {{e}}
  • +
+
+
+ + + +

It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it.

+
+
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalizeInfo.html b/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalizeInfo.html new file mode 100644 index 00000000000..abe8cbd7314 --- /dev/null +++ b/src/Vault/wwwroot/app/accounts/views/accountsRegisterFinalizeInfo.html @@ -0,0 +1,35 @@ + +
+
+ + + +
+
+ + + +

What should we call you?

+
+
+ + + +

The master password is the password you use to access your vault.

+
+
+ + + +

A master password hint can help you remember your password if you forget it.

+
+
+
+ +
+ +
+
+
diff --git a/src/Vault/wwwroot/app/apiInterceptor.js b/src/Vault/wwwroot/app/apiInterceptor.js new file mode 100644 index 00000000000..5040b83ea65 --- /dev/null +++ b/src/Vault/wwwroot/app/apiInterceptor.js @@ -0,0 +1,29 @@ +angular + .module('bit') + + .factory('apiInterceptor', function ($injector, $q, toastr) { + return { + request: function (config) { + return config; + }, + response: function (response) { + if (response.status === 401 || response.status == 403) { + $injector.get('authService').logOut(); + $injector.get('$state').go('frontend.login.info').then(function () { + toastr.warning('Your login session has expired.', 'Logged out'); + }); + } + + return response || $q.when(response); + }, + responseError: function (rejection) { + if (rejection.status === 401 || rejection.status == 403) { + $injector.get('authService').logOut(); + $injector.get('$state').go('frontend.login.info').then(function () { + toastr.warning('Your login session has expired.', 'Logged out'); + }); + } + return $q.reject(rejection); + } + }; + }); \ No newline at end of file diff --git a/src/Vault/wwwroot/app/app.js b/src/Vault/wwwroot/app/app.js new file mode 100644 index 00000000000..b27f39d254a --- /dev/null +++ b/src/Vault/wwwroot/app/app.js @@ -0,0 +1,18 @@ +angular + .module('bit', [ + 'ui.router', + 'ngMessages', + 'angular-jwt', + 'angular-md5', + 'ui.bootstrap.showErrors', + 'toastr', + + 'bit.directives', + 'bit.services', + + 'bit.global', + 'bit.accounts', + 'bit.vault', + 'bit.settings', + 'bit.tools' + ]); diff --git a/src/Vault/wwwroot/app/config.js b/src/Vault/wwwroot/app/config.js new file mode 100644 index 00000000000..acedfd38f9c --- /dev/null +++ b/src/Vault/wwwroot/app/config.js @@ -0,0 +1,157 @@ +angular + .module('bit') + + .config(function ($stateProvider, $urlRouterProvider, $httpProvider, jwtInterceptorProvider, $uibTooltipProvider, toastrConfig) { + jwtInterceptorProvider.tokenGetter = /*@ngInject*/ function (config, appSettings, tokenService) { + if (config.url.indexOf(appSettings.apiUri) === 0) { + return tokenService.getToken(); + } + }; + + angular.extend(toastrConfig, { + closeButton: true, + progressBar: true, + showMethod: 'slideDown', + target: '.toast-target' + }); + + $uibTooltipProvider.options({ + popupDelay: 600 + }); + + if (!$httpProvider.defaults.headers.get) { + $httpProvider.defaults.headers.get = {}; + } + + $httpProvider.defaults.headers.get['If-Modified-Since'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; + $httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache'; + $httpProvider.defaults.headers.get.Pragma = 'no-cache'; + + $httpProvider.interceptors.push('apiInterceptor'); + $httpProvider.interceptors.push('jwtInterceptor'); + + $urlRouterProvider.otherwise('/'); + + $stateProvider + // Backend + .state('backend', { + templateUrl: 'app/views/backendLayout.html', + abstract: true, + data: { + authorize: true + } + }) + .state('backend.vault', { + url: '^/', + templateUrl: 'app/vault/views/vault.html', + controller: 'vaultController', + data: { pageTitle: 'My Vault' } + }) + .state('backend.settings', { + url: '^/settings', + templateUrl: 'app/settings/views/settings.html', + controller: 'settingsController', + data: { pageTitle: 'Settings' } + }) + .state('backend.tools', { + url: '^/tools', + templateUrl: 'app/tools/views/tools.html', + controller: 'toolsController', + data: { pageTitle: 'Tools' } + }) + + // Frontend + .state('frontend', { + templateUrl: 'app/views/frontendLayout.html', + abstract: true, + data: { + authorize: false + } + }) + .state('frontend.login', { + templateUrl: 'app/accounts/views/accountsLogin.html', + controller: 'accountsLoginController', + data: { + bodyClass: 'login-page' + } + }) + .state('frontend.login.info', { + url: '^/login', + templateUrl: 'app/accounts/views/accountsLoginInfo.html', + data: { + pageTitle: 'Log In' + } + }) + .state('frontend.login.twoFactor', { + url: '^/login/two-factor', + templateUrl: 'app/accounts/views/accountsLoginTwoFactor.html', + data: { + pageTitle: 'Log In (Two Factor)', + authorizeTwoFactor: true + } + }) + .state('frontend.logout', { + url: '^/logout', + controller: 'accountsLogoutController', + data: { + authorize: true + } + }) + .state('frontend.passwordHint', { + url: '^/password-hint', + templateUrl: 'app/accounts/views/accountsPasswordHint.html', + controller: 'accountsPasswordHintController', + data: { + pageTitle: 'Master Password Hint', + bodyClass: 'login-page' + } + }) + .state('frontend.register', { + url: '^/register', + templateUrl: 'app/accounts/views/accountsRegister.html', + controller: 'accountsRegisterController', + data: { + pageTitle: 'Register', + bodyClass: 'register-page' + } + }) + .state('frontend.registerFinalize', { + controller: 'accountsRegisterFinalizeController', + templateUrl: 'app/accounts/views/accountsRegisterFinalize.html', + data: { + bodyClass: 'register-page' + } + }) + .state('frontend.registerFinalize.info', { + url: '^/register/finalize', + templateUrl: 'app/accounts/views/accountsRegisterFinalizeInfo.html', + data: { + pageTitle: 'Finalize Registration' + } + }) + .state('frontend.registerFinalize.confirm', { + url: '^/register/finalize/confirm', + templateUrl: 'app/accounts/views/accountsRegisterFinalizeConfirm.html', + data: { + pageTitle: 'Finalize Registration (Confirm)' + } + }); + }) + .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())) { + event.preventDefault(); + $state.go('backend.vault'); + } + + return; + } + + if (!authService.isAuthenticated() || jwtHelper.isTokenExpired(tokenService.getToken())) { + event.preventDefault(); + authService.logOut(); + $state.go('frontend.login.info'); + } + }); + }); \ No newline at end of file diff --git a/src/Vault/wwwroot/app/directives/apiFieldDirective.js b/src/Vault/wwwroot/app/directives/apiFieldDirective.js new file mode 100644 index 00000000000..a053712a5a3 --- /dev/null +++ b/src/Vault/wwwroot/app/directives/apiFieldDirective.js @@ -0,0 +1,30 @@ +angular + .module('bit.directives') + + .directive('apiField', function () { + var linkFn = function (scope, element, attrs, ngModel) { + ngModel.$registerApiError = registerApiError; + ngModel.$validators.apiValidate = apiValidator; + + function apiValidator() { + ngModel.$setValidity('api', true); + return true; + } + + function registerApiError() { + ngModel.$setValidity('api', false); + } + }; + + return { + require: 'ngModel', + restrict: 'A', + compile: function (elem, attrs) { + if (!attrs.name || attrs.name === '') { + throw 'api-field element does not have a valid name attribute'; + } + + return linkFn; + } + }; + }); \ No newline at end of file diff --git a/src/Vault/wwwroot/app/directives/apiFormDirective.js b/src/Vault/wwwroot/app/directives/apiFormDirective.js new file mode 100644 index 00000000000..f240ab53c2c --- /dev/null +++ b/src/Vault/wwwroot/app/directives/apiFormDirective.js @@ -0,0 +1,35 @@ +angular + .module('bit.directives') + + .directive('apiForm', function ($rootScope, validationService) { + return { + require: 'form', + restrict: 'A', + link: function (scope, element, attrs, formCtrl) { + var watchPromise = attrs.apiForm || null; + if (watchPromise !== void 0) { + scope.$watch(watchPromise, formSubmitted.bind(null, formCtrl, scope)); + } + } + }; + + function formSubmitted(form, scope, promise) { + if (!promise || !promise.then) { + return; + } + + // reset errors + form.$errors = null; + + // start loading + form.$loading = true; + + promise.then(function success(response) { + form.$loading = false; + }, function failure(reason) { + form.$loading = false; + validationService.addErrors(form, reason); + scope.$broadcast('show-errors-check-validity'); + }); + } + }); \ No newline at end of file diff --git a/src/Vault/wwwroot/app/directives/directivesModule.js b/src/Vault/wwwroot/app/directives/directivesModule.js new file mode 100644 index 00000000000..ad4865f131d --- /dev/null +++ b/src/Vault/wwwroot/app/directives/directivesModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.directives', []); diff --git a/src/Vault/wwwroot/app/directives/masterPasswordDirective.js b/src/Vault/wwwroot/app/directives/masterPasswordDirective.js new file mode 100644 index 00000000000..7c5eb8fe36d --- /dev/null +++ b/src/Vault/wwwroot/app/directives/masterPasswordDirective.js @@ -0,0 +1,40 @@ +angular + .module('bit.directives') + + .directive('masterPassword', function (cryptoService, authService) { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, elem, attr, ngModel) { + var profile = authService.getUserProfile(); + if (!profile) { + return; + } + + // For DOM -> model validation + ngModel.$parsers.unshift(function (value) { + if (!value) { + return undefined; + } + + var key = cryptoService.makeKey(value, profile.email, true); + var valid = key == cryptoService.getKey(true); + ngModel.$setValidity('masterPassword', valid); + return valid ? value : undefined; + }); + + // For model -> DOM validation + ngModel.$formatters.unshift(function (value) { + if (!value) { + return undefined; + } + + var key = cryptoService.makeKey(value, profile.email, true); + var valid = key == cryptoService.getKey(true); + + ngModel.$setValidity('masterPassword', valid); + return value; + }); + } + }; + }); \ No newline at end of file diff --git a/src/Vault/wwwroot/app/directives/pageTitleDirective.js b/src/Vault/wwwroot/app/directives/pageTitleDirective.js new file mode 100644 index 00000000000..8385b3f8987 --- /dev/null +++ b/src/Vault/wwwroot/app/directives/pageTitleDirective.js @@ -0,0 +1,22 @@ +angular + .module('bit.directives') + + .directive('pageTitle', function ($rootScope, $timeout, appSettings) { + return { + link: function (scope, element) { + var listener = function (event, toState, toParams, fromState, fromParams) { + // Default title + var title = 'bitwarden Password Manager'; + if (toState.data && toState.data.pageTitle) { + title = toState.data.pageTitle + ' - bitwarden Password Manager'; + } + + $timeout(function () { + element.text(title); + }); + }; + + $rootScope.$on('$stateChangeStart', listener); + } + }; + }); \ No newline at end of file diff --git a/src/Vault/wwwroot/app/directives/passwordMeterDirective.js b/src/Vault/wwwroot/app/directives/passwordMeterDirective.js new file mode 100644 index 00000000000..4c9b1fcbade --- /dev/null +++ b/src/Vault/wwwroot/app/directives/passwordMeterDirective.js @@ -0,0 +1,73 @@ +angular + .module('bit.directives') + + .directive('passwordMeter', function () { + return { + template: '
{{value}}%
', + restrict: 'A', + scope: { + password: '=passwordMeter', + username: '=passwordMeterUsername', + value: '=passwordMeterStrength', + outerClass: '@?' + }, + link: function (scope) { + var measureStrength = function (username, password) { + if (!password || password == username) { + return 0; + } + + var strength = password.length; + + if (username && username !== '') { + if (username.indexOf(password) != -1) strength -= 15; + if (password.indexOf(username) != -1) strength -= username.length; + } + + if (password.length > 0 && password.length <= 4) strength += password.length; + else if (password.length >= 5 && password.length <= 7) strength += 6; + else if (password.length >= 8 && password.length <= 15) strength += 12; + else if (password.length >= 16) strength += 18; + + if (password.match(/[a-z]/)) strength += 1; + if (password.match(/[A-Z]/)) strength += 5; + if (password.match(/\d/)) strength += 5; + if (password.match(/.*\d.*\d.*\d/)) strength += 5; + if (password.match(/[!,@,#,$,%,^,&,*,?,_,~]/)) strength += 5; + if (password.match(/.*[!,@,#,$,%,^,&,*,?,_,~].*[!,@,#,$,%,^,&,*,?,_,~]/)) strength += 5; + if (password.match(/(?=.*[a-z])(?=.*[A-Z])/)) strength += 2; + if (password.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)) strength += 2; + if (password.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!,@,#,$,%,^,&,*,?,_,~])/)) strength += 2; + + strength = Math.round(strength * 2); + return Math.max(0, Math.min(100, strength)); + }; + + var getClass = function (strength) { + switch (Math.round(strength / 33)) { + case 0: + case 1: + return 'danger'; + + case 2: + return 'warning'; + case 3: + return 'success'; + } + }; + + var updateMeter = function (scope) { + scope.value = measureStrength(scope.username, scope.password); + scope.valueClass = getClass(scope.value); + }; + + scope.$watch('password', function () { + updateMeter(scope); + }); + + scope.$watch('username', function () { + updateMeter(scope); + }); + }, + }; + }); diff --git a/src/Vault/wwwroot/app/directives/passwordViewerDirective.js b/src/Vault/wwwroot/app/directives/passwordViewerDirective.js new file mode 100644 index 00000000000..5772b08d9a2 --- /dev/null +++ b/src/Vault/wwwroot/app/directives/passwordViewerDirective.js @@ -0,0 +1,27 @@ +angular + .module('bit.directives') + + .directive('passwordViewer', function () { + return { + restrict: 'A', + link: function (scope, element, attr) { + var passwordViewer = attr.passwordViewer; + if (!passwordViewer) { + return; + } + + element.onclick = function (event) { }; + element.on('click', function (event) { + var passwordElement = $(passwordViewer); + if (passwordElement && passwordElement.attr('type') == 'password') { + element.removeClass('fa-eye').addClass('fa-eye-slash'); + passwordElement.attr('type', 'text'); + } + else if (passwordElement && passwordElement.attr('type') == 'text') { + element.removeClass('fa-eye-slash').addClass('fa-eye'); + passwordElement.attr('type', 'password'); + } + }); + } + }; + }); diff --git a/src/Vault/wwwroot/app/global/globalModule.js b/src/Vault/wwwroot/app/global/globalModule.js new file mode 100644 index 00000000000..9af6c4b9f8d --- /dev/null +++ b/src/Vault/wwwroot/app/global/globalModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.global', []); diff --git a/src/Vault/wwwroot/app/global/mainController.js b/src/Vault/wwwroot/app/global/mainController.js new file mode 100644 index 00000000000..a9aa96ffdb4 --- /dev/null +++ b/src/Vault/wwwroot/app/global/mainController.js @@ -0,0 +1,78 @@ +angular + .module('bit.global') + + .controller('mainController', function ($scope, $state, authService, appSettings, toastr) { + var vm = this; + vm.bodyClass = ''; + vm.userProfile = null; + vm.searchVaultText = null; + vm.version = appSettings.version; + + $scope.currentYear = new Date().getFullYear(); + + $scope.$on('$viewContentLoaded', function () { + if ($.AdminLTE) { + if ($.AdminLTE.layout) { + $.AdminLTE.layout.fix(); + $.AdminLTE.layout.fixSidebar(); + } + + if ($.AdminLTE.pushMenu) { + $.AdminLTE.pushMenu.expandOnHover(); + } + } + }); + + $scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { + vm.searchVaultText = null; + vm.userProfile = authService.getUserProfile(); + + if (toState.data.bodyClass) { + vm.bodyClass = toState.data.bodyClass; + return; + } + else { + vm.bodyClass = ''; + } + }); + + $scope.searchVault = function () { + $state.go('backend.vault'); + }; + + $scope.addSite = function () { + $scope.$broadcast('vaultAddSite'); + }; + + $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.twoFactor = function () { + $scope.$broadcast('settingsTwoFactor'); + }; + + $scope.import = function () { + $scope.$broadcast('toolsImport'); + }; + + $scope.export = function () { + $scope.$broadcast('toolsExport'); + }; + + $scope.audits = function () { + $scope.$broadcast('toolsAudits'); + }; + }); diff --git a/src/Vault/wwwroot/app/global/sideNavController.js b/src/Vault/wwwroot/app/global/sideNavController.js new file mode 100644 index 00000000000..4d41c06c9a7 --- /dev/null +++ b/src/Vault/wwwroot/app/global/sideNavController.js @@ -0,0 +1,6 @@ +angular + .module('bit.global') + + .controller('sideNavController', function ($scope, $state) { + $scope.$state = $state; + }); diff --git a/src/Vault/wwwroot/app/global/topNavController.js b/src/Vault/wwwroot/app/global/topNavController.js new file mode 100644 index 00000000000..2cdacba4e79 --- /dev/null +++ b/src/Vault/wwwroot/app/global/topNavController.js @@ -0,0 +1,6 @@ +angular + .module('bit.global') + + .controller('topNavController', function ($scope) { + + }); diff --git a/src/Vault/wwwroot/app/services/apiService.js b/src/Vault/wwwroot/app/services/apiService.js new file mode 100644 index 00000000000..0e825e1ad09 --- /dev/null +++ b/src/Vault/wwwroot/app/services/apiService.js @@ -0,0 +1,48 @@ +angular + .module('bit.services') + + .factory('apiService', function ($resource, tokenService, appSettings) { + var _service = {}, + _apiUri = appSettings.apiUri; + + _service.sites = $resource(_apiUri + '/sites/:id', {}, { + get: { method: 'GET', params: { id: '@id' } }, + list: { method: 'GET', params: {} }, + post: { method: 'POST', params: {} }, + put: { method: 'PUT', params: { id: '@id' } }, + del: { method: 'DELETE', params: { id: '@id' } } + }); + + _service.folders = $resource(_apiUri + '/folders/:id', {}, { + get: { method: 'GET', params: { id: '@id' } }, + list: { method: 'GET', params: {} }, + post: { method: 'POST', params: {} }, + put: { method: 'PUT', params: { id: '@id' } }, + del: { method: 'DELETE', params: { id: '@id' } } + }); + + _service.ciphers = $resource(_apiUri + '/ciphers/:id', {}, { + putMany: { url: _apiUri + '/ciphers/many', method: 'PUT', params: {} } + }); + + _service.accounts = $resource(_apiUri + '/accounts', {}, { + registerToken: { url: _apiUri + '/accounts/register-token', method: 'POST', params: {} }, + register: { url: _apiUri + '/accounts/register', method: 'POST', params: {} }, + emailToken: { url: _apiUri + '/accounts/email-token', method: 'POST', params: {} }, + email: { url: _apiUri + '/accounts/email', method: 'PUT', params: {} }, + putPassword: { url: _apiUri + '/accounts/password', method: 'PUT', params: {} }, + getProfile: { url: _apiUri + '/accounts/profile', method: 'GET', params: {} }, + putProfile: { url: _apiUri + '/accounts/profile', method: 'PUT', params: {} }, + getTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'GET', params: {} }, + putTwoFactor: { url: _apiUri + '/accounts/two-factor', method: 'PUT', params: {} }, + postPasswordHint: { url: _apiUri + '/accounts/password-hint', method: 'POST', params: {} }, + putSecurityStamp: { url: _apiUri + '/accounts/security-stamp', method: 'PUT', params: {} } + }); + + _service.auth = $resource(_apiUri + '/auth', {}, { + token: { url: _apiUri + '/auth/token', method: 'POST', params: {} }, + tokenTwoFactor: { url: _apiUri + '/auth/token/two-factor', method: 'POST', params: {} } + }); + + return _service; + }); diff --git a/src/Vault/wwwroot/app/services/authService.js b/src/Vault/wwwroot/app/services/authService.js new file mode 100644 index 00000000000..d34391f381e --- /dev/null +++ b/src/Vault/wwwroot/app/services/authService.js @@ -0,0 +1,111 @@ +angular + .module('bit.services') + + .factory('authService', function (cryptoService, apiService, tokenService, $q, jwtHelper) { + var _service = {}, + _userProfile = null; + + _service.logIn = function (email, masterPassword) { + 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; + }); diff --git a/src/Vault/wwwroot/app/services/cipherService.js b/src/Vault/wwwroot/app/services/cipherService.js new file mode 100644 index 00000000000..ffb98e1fd66 --- /dev/null +++ b/src/Vault/wwwroot/app/services/cipherService.js @@ -0,0 +1,110 @@ +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, + name: cryptoService.decrypt(encryptedSite.Name), + uri: cryptoService.decrypt(encryptedSite.Uri), + username: cryptoService.decrypt(encryptedSite.Username), + password: cryptoService.decrypt(encryptedSite.Password), + 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, + uri: cryptoService.encrypt(unencryptedSite.uri, key), + name: cryptoService.encrypt(unencryptedSite.name, key), + username: cryptoService.encrypt(unencryptedSite.username, key), + password: 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; + }); diff --git a/src/Vault/wwwroot/app/services/cryptoService.js b/src/Vault/wwwroot/app/services/cryptoService.js new file mode 100644 index 00000000000..c658ff8bb2f --- /dev/null +++ b/src/Vault/wwwroot/app/services/cryptoService.js @@ -0,0 +1,114 @@ +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, 0) + }; + + 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; + }); diff --git a/src/Vault/wwwroot/app/services/passwordService.js b/src/Vault/wwwroot/app/services/passwordService.js new file mode 100644 index 00000000000..4a67c3a1e97 --- /dev/null +++ b/src/Vault/wwwroot/app/services/passwordService.js @@ -0,0 +1,105 @@ +angular + .module('bit.services') + + .factory('passwordService', function () { + var _service = {}; + + _service.generatePassword = function (options) { + var defaults = { + length: 10, + ambiguous: false, + number: true, + minNumber: 1, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + special: false, + minSpecial: 1 + }; + + // overload defaults with given options + var o = angular.extend({}, defaults, options); + + // sanitize + if (o.uppercase && o.minUppercase < 0) o.minUppercase = 1; + if (o.lowercase && o.minLowercase < 0) o.minLowercase = 1; + if (o.number && o.minNumber < 0) o.minNumber = 1; + if (o.special && o.minSpecial < 0) o.minSpecial = 1; + + if (!o.length || o.length < 1) o.length = 10; + var minLength = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; + if (o.length < minLength) o.length = minLength; + + var positions = []; + if (o.lowercase && o.minLowercase > 0) { + for (var i = 0; i < o.minLowercase; i++) { + positions.push('l'); + } + } + if (o.uppercase && o.minUppercase > 0) { + for (var j = 0; j < o.minUppercase; j++) { + positions.push('u'); + } + } + if (o.number && o.minNumber > 0) { + for (var k = 0; k < o.minNumber; k++) { + positions.push('n'); + } + } + if (o.special && o.minSpecial > 0) { + for (var l = 0; l < o.minSpecial; l++) { + positions.push('s'); + } + } + while (positions.length < o.length) { + positions.push('a'); + } + + // shuffle + positions.sort(function () { + return randomInt(0, 1) * 2 - 1; + }); + + // build out the char sets + var allCharSet = ''; + + var lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; + if (o.ambiguous) lowercaseCharSet += 'l'; + if (o.lowercase) allCharSet += lowercaseCharSet; + + var uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; + if (o.ambiguous) uppercaseCharSet += 'O'; + if (o.uppercase) allCharSet += uppercaseCharSet; + + var numberCharSet = '23456789'; + if (o.ambiguous) numberCharSet += '01'; + if (o.number) allCharSet += numberCharSet; + + var specialCharSet = '!@#$%^&*'; + if (o.special) allCharSet += specialCharSet; + + var password = ''; + for (var m = 0; m < o.length; m++) { + var positionChars; + switch (positions[m]) { + case 'l': positionChars = lowercaseCharSet; break; + case 'u': positionChars = uppercaseCharSet; break; + case 'n': positionChars = numberCharSet; break; + case 's': positionChars = specialCharSet; break; + case 'a': positionChars = allCharSet; break; + } + + var randomCharIndex = randomInt(0, positionChars.length - 1); + password += positionChars.charAt(randomCharIndex); + } + + return password; + }; + + function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + return _service; + }); diff --git a/src/Vault/wwwroot/app/services/servicesModule.js b/src/Vault/wwwroot/app/services/servicesModule.js new file mode 100644 index 00000000000..0fbfca98db6 --- /dev/null +++ b/src/Vault/wwwroot/app/services/servicesModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.services', ['ngResource', 'ngStorage', 'angular-jwt']); diff --git a/src/Vault/wwwroot/app/services/tokenService.js b/src/Vault/wwwroot/app/services/tokenService.js new file mode 100644 index 00000000000..37cd6b43590 --- /dev/null +++ b/src/Vault/wwwroot/app/services/tokenService.js @@ -0,0 +1,27 @@ +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; + }); diff --git a/src/Vault/wwwroot/app/services/validationService.js b/src/Vault/wwwroot/app/services/validationService.js new file mode 100644 index 00000000000..c8f432081c3 --- /dev/null +++ b/src/Vault/wwwroot/app/services/validationService.js @@ -0,0 +1,52 @@ +angular + .module('bit.services') + + .factory('validationService', function () { + var _service = {}; + + _service.addErrors = function (form, reason) { + var data = reason.data; + var defaultErrorMessage = 'An unexpected error has occured.'; + form.$errors = []; + + if (!data || !angular.isObject(data)) { + form.$errors.push(defaultErrorMessage); + return; + } + + if (!data.ValidationErrors) { + if (data.Message) { + form.$errors.push(data.Message); + } + else { + form.$errors.push(defaultErrorMessage); + } + + return; + } + + for (var key in data.ValidationErrors) { + if (!data.ValidationErrors.hasOwnProperty(key)) { + continue; + } + + for (var i = 0; i < data.ValidationErrors[key].length; i++) { + _service.addError(form, key, data.ValidationErrors[key][i]); + } + } + }; + + _service.addError = function (form, key, errorMessage, clearExistingErrors) { + if (clearExistingErrors || !form.$errors) { + form.$errors = []; + } + + form.$errors.push(errorMessage); + if (key && key !== '' && form[key] && form[key].$registerApiError) { + form[key].$registerApiError(); + } + }; + + + return _service; + }); diff --git a/src/Vault/wwwroot/app/settings.js b/src/Vault/wwwroot/app/settings.js new file mode 100644 index 00000000000..3e15adc0468 --- /dev/null +++ b/src/Vault/wwwroot/app/settings.js @@ -0,0 +1,2 @@ +angular.module('bit') +.constant('appSettings', {"rememberdEmailCookieName":"bit.rememberedEmail","version":"0.0.1","environment":"Development","apiUri":"http://localhost:4000"}); diff --git a/src/Vault/wwwroot/app/settings/settingsChangeEmailController.js b/src/Vault/wwwroot/app/settings/settingsChangeEmailController.js new file mode 100644 index 00000000000..f2ef340b84a --- /dev/null +++ b/src/Vault/wwwroot/app/settings/settingsChangeEmailController.js @@ -0,0 +1,66 @@ +angular + .module('bit.settings') + + .controller('settingsChangeEmailController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, cipherService, authService, $q, toastr) { + var _masterPasswordHash, + _newMasterPasswordHash, + _newKey; + + $scope.token = function (model) { + _masterPasswordHash = cryptoService.hashPassword(model.masterPassword); + + var request = { + newEmail: model.newEmail, + masterPasswordHash: _masterPasswordHash + }; + + $scope.tokenPromise = apiService.accounts.emailToken(request, function () { + _newKey = cryptoService.makeKey(model.masterPassword, model.newEmail); + _newMasterPasswordHash = cryptoService.hashPassword(model.masterPassword, _newKey); + + $scope.tokenSent = true; + }).$promise; + }; + + $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); + }).$promise; + + var reencryptedFolders = []; + var foldersPromise = apiService.folders.list({ dirty: false }, function (encryptedFolders) { + var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data); + reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, _newKey); + }).$promise; + + $q.all([sitesPromise, foldersPromise]).then(function () { + var request = { + token: model.token, + newEmail: model.newEmail, + masterPasswordHash: _masterPasswordHash, + newMasterPasswordHash: _newMasterPasswordHash, + ciphers: reencryptedSites.concat(reencryptedFolders) + }; + + $scope.confirmPromise = apiService.accounts.email(request, function () { + $uibModalInstance.dismiss('cancel'); + authService.logOut(); + $state.go('frontend.login.info').then(function () { + toastr.success('Please log back in.', 'Email Changed') + }); + }, function () { + // TODO: recovery mode + $uibModalInstance.dismiss('cancel'); + toastr.error('Something went wrong.', 'Oh No!'); + }).$promise; + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/settings/settingsChangePasswordController.js b/src/Vault/wwwroot/app/settings/settingsChangePasswordController.js new file mode 100644 index 00000000000..844f540a350 --- /dev/null +++ b/src/Vault/wwwroot/app/settings/settingsChangePasswordController.js @@ -0,0 +1,53 @@ +angular + .module('bit.settings') + + .controller('settingsChangePasswordController', function ($scope, $state, apiService, $uibModalInstance, + cryptoService, authService, cipherService, validationService, $q, toastr) { + $scope.save = function (model, form) { + if ($scope.model.newMasterPassword != $scope.model.confirmNewMasterPassword) { + validationService.addError(form, 'ConfirmNewMasterPassword', 'New master password confirmation does not match.', true); + return; + } + + $scope.processing = true; + + var profile = authService.getUserProfile(); + var newKey = cryptoService.makeKey(model.newMasterPassword, profile.email); + + var reencryptedSites = []; + var sitesPromise = apiService.sites.list({ dirty: false }, function (encryptedSites) { + var unencryptedSites = cipherService.decryptSites(encryptedSites.Data); + reencryptedSites = cipherService.encryptSites(unencryptedSites, newKey); + }).$promise; + + var reencryptedFolders = []; + var foldersPromise = apiService.folders.list({ dirty: false }, function (encryptedFolders) { + var unencryptedFolders = cipherService.decryptFolders(encryptedFolders.Data); + reencryptedFolders = cipherService.encryptFolders(unencryptedFolders, newKey); + }).$promise; + + $q.all([sitesPromise, foldersPromise]).then(function () { + var request = { + masterPasswordHash: cryptoService.hashPassword(model.masterPassword), + newMasterPasswordHash: cryptoService.hashPassword(model.newMasterPassword, newKey), + ciphers: reencryptedSites.concat(reencryptedFolders) + }; + + $scope.savePromise = apiService.accounts.putPassword(request, function () { + $uibModalInstance.dismiss('cancel'); + authService.logOut(); + $state.go('frontend.login.info').then(function () { + toastr.success('Please log back in.', 'Master Password Changed') + }); + }, function () { + // TODO: recovery mode + $uibModalInstance.dismiss('cancel'); + toastr.error('Something went wrong.', 'Oh No!'); + }).$promise; + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/settings/settingsController.js b/src/Vault/wwwroot/app/settings/settingsController.js new file mode 100644 index 00000000000..4025ca18e74 --- /dev/null +++ b/src/Vault/wwwroot/app/settings/settingsController.js @@ -0,0 +1,72 @@ +angular + .module('bit.settings') + + .controller('settingsController', function ($scope, $uibModal, apiService, toastr, authService) { + $scope.model = {}; + + apiService.accounts.getProfile({}, function (user) { + $scope.model = { + name: user.Name, + email: user.Email, + masterPasswordHint: user.MasterPasswordHint, + culture: user.Culture, + twoFactorEnabled: user.TwoFactorEnabled + }; + }); + + $scope.save = function (model) { + $scope.savePromise = apiService.accounts.putProfile({}, model, function (profile) { + authService.setUserProfile(profile); + toastr.success('Account has been updated.', 'Success!'); + }).$promise; + }; + + $scope.changePassword = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/settings/views/settingsChangePassword.html', + controller: 'settingsChangePasswordController' + }); + }; + + $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' + }); + }; + + $scope.$on('settingsChangeEmail', function (event, args) { + $scope.changeEmail(); + }); + + $scope.twoFactor = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/settings/views/settingsTwoFactor.html', + controller: 'settingsTwoFactorController' + }); + }; + + $scope.$on('settingsTwoFactor', function (event, args) { + $scope.twoFactor(); + }); + + $scope.sessions = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/settings/views/settingsSessions.html', + controller: 'settingsSessionsController' + }); + }; + + $scope.$on('settingsSessions', function (event, args) { + $scope.sessions(); + }); + }); diff --git a/src/Vault/wwwroot/app/settings/settingsModule.js b/src/Vault/wwwroot/app/settings/settingsModule.js new file mode 100644 index 00000000000..4c5aa7f7c2b --- /dev/null +++ b/src/Vault/wwwroot/app/settings/settingsModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.settings', ['ui.bootstrap', 'toastr']); diff --git a/src/Vault/wwwroot/app/settings/settingsSessionsController.js b/src/Vault/wwwroot/app/settings/settingsSessionsController.js new file mode 100644 index 00000000000..87fb77e3fa2 --- /dev/null +++ b/src/Vault/wwwroot/app/settings/settingsSessionsController.js @@ -0,0 +1,22 @@ +angular + .module('bit.settings') + + .controller('settingsSessionsController', function ($scope, $state, apiService, $uibModalInstance, cryptoService, authService, toastr) { + $scope.submit = function (model) { + var request = { + masterPasswordHash: cryptoService.hashPassword(model.masterPassword) + }; + + $scope.submitPromise = apiService.accounts.putSecurityStamp(request, function () { + $uibModalInstance.dismiss('cancel'); + authService.logOut(); + $state.go('frontend.login.info').then(function () { + toastr.success('Please log back in.', 'All Sessions Deauthorized') + }); + }).$promise; + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/settings/settingsTwoFactorController.js b/src/Vault/wwwroot/app/settings/settingsTwoFactorController.js new file mode 100644 index 00000000000..b621624aa18 --- /dev/null +++ b/src/Vault/wwwroot/app/settings/settingsTwoFactorController.js @@ -0,0 +1,62 @@ +angular + .module('bit.settings') + + .controller('settingsTwoFactorController', function ($scope, apiService, $uibModalInstance, cryptoService, authService, $q, toastr) { + var _issuer = 'bitwarden', + _profile = authService.getUserProfile(), + _masterPasswordHash; + + $scope.account = _profile.email; + $scope.enabled = function () { + return _profile.extended && _profile.extended.twoFactorEnabled; + } + + $scope.auth = function (model) { + _masterPasswordHash = cryptoService.hashPassword(model.masterPassword); + + $scope.authPromise = apiService.accounts.getTwoFactor({ + 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 + }; + }).$promise; + }; + + $scope.update = function (model) { + var currentlyEnabled = $scope.twoFactorModel.enabled; + if (currentlyEnabled && !confirm('Are you sure you want to disable two-step login?')) { + return; + } + + var request = { + enabled: !currentlyEnabled, + token: model ? model.token : null, + masterPasswordHash: _masterPasswordHash, + }; + + $scope.updatePromise = apiService.accounts.putTwoFactor({}, request, function (response) { + if (response.TwoFactorEnabled) { + toastr.success('Two-step login has been enabled.'); + if (_profile.extended) _profile.extended.twoFactorEnabled = true; + } + else { + toastr.success('Two-step login has been disabled.'); + if (_profile.extended) _profile.extended.twoFactorEnabled = false; + } + + $scope.close(); + }).$promise; + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/settings/views/settings.html b/src/Vault/wwwroot/app/settings/views/settings.html new file mode 100644 index 00000000000..1e3dae90d9e --- /dev/null +++ b/src/Vault/wwwroot/app/settings/views/settings.html @@ -0,0 +1,46 @@ +
+

+ Settings + manage your account +

+
+
+
+
+

General

+
+
+
+
+

Errors have occured

+
    +
  • {{e}}
  • +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/settings/views/settingsChangeEmail.html b/src/Vault/wwwroot/app/settings/views/settingsChangeEmail.html new file mode 100644 index 00000000000..a972939c3eb --- /dev/null +++ b/src/Vault/wwwroot/app/settings/views/settingsChangeEmail.html @@ -0,0 +1,54 @@ + +
+ + +
+
+ + +
+ diff --git a/src/Vault/wwwroot/app/settings/views/settingsChangePassword.html b/src/Vault/wwwroot/app/settings/views/settingsChangePassword.html new file mode 100644 index 00000000000..c88bb8881d9 --- /dev/null +++ b/src/Vault/wwwroot/app/settings/views/settingsChangePassword.html @@ -0,0 +1,46 @@ + +
+ + +
+ diff --git a/src/Vault/wwwroot/app/settings/views/settingsSessions.html b/src/Vault/wwwroot/app/settings/views/settingsSessions.html new file mode 100644 index 00000000000..4d46373bcfc --- /dev/null +++ b/src/Vault/wwwroot/app/settings/views/settingsSessions.html @@ -0,0 +1,32 @@ + +
+ + +
diff --git a/src/Vault/wwwroot/app/settings/views/settingsTwoFactor.html b/src/Vault/wwwroot/app/settings/views/settingsTwoFactor.html new file mode 100644 index 00000000000..74a5f2202c6 --- /dev/null +++ b/src/Vault/wwwroot/app/settings/views/settingsTwoFactor.html @@ -0,0 +1,82 @@ + +
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/tools/toolsAuditsController.js b/src/Vault/wwwroot/app/tools/toolsAuditsController.js new file mode 100644 index 00000000000..0cbafe91ae3 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/toolsAuditsController.js @@ -0,0 +1,8 @@ +angular + .module('bit.tools') + + .controller('toolsAuditsController', function ($scope, apiService, $uibModalInstance, toastr) { + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/tools/toolsController.js b/src/Vault/wwwroot/app/tools/toolsController.js new file mode 100644 index 00000000000..833a1e44a00 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/toolsController.js @@ -0,0 +1,42 @@ +angular + .module('bit.tools') + + .controller('toolsController', function ($scope, $uibModal, apiService, toastr, authService) { + $scope.import = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/tools/views/toolsImport.html', + controller: 'toolsImportController', + size: 'sm' + }); + }; + + $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' + }); + }; + + $scope.$on('toolsExport', function (event, args) { + $scope.export(); + }); + + $scope.audits = function () { + $uibModal.open({ + animation: true, + templateUrl: 'app/tools/views/toolsAudits.html', + controller: 'toolsAuditsController' + }); + }; + + $scope.$on('toolsAudits', function (event, args) { + $scope.audits(); + }); + }); diff --git a/src/Vault/wwwroot/app/tools/toolsExportController.js b/src/Vault/wwwroot/app/tools/toolsExportController.js new file mode 100644 index 00000000000..e285b00e635 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/toolsExportController.js @@ -0,0 +1,71 @@ +angular + .module('bit.tools') + + .controller('toolsExportController', function ($scope, apiService, authService, $uibModalInstance, cryptoService, cipherService, $q, toastr) { + $scope.export = function (model) { + $scope.startedExport = true; + apiService.sites.list({ expand: ['folder'] }, function (sites) { + try { + var decSites = cipherService.decryptSites(sites.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 + }; + + exportSites.push(site); + } + + var csvString = Papa.unparse(exportSites); + 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()); + } + else { + var a = window.document.createElement('a'); + a.href = window.URL.createObjectURL(csvBlob, { type: 'text/plain' }); + a.download = makeFileName(); + document.body.appendChild(a); + a.click(); // IE: "Access is denied". ref: https://connect.microsoft.com/IE/feedback/details/797361/ie-10-treats-blob-url-as-cross-origin-and-denies-access + document.body.removeChild(a); + } + + toastr.success('Your data has been exported. Check your browser\'s downloads folder.', 'Success!'); + $scope.close(); + } + catch (err) { + toastr.error('Something went wrong. Please try again.', 'Error!'); + $scope.close(); + } + }, function () { + toastr.error('Something went wrong. Please try again.', 'Error!'); + $scope.close(); + }); + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + + function makeFileName() { + var now = new Date(); + var dateString = + now.getFullYear() + '' + padNumber((now.getMonth() + 1), 2) + '' + padNumber(now.getDate(), 2) + + padNumber(now.getHours(), 2) + '' + padNumber(now.getMinutes(), 2) + + padNumber(now.getSeconds(), 2); + + return 'bitwarden_export_' + dateString + '.csv'; + } + + function padNumber(number, width, paddingCharacter) { + paddingCharacter = paddingCharacter || '0'; + number = number + ''; + return number.length >= width ? number : new Array(width - number.length + 1).join(paddingCharacter) + number; + } + }); diff --git a/src/Vault/wwwroot/app/tools/toolsImportController.js b/src/Vault/wwwroot/app/tools/toolsImportController.js new file mode 100644 index 00000000000..eb2b7433efd --- /dev/null +++ b/src/Vault/wwwroot/app/tools/toolsImportController.js @@ -0,0 +1,8 @@ +angular + .module('bit.tools') + + .controller('toolsImportController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, $q, toastr) { + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/tools/toolsModule.js b/src/Vault/wwwroot/app/tools/toolsModule.js new file mode 100644 index 00000000000..98191f49988 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/toolsModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.tools', ['ui.bootstrap', 'toastr']); diff --git a/src/Vault/wwwroot/app/tools/views/tools.html b/src/Vault/wwwroot/app/tools/views/tools.html new file mode 100644 index 00000000000..f17906b6271 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/views/tools.html @@ -0,0 +1,9 @@ +
+

+ Tools + helpful utilities +

+
+
+ Content +
diff --git a/src/Vault/wwwroot/app/tools/views/toolsAudits.html b/src/Vault/wwwroot/app/tools/views/toolsAudits.html new file mode 100644 index 00000000000..adf9a56c7b2 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/views/toolsAudits.html @@ -0,0 +1,10 @@ + + + diff --git a/src/Vault/wwwroot/app/tools/views/toolsExport.html b/src/Vault/wwwroot/app/tools/views/toolsExport.html new file mode 100644 index 00000000000..cee32b4b897 --- /dev/null +++ b/src/Vault/wwwroot/app/tools/views/toolsExport.html @@ -0,0 +1,28 @@ + +
+ + +
+ diff --git a/src/Vault/wwwroot/app/tools/views/toolsImport.html b/src/Vault/wwwroot/app/tools/views/toolsImport.html new file mode 100644 index 00000000000..44f283c098e --- /dev/null +++ b/src/Vault/wwwroot/app/tools/views/toolsImport.html @@ -0,0 +1,21 @@ + +
+ + +
diff --git a/src/Vault/wwwroot/app/vault/vaultAddFolderController.js b/src/Vault/wwwroot/app/vault/vaultAddFolderController.js new file mode 100644 index 00000000000..92fc9d3f84e --- /dev/null +++ b/src/Vault/wwwroot/app/vault/vaultAddFolderController.js @@ -0,0 +1,17 @@ +angular + .module('bit.vault') + + .controller('vaultAddFolderController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService) { + $scope.savePromise = null; + $scope.save = function (model) { + var folder = cipherService.encryptFolder(model); + $scope.savePromise = apiService.folders.post(folder, function (response) { + var decFolder = cipherService.decryptFolder(response); + $uibModalInstance.close(decFolder); + }).$promise; + }; + + $scope.close = function () { + $uibModalInstance.dismiss('close'); + }; + }); diff --git a/src/Vault/wwwroot/app/vault/vaultAddSiteController.js b/src/Vault/wwwroot/app/vault/vaultAddSiteController.js new file mode 100644 index 00000000000..93ea30f6ced --- /dev/null +++ b/src/Vault/wwwroot/app/vault/vaultAddSiteController.js @@ -0,0 +1,47 @@ +angular + .module('bit.vault') + + .controller('vaultAddSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, folders, selectedFolder) { + $scope.folders = folders; + $scope.site = { + 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) { + 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?')) { + $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('close'); + }; + }); diff --git a/src/Vault/wwwroot/app/vault/vaultController.js b/src/Vault/wwwroot/app/vault/vaultController.js new file mode 100644 index 00000000000..10f58e24e36 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/vaultController.js @@ -0,0 +1,155 @@ +angular + .module('bit.vault') + + .controller('vaultController', function ($scope, $uibModal, apiService, $filter, cryptoService, authService, toastr, cipherService) { + $scope.sites = []; + $scope.folders = []; + + apiService.sites.list({}, function (sites) { + var decSites = []; + for (var i = 0; i < sites.Data.length; i++) { + var decSite = { + id: sites.Data[i].Id, + folderId: sites.Data[i].FolderId + }; + + try { decSite.name = cryptoService.decrypt(sites.Data[i].Name); } + catch (err) { decSite.name = "[error: cannot decrypt]"; } + + try { decSite.username = cryptoService.decrypt(sites.Data[i].Username); } + catch (err) { decSite.username = "[error: cannot decrypt]"; } + + decSites.push(decSite); + } + + $scope.sites = decSites; + }); + + apiService.folders.list({}, function (folders) { + var decFolders = [{ + id: null, + name: '(none)' + }]; + + for (var i = 0; i < folders.Data.length; i++) { + var decFolder = { + id: folders.Data[i].Id + }; + + try { decFolder.name = cryptoService.decrypt(folders.Data[i].Name); } + catch (err) { decFolder.name = "[error: cannot decrypt]"; } + + decFolders.push(decFolder); + } + + $scope.folders = decFolders; + }); + + $scope.editSite = function (site) { + var editModel = $uibModal.open({ + animation: true, + templateUrl: 'app/vault/views/vaultEditSite.html', + controller: 'vaultEditSiteController', + resolve: { + siteId: function () { return site.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; + } + }); + }; + + $scope.$on('vaultAddSite', function (event, args) { + $scope.addSite(); + }); + + $scope.addSite = function (folder) { + var addModel = $uibModal.open({ + animation: true, + templateUrl: 'app/vault/views/vaultAddSite.html', + controller: 'vaultAddSiteController', + resolve: { + folders: function () { return $scope.folders; }, + selectedFolder: function () { return folder; } + } + }); + + addModel.result.then(function (addedSite) { + $scope.sites.push(addedSite); + }); + }; + + $scope.deleteSite = function (site) { + if (!confirm("Are you sure you want to delete this site (" + site.name + ")?")) { + return; + } + + apiService.sites.del({ id: site.id }, function () { + var index = $scope.sites.indexOf(site); + $scope.sites.splice(index, 1); + }); + }; + + $scope.editFolder = function (folder) { + var editModel = $uibModal.open({ + animation: true, + templateUrl: 'app/vault/views/vaultEditFolder.html', + controller: 'vaultEditFolderController', + size: 'sm', + resolve: { + folderId: function () { return folder.id; } + } + }); + + editModel.result.then(function (editedFolder) { + var folder = $filter('filter')($scope.folders, { id: editedFolder.id }, true); + if (folder && folder.length > 0) { + folder[0].name = editedFolder.name; + } + }); + }; + + $scope.$on('vaultAddFolder', function (event, args) { + $scope.addFolder(); + }); + + $scope.addFolder = function () { + var addModel = $uibModal.open({ + animation: true, + templateUrl: 'app/vault/views/vaultAddFolder.html', + controller: 'vaultAddFolderController', + size: 'sm' + }); + + addModel.result.then(function (addedFolder) { + $scope.folders.push(addedFolder); + }); + }; + + $scope.deleteFolder = function (folder) { + if (!confirm("Are you sure you want to delete this folder (" + folder.name + ")?")) { + return; + } + + apiService.folders.del({ id: folder.id }, function () { + var index = $scope.folders.indexOf(folder); + $scope.folders.splice(index, 1); + }); + }; + + $scope.canDeleteFolder = function (folder) { + if (!folder || !folder.id) { + return false; + } + + var sites = $filter('filter')($scope.sites, { folderId: folder.id }); + return sites.length === 0; + }; + }); diff --git a/src/Vault/wwwroot/app/vault/vaultEditFolderController.js b/src/Vault/wwwroot/app/vault/vaultEditFolderController.js new file mode 100644 index 00000000000..e23bfb4a267 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/vaultEditFolderController.js @@ -0,0 +1,23 @@ +angular + .module('bit.vault') + + .controller('vaultEditFolderController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, folderId) { + $scope.folder = {}; + + apiService.folders.get({ id: folderId }, function (folder) { + $scope.folder = cipherService.decryptFolder(folder); + }); + + $scope.savePromise = null; + $scope.save = function (model) { + var folder = cipherService.encryptFolder(model); + $scope.savePromise = apiService.folders.put({ id: folderId }, folder, function (response) { + var decFolder = cipherService.decryptFolder(response); + $uibModalInstance.close(decFolder); + }).$promise; + }; + + $scope.close = function () { + $uibModalInstance.dismiss('cancel'); + }; + }); diff --git a/src/Vault/wwwroot/app/vault/vaultEditSiteController.js b/src/Vault/wwwroot/app/vault/vaultEditSiteController.js new file mode 100644 index 00000000000..0fc210d1f1f --- /dev/null +++ b/src/Vault/wwwroot/app/vault/vaultEditSiteController.js @@ -0,0 +1,48 @@ +angular + .module('bit.vault') + + .controller('vaultEditSiteController', function ($scope, apiService, $uibModalInstance, cryptoService, cipherService, passwordService, siteId, folders) { + $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) { + 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?')) { + $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'); + }; + }); diff --git a/src/Vault/wwwroot/app/vault/vaultModule.js b/src/Vault/wwwroot/app/vault/vaultModule.js new file mode 100644 index 00000000000..84125898a91 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/vaultModule.js @@ -0,0 +1,2 @@ +angular + .module('bit.vault', ['ui.bootstrap', 'ngclipboard']); diff --git a/src/Vault/wwwroot/app/vault/views/vault.html b/src/Vault/wwwroot/app/vault/views/vault.html new file mode 100644 index 00000000000..3a969cb9a62 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/views/vault.html @@ -0,0 +1,51 @@ +
+

+ My Vault + safe and secure +

+
+
+
+
+

{{folder.name}}

+
+ + + +
+
+
+
+

No sites in this folder.

+ +
+
+ + + + + + + + + + + + + + + +
SiteUsername
{{site.name}}{{site.username}} + + +
+
+
+
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/vault/views/vaultAddFolder.html b/src/Vault/wwwroot/app/vault/views/vaultAddFolder.html new file mode 100644 index 00000000000..703a6207944 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/views/vaultAddFolder.html @@ -0,0 +1,24 @@ + +
+ + +
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/vault/views/vaultAddSite.html b/src/Vault/wwwroot/app/vault/views/vaultAddSite.html new file mode 100644 index 00000000000..21f0f62d503 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/views/vaultAddSite.html @@ -0,0 +1,92 @@ + +
+ + +
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/vault/views/vaultEditFolder.html b/src/Vault/wwwroot/app/vault/views/vaultEditFolder.html new file mode 100644 index 00000000000..2975108030c --- /dev/null +++ b/src/Vault/wwwroot/app/vault/views/vaultEditFolder.html @@ -0,0 +1,24 @@ + +
+ + +
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/vault/views/vaultEditSite.html b/src/Vault/wwwroot/app/vault/views/vaultEditSite.html new file mode 100644 index 00000000000..c390f671619 --- /dev/null +++ b/src/Vault/wwwroot/app/vault/views/vaultEditSite.html @@ -0,0 +1,95 @@ + +
+ + +
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/views/backendLayout.html b/src/Vault/wwwroot/app/views/backendLayout.html new file mode 100644 index 00000000000..615f9a01572 --- /dev/null +++ b/src/Vault/wwwroot/app/views/backendLayout.html @@ -0,0 +1,78 @@ +
+
+ + +
+ + + +
+
+ +
+ + Copyright © , bitwarden.com +
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/app/views/frontendLayout.html b/src/Vault/wwwroot/app/views/frontendLayout.html new file mode 100644 index 00000000000..47e1b0a201d --- /dev/null +++ b/src/Vault/wwwroot/app/views/frontendLayout.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/src/Vault/wwwroot/favicon.ico b/src/Vault/wwwroot/favicon.ico new file mode 100644 index 00000000000..f2aadce4146 Binary files /dev/null and b/src/Vault/wwwroot/favicon.ico differ diff --git a/src/Vault/wwwroot/images/boxed-bg-2x.png b/src/Vault/wwwroot/images/boxed-bg-2x.png new file mode 100644 index 00000000000..2050e8aa04a Binary files /dev/null and b/src/Vault/wwwroot/images/boxed-bg-2x.png differ diff --git a/src/Vault/wwwroot/images/boxed-bg.png b/src/Vault/wwwroot/images/boxed-bg.png new file mode 100644 index 00000000000..da898fc3cbb Binary files /dev/null and b/src/Vault/wwwroot/images/boxed-bg.png differ diff --git a/src/Vault/wwwroot/index.html b/src/Vault/wwwroot/index.html new file mode 100644 index 00000000000..81215e05549 --- /dev/null +++ b/src/Vault/wwwroot/index.html @@ -0,0 +1,124 @@ + + + + + + + + bitwarden.com Password Manager + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Vault/wwwroot/web.config b/src/Vault/wwwroot/web.config new file mode 100644 index 00000000000..e780518bcdc --- /dev/null +++ b/src/Vault/wwwroot/web.config @@ -0,0 +1,9 @@ + + + + + + + + +