├── .env ├── .gitignore ├── .rspec ├── .travis.yml ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── emoji │ │ │ ├── warning.png │ │ │ └── white_check_mark.png │ │ └── mockup │ │ │ ├── grid.png │ │ │ ├── home.png │ │ │ ├── person.png │ │ │ ├── retina_wood.png │ │ │ └── web.png │ ├── javascripts │ │ ├── application.coffee │ │ ├── collections │ │ │ └── item.coffee │ │ ├── ext │ │ │ ├── backbone.coffee │ │ │ └── backbone.layoutmanager.coffee │ │ ├── mockup.coffee │ │ ├── models.coffee │ │ ├── models │ │ │ ├── item.coffee │ │ │ ├── item_key.coffee │ │ │ ├── keypair.coffee │ │ │ ├── keypair_authenticator.coffee │ │ │ └── keypair_generator.coffee │ │ ├── routers │ │ │ ├── item_router.coffee │ │ │ └── key_router.coffee │ │ ├── templates │ │ │ ├── item │ │ │ │ ├── create.mustache │ │ │ │ ├── edit.mustache │ │ │ │ ├── list.mustache │ │ │ │ ├── list_item.mustache │ │ │ │ └── show.mustache │ │ │ ├── keypair │ │ │ │ ├── create.mustache │ │ │ │ ├── download.mustache │ │ │ │ ├── load.mustache │ │ │ │ └── unlock.mustache │ │ │ ├── main.mustache │ │ │ └── setup.mustache │ │ ├── ui.coffee │ │ └── views │ │ │ ├── item │ │ │ ├── _form.coffee │ │ │ ├── create.coffee │ │ │ ├── edit.coffee │ │ │ ├── list.coffee │ │ │ ├── list_item.coffee │ │ │ └── show.coffee.erb │ │ │ └── keypair │ │ │ ├── create.coffee │ │ │ ├── download.coffee │ │ │ ├── load.coffee │ │ │ └── unlock.coffee │ └── stylesheets │ │ ├── application.css │ │ └── mockup.scss ├── controllers │ ├── application_controller.rb │ ├── auth │ │ └── rsa_controller.rb │ ├── dashboard_controller.rb │ └── items_controller.rb ├── mailers │ └── .gitkeep ├── models │ ├── item.rb │ ├── rsa_challenge.rb │ ├── share.rb │ └── user.rb ├── presenters │ ├── item_list_presenter.rb │ ├── item_presenter.rb │ └── user_presenter.rb └── views │ ├── dashboard │ ├── index.html.erb │ └── mockup.html │ └── layouts │ └── application.html.erb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cucumber.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── secret_token.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml └── routes.rb ├── db ├── migrate │ ├── 20130120120320_create_items.rb │ ├── 20130120120408_create_shares.rb │ ├── 20130120120503_create_users.rb │ └── 20130212195016_change_public_key_to_text.rb ├── schema.rb └── seeds.rb ├── docs ├── DataFormat.md └── core.md ├── lib └── tasks │ ├── brakeman.rake │ └── jasmine.rake ├── log └── .gitkeep ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico └── robots.txt ├── script ├── bootstrap └── rails ├── spec ├── acceptance │ ├── item_spec.rb │ └── key_spec.rb ├── controllers │ ├── auth │ │ └── rsa_controller_spec.rb │ └── items_controller_spec.rb ├── fixtures │ ├── priv.pem │ └── pub.pem ├── javascripts │ ├── fixtures │ │ ├── priv.pem │ │ └── pub.pem │ ├── models │ │ ├── item_key_spec.coffee │ │ ├── item_spec.coffee │ │ ├── keypair_authenticator_spec.coffee │ │ └── keypair_spec.coffee │ ├── routers │ │ └── item_router_spec.coffee │ ├── spec.css │ ├── spec.js.coffee │ └── views │ │ └── item │ │ └── create_spec.coffee ├── models │ ├── item_spec.rb │ ├── rsa_challenge_spec.rb │ └── user_spec.rb ├── presenters │ ├── item_list_presenter_spec.rb │ ├── item_presenter_spec.rb │ └── user_presenter_spec.rb ├── spec_helper.rb └── support │ ├── acceptance.rb │ ├── controller_spec_helpers.rb │ ├── fixtures.rb │ ├── mocks.rb │ └── webmock.rb └── vendor └── assets ├── javascripts ├── ZeroClipboard.js ├── backbone.js ├── backbone.layoutmanager.js ├── forge.coffee ├── forge │ ├── aes.js │ ├── asn1.js │ ├── des.js │ ├── hmac.js │ ├── jsbn.js │ ├── md5.js │ ├── mgf1.js │ ├── oids.js │ ├── pbkdf2.js │ ├── pki.js │ ├── prng.js │ ├── pss.js │ ├── random.js │ ├── rc2.js │ ├── rsa.js │ ├── sha1.js │ ├── sha256.js │ └── util.js ├── form2js.js ├── jquery.js ├── jquery.toObject.js ├── mustache.js └── underscore.js └── swf └── ZeroClipboard.swf /.env: -------------------------------------------------------------------------------- 1 | # These values are used only in test and development mode. To run Swordfish in 2 | # production, set these variables in your production server environment. 3 | 4 | MONGODB_URI=mongodb://localhost:27017 5 | 6 | # These keys are used for RSA key authentication. It is essential that they be 7 | # changed to new values in production to maintain authentication security. 8 | # 9 | # Generted from `rails console` with `SecureRandom.base64(16)` 10 | AUTH_SECRET=2ObZZxmcFJi5LZa97sv5KyAe+TLA0kqR 11 | # Generted from `rails console` with `SecureRandom.base64(64)` 12 | SECRET_TOKEN=Ik7iwp1YV2y0zpHSgk+ZY5dIP3uJ9bAzI4Tr3j/1SK3OZ8kHqw/TK1zebDNd22aCFr331b6OYoi6Hnwsg4tB6A== 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /log/*.log 3 | /tmp 4 | .DS_Store 5 | /.rbenv-version 6 | /bin 7 | /vendor/gems 8 | /db/*.sqlite3 9 | .ruby-version 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | env: GH_POSTGRESQL_USER=postgres 5 | before_script: 6 | - psql -c 'create database swordfish_test;' -U postgres 7 | - rake db:test:load 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## If you find a security vulnerability: 4 | 5 | 1. **DO NOT POST ABOUT IT PUBLICLY** 6 | 2. Send an email to bkeepers@github.com with details about the security vulnerability. 7 | 3. After a fix has been released, a public announcement will be made giving all glory and honor to you. 8 | 9 | ## If you find what looks like a bug: 10 | 11 | 1. Search the [mailing list](https://groups.google.com/group/swordfishapp) to see if anyone else had the same issue. 12 | 2. Check the [GitHub issue tracker](https://github.com/github/swordfish/issues) to see if anyone else has reported issue. 13 | 3. If you don't see anything, [create an issue](https://github.com/github/swordfish/issues/new) with information on how to reproduce the issue. 14 | 15 | ## If you want to contribute an enhancement or a fix: 16 | 17 | 1. Fork the project on GitHub. 18 | 2. Make your changes with tests. 19 | 3. Commit the changes without making changes to the Rakefile, Gemfile, gemspec, or any other files that aren't related to your enhancement or fix. 20 | 4. Send a pull request. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '1.9.3' 4 | 5 | gem 'rails' 6 | gem 'pg' 7 | gem 'net-ssh' # for RSA key fingerprinting 8 | gem 'dotenv', :group => [:test, :development] 9 | gem 'strong_parameters' 10 | 11 | group :assets do 12 | gem 'sass-rails' 13 | gem 'compass-rails' 14 | gem 'coffee-rails' 15 | gem 'uglifier' 16 | gem 'hogan_assets' 17 | end 18 | 19 | group :development, :test do 20 | gem 'rspec-rails' 21 | gem 'jasminerice' 22 | gem 'guard-jasmine' 23 | end 24 | 25 | group :test do 26 | gem 'webmock', :require => false 27 | gem 'poltergeist' 28 | gem 'database_cleaner' 29 | gem 'brakeman', :require => false 30 | gem 'launchy' 31 | gem 'pry' 32 | end 33 | 34 | group :guard do 35 | gem 'guard-bundler' 36 | gem 'guard-rspec' 37 | end 38 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (3.2.13) 5 | actionpack (= 3.2.13) 6 | mail (~> 2.5.3) 7 | actionpack (3.2.13) 8 | activemodel (= 3.2.13) 9 | activesupport (= 3.2.13) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | journey (~> 1.0.4) 13 | rack (~> 1.4.5) 14 | rack-cache (~> 1.2) 15 | rack-test (~> 0.6.1) 16 | sprockets (~> 2.2.1) 17 | activemodel (3.2.13) 18 | activesupport (= 3.2.13) 19 | builder (~> 3.0.0) 20 | activerecord (3.2.13) 21 | activemodel (= 3.2.13) 22 | activesupport (= 3.2.13) 23 | arel (~> 3.0.2) 24 | tzinfo (~> 0.3.29) 25 | activeresource (3.2.13) 26 | activemodel (= 3.2.13) 27 | activesupport (= 3.2.13) 28 | activesupport (3.2.13) 29 | i18n (= 0.6.1) 30 | multi_json (~> 1.0) 31 | addressable (2.2.8) 32 | arel (3.0.2) 33 | brakeman (1.9.1) 34 | erubis (~> 2.6) 35 | fastercsv (~> 1.5) 36 | haml (~> 3.0) 37 | highline (~> 1.6) 38 | multi_json (~> 1.3) 39 | ruby2ruby (~> 2.0) 40 | ruby_parser (~> 3.1.1) 41 | sass (~> 3.0) 42 | terminal-table (~> 1.4) 43 | builder (3.0.4) 44 | capybara (1.1.4) 45 | mime-types (>= 1.16) 46 | nokogiri (>= 1.3.3) 47 | rack (>= 1.0.0) 48 | rack-test (>= 0.5.4) 49 | selenium-webdriver (~> 2.0) 50 | xpath (~> 0.1.4) 51 | childprocess (0.3.8) 52 | ffi (~> 1.0, >= 1.0.11) 53 | chunky_png (1.2.5) 54 | coderay (1.0.8) 55 | coffee-rails (3.2.2) 56 | coffee-script (>= 2.2.0) 57 | railties (~> 3.2.0) 58 | coffee-script (2.2.0) 59 | coffee-script-source 60 | execjs 61 | coffee-script-source (1.4.0) 62 | compass (0.12.2) 63 | chunky_png (~> 1.2) 64 | fssm (>= 0.2.7) 65 | sass (~> 3.1) 66 | compass-rails (1.0.3) 67 | compass (>= 0.12.2, < 0.14) 68 | crack (0.3.1) 69 | database_cleaner (0.9.1) 70 | diff-lcs (1.1.3) 71 | dotenv (0.1.0) 72 | erubis (2.7.0) 73 | eventmachine (1.0.0) 74 | execjs (1.4.0) 75 | multi_json (~> 1.0) 76 | fastercsv (1.5.5) 77 | faye-websocket (0.4.6) 78 | eventmachine (>= 0.12.0) 79 | ffi (1.3.1) 80 | fssm (0.2.9) 81 | guard (1.2.3) 82 | listen (>= 0.4.2) 83 | thor (>= 0.14.6) 84 | guard-bundler (1.0.0) 85 | bundler (~> 1.0) 86 | guard (~> 1.1) 87 | guard-jasmine (1.11.1) 88 | childprocess 89 | guard (>= 1.1.0) 90 | multi_json 91 | thor 92 | guard-rspec (1.2.0) 93 | guard (>= 1.1) 94 | haml (3.1.7) 95 | highline (1.6.15) 96 | hike (1.2.1) 97 | hogan_assets (1.3.1) 98 | execjs (>= 1.2.9) 99 | sprockets (>= 2.0.3) 100 | tilt (>= 1.3.3) 101 | http_parser.rb (0.5.3) 102 | i18n (0.6.1) 103 | jasminerice (0.0.10) 104 | coffee-rails 105 | haml 106 | journey (1.0.4) 107 | json (1.7.7) 108 | launchy (2.1.0) 109 | addressable (~> 2.2.6) 110 | listen (0.4.7) 111 | rb-fchange (~> 0.0.5) 112 | rb-fsevent (~> 0.9.1) 113 | rb-inotify (~> 0.8.8) 114 | mail (2.5.3) 115 | i18n (>= 0.4.0) 116 | mime-types (~> 1.16) 117 | treetop (~> 1.4.8) 118 | method_source (0.8.1) 119 | mime-types (1.22) 120 | multi_json (1.7.2) 121 | net-ssh (2.5.2) 122 | nokogiri (1.5.6) 123 | pg (0.14.1) 124 | poltergeist (1.0.2) 125 | capybara (~> 1.1) 126 | childprocess (~> 0.3) 127 | faye-websocket (~> 0.4, >= 0.4.4) 128 | http_parser.rb (~> 0.5.3) 129 | multi_json (~> 1.0) 130 | polyglot (0.3.3) 131 | pry (0.9.10) 132 | coderay (~> 1.0.5) 133 | method_source (~> 0.8) 134 | slop (~> 3.3.1) 135 | rack (1.4.5) 136 | rack-cache (1.2) 137 | rack (>= 0.4) 138 | rack-ssl (1.3.3) 139 | rack 140 | rack-test (0.6.2) 141 | rack (>= 1.0) 142 | rails (3.2.13) 143 | actionmailer (= 3.2.13) 144 | actionpack (= 3.2.13) 145 | activerecord (= 3.2.13) 146 | activeresource (= 3.2.13) 147 | activesupport (= 3.2.13) 148 | bundler (~> 1.0) 149 | railties (= 3.2.13) 150 | railties (3.2.13) 151 | actionpack (= 3.2.13) 152 | activesupport (= 3.2.13) 153 | rack-ssl (~> 1.3.2) 154 | rake (>= 0.8.7) 155 | rdoc (~> 3.4) 156 | thor (>= 0.14.6, < 2.0) 157 | rake (10.0.4) 158 | rb-fchange (0.0.5) 159 | ffi 160 | rb-fsevent (0.9.1) 161 | rb-inotify (0.8.8) 162 | ffi (>= 0.5.0) 163 | rdoc (3.12.2) 164 | json (~> 1.4) 165 | rspec (2.11.0) 166 | rspec-core (~> 2.11.0) 167 | rspec-expectations (~> 2.11.0) 168 | rspec-mocks (~> 2.11.0) 169 | rspec-core (2.11.0) 170 | rspec-expectations (2.11.1) 171 | diff-lcs (~> 1.1.3) 172 | rspec-mocks (2.11.1) 173 | rspec-rails (2.11.0) 174 | actionpack (>= 3.0) 175 | activesupport (>= 3.0) 176 | railties (>= 3.0) 177 | rspec (~> 2.11.0) 178 | ruby2ruby (2.0.2) 179 | ruby_parser (~> 3.1) 180 | sexp_processor (~> 4.0) 181 | ruby_parser (3.1.1) 182 | sexp_processor (~> 4.1) 183 | rubyzip (0.9.9) 184 | sass (3.2.5) 185 | sass-rails (3.2.6) 186 | railties (~> 3.2.0) 187 | sass (>= 3.1.10) 188 | tilt (~> 1.3) 189 | selenium-webdriver (2.29.0) 190 | childprocess (>= 0.2.5) 191 | multi_json (~> 1.0) 192 | rubyzip 193 | websocket (~> 1.0.4) 194 | sexp_processor (4.1.4) 195 | slop (3.3.3) 196 | sprockets (2.2.2) 197 | hike (~> 1.2) 198 | multi_json (~> 1.0) 199 | rack (~> 1.0) 200 | tilt (~> 1.1, != 1.3.0) 201 | strong_parameters (0.1.6) 202 | actionpack (~> 3.0) 203 | activemodel (~> 3.0) 204 | railties (~> 3.0) 205 | terminal-table (1.4.5) 206 | thor (0.18.1) 207 | tilt (1.3.6) 208 | treetop (1.4.12) 209 | polyglot 210 | polyglot (>= 0.3.1) 211 | tzinfo (0.3.37) 212 | uglifier (1.2.4) 213 | execjs (>= 0.3.0) 214 | multi_json (>= 1.0.2) 215 | webmock (1.8.8) 216 | addressable (~> 2.2.8) 217 | crack (>= 0.1.7) 218 | websocket (1.0.7) 219 | xpath (0.1.4) 220 | nokogiri (~> 1.3) 221 | 222 | PLATFORMS 223 | ruby 224 | 225 | DEPENDENCIES 226 | brakeman 227 | coffee-rails 228 | compass-rails 229 | database_cleaner 230 | dotenv 231 | guard-bundler 232 | guard-jasmine 233 | guard-rspec 234 | hogan_assets 235 | jasminerice 236 | launchy 237 | net-ssh 238 | pg 239 | poltergeist 240 | pry 241 | rails 242 | rspec-rails 243 | sass-rails 244 | strong_parameters 245 | uglifier 246 | webmock 247 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'bundler' do 2 | watch('Gemfile') 3 | end 4 | 5 | guard 'rspec', :version => 2 do 6 | watch(%r{^spec/.+_spec\.rb$}) 7 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 8 | watch('spec/spec_helper.rb') { "spec" } 9 | 10 | # Rails example 11 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 12 | watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 13 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 14 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 15 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 16 | end 17 | 18 | guard :jasmine do 19 | watch(%r{spec/javascripts/spec\.(js\.coffee|js|coffee)$}) { 'spec/javascripts' } 20 | watch(%r{spec/javascripts/.+_spec\.(js\.coffee|js|coffee)$}) 21 | watch(%r{app/assets/javascripts/(.+?)\.(js\.coffee|js|coffee)(?:\.\w+)*$}) { |m| "spec/javascripts/#{ m[1] }_spec.#{ m[2] }" } 22 | end 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE: This repository is no longer supported or updated by GitHub. If you wish to continue to develop this code yourself, we recommend you fork it.** 2 | 3 | # Swordfish [![Build Status](https://secure.travis-ci.org/github/swordfish.png?branch=master)](http://travis-ci.org/github/swordfish) 4 | 5 | Swordfish was an experiment in building a group-optimized password management 6 | app. It is unmaintained and is likely insecure. 7 | 8 | ## Recommended Reading 9 | 10 | * [Why passwords have never been weaker—and crackers have never been stronger](http://arstechnica.com/security/2012/08/passwords-under-assault/) - A great article about password cracking techniques and the implications for choosing strong passwords. 11 | * [JavaScript Cryptography Considered Harmful](http://www.matasano.com/articles/javascript-cryptography/) - A thoughtful critique of JavaScript encryption, discussed in [#28](https://github.com/github/swordfish/issues/28). 12 | * [A JavaScript Implementation of TLS](http://digitalbazaar.com/2010/07/20/javascript-tls-1/) - The background of [Forge](https://github.com/digitalbazaar/forge/blob/master/README.md), the encryption library used by Swordfish. 13 | * [Web Cryptography API](http://www.w3.org/2012/webcrypto/WebCryptoAPI/) - A draft W3C proposal for a JavaScript cryptography API. If all goes well, this will eventually replace Forge for all of the cryptography in Swordfish. 14 | * [Megabad: A quick look at the state of Mega’s encryption](http://arstechnica.com/business/2013/01/megabad-a-quick-look-at-the-state-of-megas-encryption/) - Mega essentially uses the same approach as Swordfish: RSA keys for each user, coupled with a randomly generated key for AES encryption of each item. 15 | 16 | ## Working on Swordfish 17 | 18 | Use the `bootstrap` script to get the environment set up. 19 | 20 | script/bootstrap 21 | 22 | Now you will need to run the database migrations before you run the server. 23 | 24 | bundle exec rake db:create db:migrate 25 | 26 | Finally you can start the application. 27 | 28 | script/rails s 29 | 30 | Running the tests requires PhantomJS. If you're on Mac OS X you can use homebrew 31 | to install this for you. 32 | 33 | brew install phantomjs 34 | 35 | If you hack on Swordfish and end up adding or editing features you will want to 36 | run the tests. 37 | 38 | bundle exec rake 39 | 40 | Want to join the [core team](https://github.com/github/swordfish/blob/master/docs/core.md)? 41 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Swordfish::Application.load_tasks 8 | 9 | task :default => 'guard:jasmine' 10 | -------------------------------------------------------------------------------- /app/assets/images/emoji/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/emoji/warning.png -------------------------------------------------------------------------------- /app/assets/images/emoji/white_check_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/emoji/white_check_mark.png -------------------------------------------------------------------------------- /app/assets/images/mockup/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/mockup/grid.png -------------------------------------------------------------------------------- /app/assets/images/mockup/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/mockup/home.png -------------------------------------------------------------------------------- /app/assets/images/mockup/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/mockup/person.png -------------------------------------------------------------------------------- /app/assets/images/mockup/retina_wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/mockup/retina_wood.png -------------------------------------------------------------------------------- /app/assets/images/mockup/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/assets/images/mockup/web.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.coffee: -------------------------------------------------------------------------------- 1 | #= require jquery 2 | #= require models 3 | 4 | class @Application 5 | _.extend @prototype, Backbone.Events 6 | 7 | @on: (args...) -> 8 | @prototype.on(args...) 9 | 10 | constructor: (@keypair = Keypair.load()) -> 11 | @trigger 'initialize' 12 | 13 | setKey: (key) -> 14 | @keypair = new Keypair(key) 15 | @keypair.savePrivateKey() 16 | @keypair 17 | 18 | authenticate: -> 19 | new KeypairAuthenticator(@keypair).request() 20 | 21 | # FIXME: make a class for UI concerns and move this there 22 | layout: (layout) -> 23 | unless layout == @current_layout 24 | @current_layout = layout 25 | $(document.body).html(layout.el) 26 | layout.render() 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/collections/item.coffee: -------------------------------------------------------------------------------- 1 | class Item.Collection extends Backbone.Collection 2 | model: Item 3 | url: '/items' 4 | 5 | constructor: (models, options) -> 6 | @keypair = options.keypair 7 | super 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/ext/backbone.coffee: -------------------------------------------------------------------------------- 1 | # Swordfish customizations to Backbone 2 | 3 | _.extend Backbone.Collection.prototype, 4 | load: (id) -> 5 | result = @deferred() 6 | if record = @get(id) 7 | result.resolve(record) 8 | else 9 | finder = => 10 | @off 'reset', finder 11 | result.resolve(@get(id)) 12 | @on 'reset', finder 13 | result 14 | 15 | deferred: -> 16 | jQuery.Deferred() 17 | 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/ext/backbone.layoutmanager.coffee: -------------------------------------------------------------------------------- 1 | # Swordfish customizations to LayoutManager 2 | 3 | Backbone.LayoutManager.configure( 4 | manage: true 5 | 6 | fetch: (path) -> 7 | HoganTemplates[path] 8 | 9 | render: (template, context) -> 10 | template.render(context || {}) 11 | ) 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/mockup.coffee: -------------------------------------------------------------------------------- 1 | #= require lib/jquery 2 | 3 | $(document).on 'keypress', (e) -> 4 | if e.keyCode == 96 5 | $(document.body).toggleClass('grid') 6 | 7 | $ -> 8 | $('body').append('
'); -------------------------------------------------------------------------------- /app/assets/javascripts/models.coffee: -------------------------------------------------------------------------------- 1 | #= require underscore 2 | #= require backbone 3 | #= require ext/backbone 4 | #= require forge 5 | #= require_tree ./models 6 | #= require_tree ./collections 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/item.coffee: -------------------------------------------------------------------------------- 1 | class @Item extends Backbone.Model 2 | initialize: -> 3 | @set 'key', @collection.keypair.encrypt(ItemKey.generate()) unless @has('key') 4 | @on 'change:data', @encryptData 5 | @encryptData() 6 | 7 | encryptData: -> 8 | if data = @get('data') 9 | @set 'encrypted_data', @key().encrypt(data) 10 | @unset 'data' 11 | 12 | key: -> 13 | new ItemKey(@collection.keypair.decrypt(@get('key'))) 14 | 15 | data: -> 16 | @key().decrypt(@get('encrypted_data')) -------------------------------------------------------------------------------- /app/assets/javascripts/models/item_key.coffee: -------------------------------------------------------------------------------- 1 | class @ItemKey 2 | @cipher: forge.aes 3 | @key_size: 256 / 8 4 | 5 | @generate: -> 6 | forge.random.getBytes(@key_size) 7 | 8 | constructor: (@key) -> 9 | 10 | encrypt: (data) -> 11 | iv = forge.random.getBytes(16) 12 | buffer = forge.util.createBuffer(JSON.stringify(data)) 13 | cipher = @constructor.cipher.startEncrypting(@key, iv) 14 | 15 | cipher.update(buffer) 16 | cipher.finish() 17 | ciphertext = cipher.output.data 18 | 19 | JSON.stringify 20 | iv : forge.util.encode64(iv) 21 | data: forge.util.encode64(ciphertext) 22 | 23 | decrypt: (data) -> 24 | params = JSON.parse(data) 25 | iv = forge.util.decode64(params['iv']) 26 | ciphertext = forge.util.decode64(params['data']) 27 | buffer = forge.util.createBuffer(ciphertext) 28 | cipher = forge.aes.startDecrypting(@key, iv) 29 | 30 | cipher.update(buffer) 31 | cipher.finish() 32 | JSON.parse(cipher.output.data) 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/keypair.coffee: -------------------------------------------------------------------------------- 1 | class @Keypair 2 | @localStorage: window.localStorage 3 | @ajax: jQuery.ajax 4 | 5 | @load: -> 6 | new @(key) if key = @localStorage['privateKey'] 7 | 8 | constructor: (@privateKeyPem) -> 9 | 10 | savePrivateKey: () -> 11 | @constructor.localStorage['privateKey'] = @privateKeyPem 12 | 13 | # Public: Unlock the keypair with the given passphrase. 14 | # 15 | # Returns a jQuery.Deferred() that will resolve when successful. 16 | unlock: (password) -> 17 | if @privateKey = forge.pki.decryptRsaPrivateKey(@privateKeyPem, password) 18 | @publicKey = forge.pki.rsa.setPublicKey(@privateKey.n, @privateKey.e) 19 | 20 | @isUnlocked() 21 | 22 | isUnlocked: -> 23 | !!@privateKey 24 | 25 | encrypt: (data) -> 26 | forge.util.encode64(@publicKey.encrypt(data)) 27 | 28 | decrypt: (data) -> 29 | @privateKey.decrypt(forge.util.decode64(data)) 30 | 31 | publicKeyPem: -> 32 | forge.pki.publicKeyToPem(@publicKey) 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/keypair_authenticator.coffee: -------------------------------------------------------------------------------- 1 | class @KeypairAuthenticator 2 | @ajax: jQuery.ajax 3 | 4 | constructor: (@keypair) -> 5 | jQuery.extend @, jQuery.Deferred() 6 | @done @setupAjax 7 | 8 | # Public: Request a challenge from the server. 9 | request: -> 10 | @constructor.ajax( 11 | type: 'POST' 12 | url: '/auth/rsa' 13 | data: @keypair.publicKeyPem() 14 | dataType: 'text' 15 | ).done @respond 16 | @ 17 | 18 | # Internal: Respond to the challenge from the server. 19 | respond: (data) => 20 | @challenge = @decryptChallenge(data) 21 | 22 | @constructor.ajax( 23 | type: 'PUT' 24 | url: '/auth/rsa' 25 | dataType: 'text' 26 | data: @challenge 27 | ).done => @resolve(@challenge) 28 | 29 | # Internal: Decrypt the challenge from the server. 30 | decryptChallenge: (challenge) -> 31 | forge.util.encode64(@keypair.decrypt(challenge)) 32 | 33 | # Internal 34 | setupAjax: => 35 | jQuery.ajaxPrefilter @setHeader 36 | 37 | # Internal 38 | setHeader: (options, originalOptions, xhr) => 39 | xhr.setRequestHeader('X-Challenge', @challenge) 40 | -------------------------------------------------------------------------------- /app/assets/javascripts/models/keypair_generator.coffee: -------------------------------------------------------------------------------- 1 | # Generate an RSA public and encrypted private key. 2 | # 3 | # Generating a keypair is time intensive, but this implementation works in 4 | # chunks, pausing to allow the UI to update. 5 | # 6 | # generator = new KeypairGenerator('mypassphrase').start() 7 | # 8 | # generator.progress -> 9 | # console.log 'progress' 10 | # updateUI() 11 | # 12 | # generator.done (pub, priv) -> 13 | # console.log 'generated keys', pub, priv 14 | # saveKeys(pub, priv) 15 | # 16 | class @KeypairGenerator 17 | keysize: 1024 18 | interval: 10 19 | 20 | constructor: (@passphrase) -> 21 | @deferred = jQuery.Deferred() 22 | 23 | start: -> 24 | @state = forge.pki.rsa.createKeyPairGenerationState(@keysize) 25 | @delay @progress 26 | @deferred 27 | 28 | progress: => 29 | if forge.pki.rsa.stepKeyPairGenerationState(@state, @interval) 30 | @finish() 31 | else 32 | @deferred.notify(@state) 33 | @delay @progress 34 | 35 | finish: -> 36 | @deferred.resolve(@publicKey(), @encryptedPrivateKey()) 37 | 38 | encryptedPrivateKey: -> 39 | forge.pki.encryptRsaPrivateKey(@state.keys.privateKey, @passphrase) 40 | 41 | publicKey: -> 42 | forge.pki.publicKeyToPem(@state.keys.publicKey) 43 | 44 | # Delay to prevent from locking the interface. 45 | # 46 | # Override in tests to allow synchronous tests. 47 | delay: (fn) -> 48 | _.delay(fn, 0) 49 | 50 | -------------------------------------------------------------------------------- /app/assets/javascripts/routers/item_router.coffee: -------------------------------------------------------------------------------- 1 | class @ItemRouter extends Backbone.Router 2 | routes: 3 | '': 'items' 4 | 'items/new': 'newItem' 5 | 'items/:id': 'item' 6 | 'items/:id/edit': 'edit' 7 | 8 | constructor: (options) -> 9 | super 10 | 11 | @app = options.app 12 | @on 'all', @ensureLayout 13 | 14 | @itemsCollection = new Item.Collection([], keypair: @app.keypair) 15 | 16 | @layout = new Backbone.LayoutManager({ 17 | template: "templates/main", 18 | 19 | views: 20 | "#items": new Item.Views.List(collection: @itemsCollection) 21 | "#details": @details = new Backbone.View() 22 | }) 23 | 24 | ensureLayout: => 25 | @app.layout @layout 26 | 27 | items: => 28 | # ensure items collection has access to the keypair 29 | @itemsCollection.keypair = @app.keypair 30 | @itemsCollection.fetch() 31 | 32 | newItem: (id) => 33 | @content new Item.Views.Create(collection: @itemsCollection) 34 | 35 | item: (id) => 36 | @itemsCollection.load(id).then (item) => 37 | @content new Item.Views.Show(model: item) 38 | 39 | edit: (id) => 40 | @itemsCollection.load(id).then (item) => 41 | @content new Item.Views.Edit(model: item) 42 | 43 | content: (view) -> 44 | @details.setView(view).render() 45 | -------------------------------------------------------------------------------- /app/assets/javascripts/routers/key_router.coffee: -------------------------------------------------------------------------------- 1 | class @KeyRouter extends Backbone.Router 2 | routes: 3 | 'key/new': 'newKey' 4 | 'key/download': 'download' 5 | 'key/load': 'load' 6 | 'key/unlock': 'unlock' 7 | 8 | constructor: (options) -> 9 | super 10 | @app = options.app 11 | @layout = new Backbone.LayoutManager( 12 | template: 'templates/setup' 13 | ) 14 | @on 'all', @ensureLayout 15 | 16 | if @app.keypair 17 | window.location.hash = "#key/unlock" unless @app.keypair.isUnlocked() 18 | else 19 | window.location.hash = "#key/new" 20 | 21 | newKey: -> 22 | @layout.setView('#content', new Keypair.Views.Create(app: @app)).render() 23 | 24 | download: -> 25 | @layout.setView('#content', new Keypair.Views.Download(app: @app)).render() 26 | 27 | load: -> 28 | @layout.setView('#content', new Keypair.Views.Load(app: @app)).render() 29 | 30 | unlock: -> 31 | @layout.setView('#content', new Keypair.Views.Unlock(app: @app)).render() 32 | 33 | ensureLayout: => 34 | @app.layout @layout 35 | 36 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/item/create.mustache: -------------------------------------------------------------------------------- 1 |
2 |

Create a New Item

3 |

4 |

5 | 6 | 7 |

8 | 9 |

10 | 11 | 12 |

13 | 14 |

15 | 16 | 17 |

18 | 19 |

20 | 21 | 22 |

23 | 24 |

25 | 26 | 27 | 28 |

29 | 30 | 33 |
34 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/item/edit.mustache: -------------------------------------------------------------------------------- 1 |
2 |

{{title}}

3 |

4 |

5 | 6 | 7 |

8 | 9 | {{#data}} 10 |

11 | 12 | 13 |

14 | 15 |

16 | 17 | 18 |

19 | 20 |

21 | 22 | 23 |

24 | 25 |

26 | 27 | 28 | 29 |

30 | {{/data}} 31 | 32 | 35 |
36 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/item/list.mustache: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/item/list_item.mustache: -------------------------------------------------------------------------------- 1 | 2 | {{title}} 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/item/show.mustache: -------------------------------------------------------------------------------- 1 |
2 |

{{title}}

3 | {{#data}} 4 |
5 |
Username
6 |
{{username}}
7 |
URL
8 |
{{url}}
9 |
Password
10 |
11 | 12 | •••••••••••• 13 | reveal 14 | hide 15 | 16 | copy to clipboard 17 |
18 |
19 | {{/data}} 20 | 21 | 24 |
25 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/keypair/create.mustache: -------------------------------------------------------------------------------- 1 |
2 |

Create a Key

3 | 4 |

Enter a passphrase that will be used to secure your private key.

5 | 6 | 7 | 8 | 12 | 13 |
14 |
-------------------------------------------------------------------------------- /app/assets/javascripts/templates/keypair/download.mustache: -------------------------------------------------------------------------------- 1 |

Download Your Private Key

2 | 3 |

This private key and your passphrase is required to access all of your data. It is not saved on the server, so you will need to upload it whenever you sign in. If you lose this key, you will not be able to access your data.

4 | 5 | Download Private Key 6 | 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/keypair/load.mustache: -------------------------------------------------------------------------------- 1 |

Load Private Key

2 | 3 |
4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/keypair/unlock.mustache: -------------------------------------------------------------------------------- 1 |
2 |

Unlock

3 | 4 |

Enter your passphrase to unlock your private key.

5 |

6 | 7 | 8 | 9 | 10 |
-------------------------------------------------------------------------------- /app/assets/javascripts/templates/main.mustache: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/templates/setup.mustache: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/ui.coffee: -------------------------------------------------------------------------------- 1 | #= require hogan 2 | #= require backbone.layoutmanager 3 | #= require form2js 4 | #= require jquery.toObject 5 | #= require mustache 6 | #= require ext/backbone.layoutmanager 7 | 8 | #= require_tree ./templates 9 | #= require_tree ./views 10 | #= require_tree ./routers 11 | 12 | Application.on 'initialize', -> 13 | new KeyRouter(app: @) 14 | new ItemRouter(app: @) 15 | Backbone.history.start() 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/item/_form.coffee: -------------------------------------------------------------------------------- 1 | Item.Views ?= {} 2 | 3 | class Item.Views.Form extends Backbone.View 4 | events: 5 | 'submit form': 'submit' 6 | 'keyup #item-password': 'checkPassword' 7 | 'change #item-password': 'checkPassword' 8 | 'keyup #item-confirm-password': 'checkPassword' 9 | 'change #item-confirm-password': 'checkPassword' 10 | 11 | checkPassword: => 12 | @$('#item-password-confirmation-status').attr 'class', 13 | if @passwordConfirmed() 14 | if @password() 15 | 'match' 16 | else 17 | '' 18 | else 19 | 'mismatch' 20 | 21 | submit: (event) => 22 | event.preventDefault() 23 | if @passwordConfirmed() 24 | params = @$('form').toObject(mode: 'combine') 25 | params.data ||= {} 26 | params.data.password = @password() 27 | @save(params) 28 | else 29 | @$('.error').text("Password doesn't match Confirm. Please type your password again.") 30 | 31 | passwordConfirmed: -> 32 | @password() == @passwordConfirmation() 33 | 34 | password: -> @$('#item-password').val() 35 | passwordConfirmation: -> @$('#item-confirm-password').val() 36 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/item/create.coffee: -------------------------------------------------------------------------------- 1 | Item.Views ?= {} 2 | 3 | class Item.Views.Create extends Item.Views.Form 4 | template: 'templates/item/create' 5 | 6 | save: (params) -> 7 | @collection.create params, success: (item) => 8 | Backbone.history.navigate "items/#{item.id}", true 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/item/edit.coffee: -------------------------------------------------------------------------------- 1 | Item.Views ?= {} 2 | 3 | class Item.Views.Edit extends Item.Views.Form 4 | template: 'templates/item/edit' 5 | 6 | save: (params) -> 7 | @model.save params, success: (item) => 8 | Backbone.history.navigate "items/#{item.id}", true 9 | 10 | serialize: -> 11 | _.extend @model.toJSON(), data: @model.data() 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/item/list.coffee: -------------------------------------------------------------------------------- 1 | Item.Views ?= {} 2 | 3 | class Item.Views.List extends Backbone.View 4 | template: 'templates/item/list' 5 | 6 | constructor: -> 7 | super 8 | @collection.on 'add reset', @render, @ 9 | 10 | beforeRender: => 11 | @collection.each @add 12 | 13 | add: (model) => 14 | @insertView '.items', new Item.Views.ListItem(model: model) 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/item/list_item.coffee: -------------------------------------------------------------------------------- 1 | Item.Views ?= {} 2 | 3 | class Item.Views.ListItem extends Backbone.View 4 | template: 'templates/item/list_item' 5 | className: 'item' 6 | 7 | constructor: -> 8 | super 9 | @model.on 'change', @render, @ 10 | 11 | serialize: -> 12 | _.extend @model.toJSON(), data: @model.data(), domain: @domain() 13 | 14 | domain: -> 15 | a = document.createElement('a') 16 | a.href = @model.data().url 17 | a.hostname 18 | 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/item/show.coffee.erb: -------------------------------------------------------------------------------- 1 | #= require ZeroClipboard 2 | 3 | Item.Views ?= {} 4 | 5 | class Item.Views.Show extends Backbone.View 6 | template: 'templates/item/show' 7 | 8 | events: 9 | 'click a.reveal': 'reveal' 10 | 'click a.hide': 'hide' 11 | 12 | afterRender: -> 13 | ZeroClipboard.config moviePath: "<%= asset_path 'ZeroClipboard.swf' %>" 14 | @clipboard = new ZeroClipboard @$('#copy_link')[0] 15 | @clipboard.on 'dataRequested', (client, args) => 16 | @clipboard.setText @model.data().password 17 | 18 | serialize: -> 19 | _.extend @model.toJSON(), data: @model.data() 20 | 21 | reveal: (e) => 22 | window.view = @ 23 | e.preventDefault() 24 | @$('.password').text(@model.data().password) 25 | @$el.addClass('revealed') 26 | 27 | hide: (e) => 28 | e.preventDefault() 29 | @$('.password').text('••••••••••••') 30 | @$el.removeClass('revealed') 31 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/keypair/create.coffee: -------------------------------------------------------------------------------- 1 | Keypair.Views ?= {} 2 | 3 | class Keypair.Views.Create extends Backbone.View 4 | template: 'templates/keypair/create' 5 | 6 | events: 7 | 'submit form': 'generate' 8 | 9 | constructor: (options) -> 10 | super 11 | @app = options.app 12 | 13 | generate: (event) => 14 | @passphrase = @$('input[name=passphrase]').val() 15 | generator = new KeypairGenerator(@passphrase).start() 16 | @start() 17 | generator.done @done 18 | false 19 | 20 | start: => 21 | @$('#status').text('Generating keys…') 22 | 23 | done: (publicKey, privateKey) => 24 | keypair = @app.setKey(privateKey) 25 | keypair.unlock(@passphrase) 26 | Backbone.history.navigate "key/download", true 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/keypair/download.coffee: -------------------------------------------------------------------------------- 1 | Keypair.Views ?= {} 2 | 3 | class Keypair.Views.Download extends Backbone.View 4 | template: 'templates/keypair/download' 5 | 6 | constructor: (options) -> 7 | @app = options.app 8 | super 9 | 10 | serialize: -> 11 | {href: @dataUri()} 12 | 13 | dataUri: -> 14 | "data:application/x-pem-file,#{encodeURIComponent(@app.keypair.privateKeyPem)}" 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/keypair/load.coffee: -------------------------------------------------------------------------------- 1 | Keypair.Views ?= {} 2 | 3 | class Keypair.Views.Load extends Backbone.View 4 | template: 'templates/keypair/load' 5 | 6 | events: 7 | 'change input[type=file]': 'change' 8 | 9 | constructor: (options) -> 10 | super 11 | @app = options.app 12 | 13 | change: (e) => 14 | file = e.target.files[0] 15 | reader = new FileReader() 16 | reader.onload = @load 17 | reader.readAsText(file) 18 | 19 | load: (e) => 20 | @app.setKey e.target.result 21 | Backbone.history.navigate 'key/unlock', true 22 | 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/views/keypair/unlock.coffee: -------------------------------------------------------------------------------- 1 | Keypair.Views ?= {} 2 | 3 | class Keypair.Views.Unlock extends Backbone.View 4 | template: 'templates/keypair/unlock' 5 | 6 | events: 7 | 'submit form': 'submit' 8 | 9 | constructor: (options) -> 10 | super 11 | @app = options.app 12 | 13 | afterRender: -> 14 | @$('input:first').focus() 15 | 16 | submit: (e) => 17 | if @app.keypair.unlock(@$('input[type=password]').val()) 18 | @app.authenticate().done(@done).fail(@fail) 19 | else 20 | @fail() 21 | 22 | false 23 | 24 | done: => 25 | Backbone.history.navigate '', true 26 | 27 | fail: => 28 | @$('p.error').text('Your passphrase was incorrect!') 29 | @$('input[type=password]').val('') 30 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/mockup.scss: -------------------------------------------------------------------------------- 1 | @import 'compass/reset'; 2 | @import "blueprint"; 3 | 4 | @include blueprint-typography; 5 | 6 | 7 | $container-color: rgba(110,83,46,0.2); 8 | $shadow-color: opacify($container-color, 0.1); 9 | 10 | // ==== debugging === 11 | 12 | #grid { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | right: 0; 17 | bottom: 0; 18 | background-image: image-url('mockup/grid.png'); 19 | display: none; 20 | z-index: 100; 21 | 22 | .grid & { 23 | display: block; 24 | } 25 | } 26 | 27 | // ==== basic ==== 28 | 29 | body { 30 | font-family: "Lucida Grande", Arial, Helvetica, sans-serif; 31 | background-image: image-url('mockup/retina_wood.png'); 32 | } 33 | 34 | a { 35 | text-decoration: none; 36 | } 37 | 38 | h1, h2, h3, h4, h5, h6 { 39 | font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; 40 | font-weight: 100; 41 | } 42 | 43 | // ==== layout ===== 44 | 45 | @mixin pane { 46 | position: absolute; 47 | top: 45px; 48 | bottom: 18px; 49 | overflow: hidden; 50 | 51 | footer { 52 | height: 53px; 53 | border-top: 1px solid #f7f7f7; 54 | line-height: 54px; 55 | border-bottom-left-radius: 4px; 56 | border-bottom-right-radius: 4px; 57 | @include background(linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,1))); 58 | padding: 0 18px; 59 | position: absolute; 60 | left: 0; 61 | right: 0; 62 | bottom: 0; 63 | z-index: 3; 64 | text-align: right; 65 | } 66 | 67 | .pane-content { 68 | z-index: 2; 69 | position: absolute; 70 | top: 0; 71 | right: 0; 72 | bottom: 0; 73 | left: 0; 74 | padding: 9px 0 72px 0; 75 | overflow: auto; 76 | } 77 | } 78 | 79 | #container { 80 | position: absolute; 81 | width: 960px; 82 | top: 18px; 83 | bottom: 18px; 84 | left: 50%; 85 | margin-left: -480px; 86 | background-color: $container-color; 87 | box-shadow: inset 0px 0px 5px $shadow-color; 88 | border-radius: 4px; 89 | 90 | &.setup { 91 | top: 50%; 92 | width: 480px; 93 | height: 300px; 94 | margin-left: -240px; 95 | margin-top: -150px; 96 | 97 | #content { 98 | @include pane; 99 | background: #fff; 100 | top: 18px; 101 | left: 18px; 102 | right: 18px; 103 | box-shadow: 0px 0px 4px $shadow-color; 104 | border-radius: 4px; 105 | padding: 18px; 106 | 107 | } 108 | } 109 | 110 | &>section { 111 | padding: 9px; 112 | } 113 | } 114 | 115 | @import 'compass/css3'; 116 | 117 | #header { 118 | height: 45px; 119 | padding: 0 18px; 120 | border-radius: 4px 4px 0 0; 121 | position: relative; 122 | 123 | h1 { 124 | font-size: 22px; 125 | line-height: 45px; 126 | color: #666; 127 | letter-spacing: 2px; 128 | } 129 | 130 | #search, nav { 131 | line-height: 27px; 132 | position: absolute; 133 | top: 9px; 134 | right: 18px; 135 | float: right; 136 | 137 | input { 138 | font-size: 14px; 139 | width: 200px; 140 | line-height: 18px; 141 | } 142 | } 143 | } 144 | 145 | #vaults { 146 | @include pane; 147 | left: 0px; 148 | width: 192px; 149 | padding-left: 18px; 150 | z-index: 20; 151 | 152 | .selected { 153 | background: #fff; 154 | box-shadow: -1px 1px 4px $shadow-color; 155 | } 156 | 157 | h1 { 158 | margin: 9px 0; 159 | color: #999; 160 | color: rgba(0,0,0,0.4); 161 | text-shadow: 0px 1px 1px rgba(255,255,255,0.3); 162 | font-size: 11px; 163 | line-height: 18px; 164 | font-weight: normal; 165 | text-transform: uppercase; 166 | position: relative; 167 | 168 | .add { 169 | position: absolute; 170 | top: 0; 171 | right: 9px; 172 | } 173 | } 174 | 175 | .vault { 176 | border-radius: 4px 0 0 4px; 177 | 178 | a { 179 | color: #000; 180 | display: block; 181 | padding: 18px; 182 | } 183 | 184 | img { 185 | @extend .icon; 186 | } 187 | } 188 | } 189 | 190 | #items { 191 | @include pane; 192 | background: #fff; 193 | z-index: 10; 194 | 195 | left: 210px; 196 | right: 50%; 197 | border-right: 1px dotted #f7f7f7; 198 | 199 | box-shadow: -1px 0px 4px $shadow-color; 200 | border-radius: 4px 0 0 4px; 201 | 202 | .item { 203 | display: block; 204 | padding: 9px 18px; 205 | img { 206 | padding-right: 5px; 207 | } 208 | a { 209 | vertical-align: top; 210 | } 211 | } 212 | } 213 | 214 | #details { 215 | z-index: 20; 216 | 217 | @include pane; 218 | background: #fff; 219 | left: 50%; 220 | right: 18px; 221 | border-radius: 0 4px 4px 0; 222 | box-shadow: 1px 0px 4px $shadow-color; 223 | 224 | header { 225 | margin-bottom: 36px; 226 | } 227 | 228 | .pane-content { 229 | padding-left: 18px; 230 | padding-right: 18px; 231 | } 232 | 233 | section { 234 | @include clearfix; 235 | padding-bottom: 9px; 236 | margin-bottom: 9px; 237 | } 238 | 239 | h1 { 240 | margin-bottom: 0; 241 | font-weight: 100; 242 | line-height: 54px; 243 | } 244 | 245 | .url { 246 | color: #bbb; 247 | span { 248 | color: #ddd; 249 | } 250 | } 251 | 252 | .preview { 253 | float: right; 254 | width: 168px; 255 | height: 126px; 256 | border-radius: 4px; 257 | overflow: hidden; 258 | box-shadow: 0px 1px 4px rgba(0,0,0,0.4); 259 | margin-right: 4px; 260 | 261 | img { 262 | width: 100%; 263 | } 264 | } 265 | 266 | .hide { display: none; } 267 | .revealed { 268 | .hide { display: inline; } 269 | .reveal { display: none; } 270 | } 271 | 272 | 273 | } 274 | 275 | .icon { 276 | width: 18px; 277 | height: 18px; 278 | vertical-align: middle; 279 | display: inline-block; 280 | margin-right: 4px; 281 | text-indent: -2000px; 282 | overflow: hidden; 283 | 284 | &.text { 285 | font-family: "Times New Roman", serif; 286 | text-indent: 0; 287 | background: #ccc; 288 | color: #FFF; 289 | border-radius: 9px; 290 | font-weight: bold; 291 | font-size: 14px; 292 | font-style: italic; 293 | line-height: 19px; 294 | text-align: center; 295 | } 296 | } 297 | 298 | dl { 299 | dt { 300 | clear: left; 301 | float: left; 302 | width: 63px; 303 | text-align: right; 304 | padding-right: 9px; 305 | color: #999; 306 | font-weight: normal; 307 | } 308 | dd { 309 | margin-left: 72px; 310 | min-height: 18px; 311 | } 312 | } 313 | 314 | form { 315 | p { 316 | @include clearfix; 317 | 318 | label { 319 | display: block; 320 | float: left; 321 | width: 84px; 322 | margin-right: 9px; 323 | color: #999; 324 | text-align: right; 325 | line-height: 36px; 326 | } 327 | } 328 | } 329 | 330 | #item-password-confirmation-status { 331 | display: inline-block; 332 | width: 32px; 333 | height: 32px; 334 | background-size: 32px; 335 | vertical-align: top; 336 | &.match { 337 | background-image: image-url('emoji/white_check_mark.png'); 338 | } 339 | &.mismatch { 340 | background-image: image-url('emoji/warning.png'); 341 | } 342 | } 343 | 344 | input[type=text], input[type=password], textarea { 345 | font-size: 22px; 346 | font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; 347 | font-weight: 100; 348 | } 349 | 350 | input.wide { 351 | width: 100%; 352 | box-sizing: border-box; 353 | } 354 | 355 | button, .button { 356 | background-color:#fff; 357 | border-radius:3px; 358 | border:1px solid #dadada; 359 | @include background(linear-gradient(#fff 0%, #f1f1f1 35%, #f1f1f1 65%, #fff 100%)); 360 | box-shadow: inset 0 0 3px #fff, 0px 1px 2px rgba(100, 100, 100, 0.1); 361 | font-weight:bold; 362 | padding:9px 18px; 363 | text-decoration:none; 364 | text-shadow:1px 1px 0px #ffffff; 365 | text-align: center; 366 | } 367 | 368 | p.error { 369 | color: red; 370 | } 371 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | before_filter :sign_in_required 3 | 4 | private 5 | 6 | def sign_in_required 7 | head 401 unless signed_in? 8 | end 9 | 10 | def current_user 11 | @current_user ||= current_user_from_challenge 12 | end 13 | 14 | def current_user_from_challenge 15 | if request.headers['X-Challenge'] 16 | challenge = RsaChallenge::Response.new(request.headers['X-Challenge']) 17 | User.find(challenge.user_id) if challenge.valid? 18 | end 19 | end 20 | 21 | def signed_in? 22 | !!current_user 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/auth/rsa_controller.rb: -------------------------------------------------------------------------------- 1 | class Auth::RsaController < ApplicationController 2 | skip_before_filter :sign_in_required 3 | 4 | def create 5 | challenge = RsaChallenge::Request.new(request.raw_post) 6 | render :text => challenge.value, :content_type => 'text/plain' 7 | end 8 | 9 | def update 10 | challenge = RsaChallenge::Response.new(request.raw_post) 11 | if challenge.valid? 12 | head 200 13 | else 14 | head 401 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /app/controllers/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class DashboardController < ApplicationController 2 | skip_before_filter :sign_in_required 3 | 4 | def index 5 | @user = UserPresenter.new(current_user) 6 | end 7 | 8 | def mockup 9 | render :layout => false 10 | end 11 | end -------------------------------------------------------------------------------- /app/controllers/items_controller.rb: -------------------------------------------------------------------------------- 1 | class ItemsController < ApplicationController 2 | def index 3 | render :json => ItemListPresenter.new(current_user) 4 | end 5 | 6 | def create 7 | item = Item.create!(item_params) 8 | share = item.share_with(current_user, params[:key]) 9 | render :json => ItemPresenter.new(item, share), :status => :created 10 | end 11 | 12 | def update 13 | item = Item.find(params[:id]) 14 | share = item.share_for(current_user) 15 | item.update_attributes(item_params) 16 | render :json => ItemPresenter.new(item, share) 17 | end 18 | 19 | private 20 | 21 | def item_params 22 | params.permit(:title, :encrypted_data) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/app/mailers/.gitkeep -------------------------------------------------------------------------------- /app/models/item.rb: -------------------------------------------------------------------------------- 1 | class Item < ActiveRecord::Base 2 | 3 | include ActiveModel::ForbiddenAttributesProtection 4 | 5 | has_many :shares 6 | 7 | self.include_root_in_json = false 8 | 9 | def share_with(user, key) 10 | Share.create! :item_id => id, :user_id => user.id, :key => key 11 | end 12 | 13 | def share_for(user) 14 | shares.where(:user_id => user.id).first! 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/rsa_challenge.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module RsaChallenge 4 | module Encryption 5 | def decrypt(value) 6 | crypt(:decrypt, value).split('--') 7 | rescue OpenSSL::Cipher::CipherError 8 | nil 9 | end 10 | 11 | def crypt(cipher_method, value) #:nodoc: 12 | cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc') 13 | cipher.send(cipher_method) 14 | cipher.pkcs5_keyivgen(ENV['SECRET_TOKEN']) 15 | result = cipher.update(value) 16 | result << cipher.final 17 | end 18 | end 19 | 20 | # Create an RSA challenge encrypted with the given public key. 21 | # 22 | # Inspired by SSH RSA authentication, this class will generate a challenge 23 | # encrypted with the given public key. If the client returns an unencrypted 24 | # version of this challenge, then they have the private key. 25 | # 26 | # The challenge includes the user ID that owns this key and a secret only 27 | # known by the server. The challenge is symetrically encrypted with a secret 28 | # key, and then encrypted with the public key. 29 | class Request 30 | def initialize(key) 31 | @public_key = OpenSSL::PKey::RSA.new(key) 32 | end 33 | 34 | def value 35 | Base64.encode64 @public_key.public_encrypt(encrypted_challenge_values) 36 | end 37 | 38 | def user 39 | @user ||= User.with_public_key(@public_key) 40 | end 41 | 42 | private 43 | 44 | include Encryption 45 | 46 | def challenge_values 47 | [user.id, ENV["AUTH_SECRET"], Time.now.to_f] 48 | end 49 | 50 | def encrypted_challenge_values 51 | crypt :encrypt, challenge_values.join('--') 52 | end 53 | end 54 | 55 | # Verifies the challenge from the client. 56 | # 57 | # If the provided value can be decrypted with the secret key, then the user 58 | # posesses the private key and it is a valid challenge response. 59 | class Response 60 | include Encryption 61 | 62 | attr_reader :user_id 63 | 64 | def initialize(value) 65 | @value = value 66 | end 67 | 68 | def valid? 69 | @user_id, secret, time = decrypt(Base64.decode64(@value)) 70 | secret == ENV["AUTH_SECRET"] 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /app/models/share.rb: -------------------------------------------------------------------------------- 1 | class Share < ActiveRecord::Base 2 | 3 | include ActiveModel::ForbiddenAttributesProtection 4 | 5 | belongs_to :item 6 | belongs_to :user 7 | 8 | self.include_root_in_json = false 9 | 10 | end 11 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | 3 | include ActiveModel::ForbiddenAttributesProtection 4 | 5 | has_many :shares 6 | 7 | self.include_root_in_json = false 8 | 9 | def self.with_public_key(key) 10 | where(:fingerprint => key.fingerprint).first || 11 | create!(:public_key => key.to_s, :fingerprint => key.fingerprint) 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /app/presenters/item_list_presenter.rb: -------------------------------------------------------------------------------- 1 | class ItemListPresenter 2 | def initialize(user) 3 | @user = user 4 | end 5 | 6 | def shares 7 | @shares ||= Share.where(:user_id => @user.id).index_by(&:item_id) 8 | end 9 | 10 | def items 11 | @items ||= Item.where(:id => shares.keys) 12 | end 13 | 14 | def as_json(options = nil) 15 | items.map {|item| ItemPresenter.new(item, shares[item.id]) }.as_json(options) 16 | end 17 | end -------------------------------------------------------------------------------- /app/presenters/item_presenter.rb: -------------------------------------------------------------------------------- 1 | class ItemPresenter 2 | def initialize(item, share) 3 | @item = item 4 | @share = share 5 | end 6 | 7 | def as_json(options = {}) 8 | { 9 | 'id' => @item.id.to_s, 10 | 'title' => @item.title, 11 | 'encrypted_data' => @item.encrypted_data, 12 | 'key' => @share.key 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/presenters/user_presenter.rb: -------------------------------------------------------------------------------- 1 | class UserPresenter 2 | def initialize(user) 3 | @user = user 4 | end 5 | 6 | def as_json(options = nil) 7 | { 8 | 'public_key' => @user.public_key 9 | } 10 | end 11 | end -------------------------------------------------------------------------------- /app/views/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do %> 2 | 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/dashboard/mockup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swordfish 5 | <%= stylesheet_link_tag :mockup %> 6 | <%= javascript_include_tag :mockup %> 7 | 8 | 9 |
10 | 16 |
17 |

18 | My Vaults 19 | + 20 |

21 | 27 | 28 |

29 | Shared Vaults 30 | + 31 |

32 | 38 | 44 | 50 |
51 |
52 | 174 |
175 | + 176 |
177 |
178 |
179 |
180 |
181 |

Speaker Deck

182 | http://speakerdeck.com/signin 183 |
184 |
185 |
186 | 187 |
188 |
189 |
Username
brandon@opensoul.org
190 |
Password
••••••••••
191 |
192 |
193 | 194 |
195 |

Access Log

196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 210 | 211 | 212 | 213 | 214 | 215 | 216 |
Shawn DavenportJuly 25, 9:14ami
204 |
205 |
IP
208.83.31.194
206 |
User Agent
Chrome 20.0.1132.57
207 |
Location
New Orleans, LA
208 |
209 |
Brandon KeepersJuly 23, 3:14pmInfo
217 |
218 |
219 | 223 |
224 |
225 | 226 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swordfish 6 | <%= stylesheet_link_tag :application, :media => "all" %> 7 | <%= javascript_include_tag :application, :ui %> 8 | <%= yield :head %> 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Swordfish::Application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "rails/all" 4 | 5 | if defined?(Bundler) 6 | # If you precompile assets before deploying to production, use this line 7 | Bundler.require(*Rails.groups(:assets => %w(development test))) 8 | # If you want your assets lazily compiled in production, use this line 9 | # Bundler.require(:default, :assets, Rails.env) 10 | end 11 | 12 | module Swordfish 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | config.autoload_paths += [ 20 | config.root.join('app', 'presenters') 21 | ] 22 | 23 | # Only load the plugins named here, in the order given (default is alphabetical). 24 | # :all can be used as a placeholder for all plugins not explicitly named. 25 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 26 | 27 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 28 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 29 | # config.time_zone = 'Central Time (US & Canada)' 30 | 31 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 32 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 33 | # config.i18n.default_locale = :de 34 | 35 | # Configure the default encoding used in templates for Ruby 1.9. 36 | config.encoding = "utf-8" 37 | 38 | # Configure sensitive parameters which will be filtered from the log file. 39 | config.filter_parameters += [:password, :passphrase] 40 | 41 | # Enable escaping HTML in JSON. 42 | config.active_support.escape_html_entities_in_json = true 43 | 44 | # Use SQL instead of Active Record's schema dumper when creating the database. 45 | # This is necessary if your schema can't be completely dumped by the schema dumper, 46 | # like if you have constraints or database-specific column types 47 | # config.active_record.schema_format = :sql 48 | 49 | # Enable the asset pipeline 50 | config.assets.enabled = true 51 | 52 | # Version of your assets, change this if you want to expire all your assets 53 | config.assets.version = '1.0' 54 | 55 | config.assets.initialize_on_precompile = false 56 | config.assets.precompile += %w( ui.js ) 57 | config.assets.paths << Rails.root.join("vendor", "assets", "swf") 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /config/cucumber.yml: -------------------------------------------------------------------------------- 1 | <% 2 | rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" 3 | rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" 4 | std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" 5 | %> 6 | default: <%= std_opts %> features 7 | wip: --tags @wip:3 --wip features 8 | rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip 9 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: 5 5 | host: localhost 6 | port: <%= ENV['GH_POSTGRESQL_PORT'] || 5432 %> 7 | username: <%= ENV['GH_POSTGRESQL_USER'] || ENV['USER'] %> 8 | min_messages: WARNING 9 | 10 | development: 11 | <<: *defaults 12 | database: swordfish_development 13 | 14 | test: 15 | <<: *defaults 16 | database: swordfish_test 17 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Swordfish::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Swordfish::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | config.assets.compress = false 27 | 28 | # Expands the lines which load the assets 29 | config.assets.debug = true 30 | end 31 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Swordfish::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Swordfish::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Print deprecation notices to the stderr 33 | config.active_support.deprecation = :stderr 34 | end 35 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Swordfish::Application.config.secret_token = ENV['SECRET_TOKEN'] 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Swordfish::Application.config.session_store :cookie_store, :key => '_passwords_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Swordfish::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters :format => [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Swordfish::Application.routes.draw do 2 | resources :items 3 | 4 | namespace :auth do 5 | resource :rsa, :only => [:create, :update], :controller => 'rsa' 6 | end 7 | 8 | match 'mockup', :to => 'dashboard#mockup' 9 | root :to => 'dashboard#index' 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20130120120320_create_items.rb: -------------------------------------------------------------------------------- 1 | class CreateItems < ActiveRecord::Migration 2 | def change 3 | create_table :items do |t| 4 | t.string :title 5 | t.string :encrypted_data 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20130120120408_create_shares.rb: -------------------------------------------------------------------------------- 1 | class CreateShares < ActiveRecord::Migration 2 | def change 3 | create_table :shares do |t| 4 | t.integer :item_id 5 | t.integer :user_id 6 | t.string :key 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20130120120503_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :public_key 5 | t.string :fingerprint 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20130212195016_change_public_key_to_text.rb: -------------------------------------------------------------------------------- 1 | class ChangePublicKeyToText < ActiveRecord::Migration 2 | def change 3 | change_column :users, :public_key, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20130212195016) do 15 | 16 | create_table "items", :force => true do |t| 17 | t.string "title" 18 | t.string "encrypted_data" 19 | t.datetime "created_at", :null => false 20 | t.datetime "updated_at", :null => false 21 | end 22 | 23 | create_table "shares", :force => true do |t| 24 | t.integer "item_id" 25 | t.integer "user_id" 26 | t.string "key" 27 | t.datetime "created_at", :null => false 28 | t.datetime "updated_at", :null => false 29 | end 30 | 31 | create_table "users", :force => true do |t| 32 | t.text "public_key" 33 | t.string "fingerprint" 34 | t.datetime "created_at", :null => false 35 | t.datetime "updated_at", :null => false 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Emanuel', :city => cities.first) 8 | -------------------------------------------------------------------------------- /docs/DataFormat.md: -------------------------------------------------------------------------------- 1 | # Data Format 2 | 3 | ## Items 4 | 5 | Items are persisted in a MongoDB collection called `items`, with insecure data stored in plain text, and secure data stored as an encrypted blob in the `data` attribute. 6 | 7 | { 8 | id: ObjectId("…"), 9 | title: "GitHub", 10 | data: "[JSON HASH, ENCRYPTED WITH RANDOM KEY]" 11 | } 12 | 13 | The data attribute is encrypted using a random key generated on the client. 14 | 15 | ## Shares 16 | 17 | When an item is created, the randomly generated item key is encrypted with the creator's public key and persisted in the `shares` collection. 18 | 19 | { 20 | item_id: ObjectId("…"), 21 | user_id: ObjectId("…"), 22 | key: "[RANDOM ITEM KEY ENCRYPTED WITH USER'S PUBLIC KEY]" 23 | } 24 | 25 | Sharing an item with another user is as simple as decrypting the *item key* with your own private key and re-incrypting it with their public key. -------------------------------------------------------------------------------- /docs/core.md: -------------------------------------------------------------------------------- 1 | # Core Team 2 | 3 | Swordfish is built and maintained by volunteers with a desire to increase security amongst teams without adding unnecessary overhead for individuals. 4 | 5 | ## Areas of Need 6 | 7 | There are several areas of expertise needed to make Swordfish successful. 8 | 9 | * **JavaScript** - Most of the front application is written in JavaScript (well, CoffeeScript), built around [Backbone.js](http://backbonejs.org) and uses many other libraries. 10 | * **Ruby** - The API is a relatively small Ruby on Rails application, tested with RSpec and Cucumber, with data persisted in MongoDB. 11 | * **Design** 12 | * **Cryptography** - The application uses AES to encrypt items, and then encrypts the item key with an RSA key for each user. 13 | * **Security** - Strong cryptography is useless if there are simple exploits in other parts of the application. 14 | 15 | ## Responsibilities 16 | 17 | * Review, discuss and merge [pull requests](https://github.com/github/swordfish/pulls) 18 | * Offer feedback on [Issues](https://github.com/github/swordfish/issues) and [mailing list discussions](https://groups.google.com/group/swordfishapp) 19 | * Be a decent human being 20 | * Give a damn 21 | 22 | ## Joining the team 23 | 24 | Send a 2,000 word essay on why you think you would be a good fit for the core team, along with a portrait and profile photograph to…just kidding. 25 | 26 | Just get involved. Send a pull request, comment on an issue, or participate in a discussion. If you are still interested in joining the core team after you've been involved for a few weeks, then get in touch with [Brandon Keepers](http://github.com/bkeepers). 27 | 28 | ## Members 29 | 30 | * [Brandon Keepers](http://github.com/bkeepers) 31 | -------------------------------------------------------------------------------- /lib/tasks/brakeman.rake: -------------------------------------------------------------------------------- 1 | namespace :brakeman do 2 | 3 | desc "Run Brakeman" 4 | task :run, :output_files do |t, args| 5 | require 'brakeman' 6 | 7 | files = args[:output_files].split(' ') if args[:output_files] 8 | Brakeman.run( 9 | :app_path => ".", 10 | :output_files => files, 11 | :print_report => true, 12 | :skip_checks => ['CheckForgerySetting'], 13 | :exit_on_warn => true 14 | ) 15 | end 16 | end 17 | 18 | task :default => 'brakeman:run' 19 | -------------------------------------------------------------------------------- /lib/tasks/jasmine.rake: -------------------------------------------------------------------------------- 1 | begin 2 | require 'guard/jasmine/task' 3 | Guard::JasmineTask.new 4 | rescue LoadError 5 | end -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/log/.gitkeep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Make sure all our deps exist. 3 | 4 | # Export CC to explicitly set the compiler used for cexts. 5 | 6 | export CC=cc 7 | 8 | # Generate .bundle/config instead of using env vars or command line 9 | # flags so that we have consistent configuration for our `bundle 10 | # check` and `bundle install` calls here, as well as any manual calls 11 | # to `bundle` that people might make. 12 | 13 | # We don't want old config hanging around right now. This is sorta 14 | # jank: We can do better. 15 | 16 | rm -rf .bundle 17 | mkdir .bundle 18 | 19 | echo "--- 20 | BUNDLE_BIN: bin 21 | BUNDLE_DISABLE_SHARED_GEMS: "1" 22 | BUNDLE_PATH: vendor/gems 23 | BUNDLE_WITHOUT: staging:production 24 | " > .bundle/config 25 | 26 | # Bundle install unless we're already up to date. 27 | 28 | bundle check > /dev/null 2>&1 || bundle install "$@" 29 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/acceptance/item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Items' do 4 | before(:each) do 5 | generate_key 6 | visit root_path 7 | unlock_key 8 | end 9 | 10 | scenario 'creating and editing an item' do 11 | create_item( 12 | "Title" => "example.com", 13 | "Username" => "myusername", 14 | "Password" => "mypassword", 15 | "Confirm" => "mypassword" 16 | ) 17 | 18 | within('#items') { expect(page).to have_content('example.com') } 19 | 20 | visit root_path 21 | unlock_key 22 | expect(page).to have_content("example.com") 23 | click_link "example.com" 24 | expect(page).to have_content("myusername") 25 | 26 | click_link "reveal" 27 | expect(page).to have_content("mypassword") 28 | click_link "hide" 29 | expect(page).to have_no_content("mypassword") 30 | 31 | click_link "Edit" 32 | expect(find_field("Title").value).to eql("example.com") 33 | expect(find_field("Username").value).to eql("myusername") 34 | expect(find_field("Password").value).to eql("mypassword") 35 | expect(find_field("Confirm").value).to eql("mypassword") 36 | 37 | fill_in "Title", :with => "Example" 38 | fill_in "Username", :with => "updated-username" 39 | click_button "Save" 40 | 41 | within('#items') { expect(page).to have_content('Example') } 42 | expect(page).to have_content("updated-username") 43 | end 44 | 45 | scenario 'mismatching password confirmation' do 46 | click_link "+" 47 | fill_in "Title", :with => "example.com" 48 | fill_in "Username", :with => "myusername" 49 | fill_in "Password", :with => "mypassword" 50 | click_button "Create" 51 | expect(page).to have_no_content('example.com') 52 | 53 | fill_in "Password", :with => "" 54 | fill_in "Confirm", :with => "mypassword" 55 | click_button "Create" 56 | expect(page).to have_no_content('example.com') 57 | end 58 | 59 | scenario 'requires matching passwords on update' do 60 | create_item 61 | 62 | click_link 'Edit' 63 | fill_in "Password", :with => "newpassword" 64 | click_button "Save" 65 | expect(page).to have_content('Save') 66 | 67 | fill_in "Confirm", :with => "newpassword" 68 | click_button "Save" 69 | click_link "reveal" 70 | expect(page).to have_content("newpassword") 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/acceptance/key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Key Management' do 4 | scenario 'Create key to sign up' do 5 | visit root_path 6 | 7 | expect(page).to have_content('Create a Key') 8 | 9 | fill_in 'passphrase', :with => 'this is my secret' 10 | click_button 'Generate Key' 11 | 12 | wait_forever do 13 | expect(page).to have_content('Download Private Key') 14 | end 15 | 16 | click_link 'Done' 17 | expect(page).to be_unlocked 18 | end 19 | 20 | scenario 'Upload key to sign up' do 21 | visit root_path 22 | expect(page).to have_content('Load existing key') 23 | 24 | click_link 'Load existing key' 25 | attach_file 'key', 'spec/fixtures/priv.pem' 26 | fill_in 'passphrase', :with => 'testing' 27 | click_button 'Unlock' 28 | expect(page).to be_unlocked 29 | 30 | # Ensure item creation works with loaded key 31 | create_item 'Title' => 'Loaded Key' 32 | expect(page).to have_content('Loaded Key') 33 | end 34 | 35 | scenario 'Unlocking key' do 36 | generate_key 37 | visit root_path 38 | unlock_key 39 | expect(page).to be_unlocked 40 | end 41 | 42 | scenario 'Failing to unlock key' do 43 | generate_key 44 | visit root_path 45 | 46 | unlock_key 'wrong passphrase' 47 | expect(page).to have_content('Your passphrase was incorrect!') 48 | 49 | unlock_key 50 | expect(page).to be_unlocked 51 | end 52 | 53 | scenario 'Locking key' do 54 | generate_key 55 | visit root_path 56 | unlock_key 57 | 58 | click_link 'Lock' 59 | 60 | expect(page).to have_content('Unlock') 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/controllers/auth/rsa_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Auth::RsaController do 4 | let(:public_key) { fixture('pub.pem') } 5 | 6 | describe 'create' do 7 | subject do 8 | raw_post :create, public_key 9 | end 10 | 11 | it 'responds with 200' do 12 | expect(subject).to be_success 13 | end 14 | 15 | it 'responds with text/plain content type' do 16 | expect(subject.content_type).to eq('text/plain') 17 | end 18 | 19 | it 'returns a challenge' do 20 | challenge = mock(:value => "foo") 21 | RsaChallenge::Request.should_receive(:new).and_return(challenge) 22 | expect(subject.body).to eql("foo") 23 | end 24 | end 25 | 26 | describe 'update' do 27 | let(:user) { User.create!(:public_key => public_key) } 28 | 29 | context 'with a valid response' do 30 | before do 31 | challenge = mock(:valid? => true, :user_id => user.id) 32 | RsaChallenge::Response.stub(:new).and_return(challenge) 33 | end 34 | 35 | subject do 36 | raw_post :update, 'valid' 37 | end 38 | 39 | it 'returns 200' do 40 | expect(subject).to be_success 41 | end 42 | end 43 | 44 | context 'with an invalid response' do 45 | before do 46 | challenge = mock(:valid? => false) 47 | RsaChallenge::Response.stub(:new).and_return(challenge) 48 | end 49 | 50 | subject do 51 | raw_post :update, 'invalid' 52 | end 53 | 54 | it 'returns 401' do 55 | expect(subject.status).to eq(401) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/controllers/items_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ItemsController do 4 | context 'when signed in' do 5 | before { sign_in_as mock_user } 6 | 7 | describe 'create' do 8 | subject do 9 | post :create, 10 | :title => 'example.com', 11 | :data => 'encrypted', 12 | :key => 'userkey' 13 | end 14 | 15 | it { expect(subject.status).to be(201) } 16 | 17 | it 'creates an item' do 18 | subject 19 | expect(Item.where(:title => 'example.com' ).first).to be_instance_of(Item) 20 | end 21 | end 22 | 23 | describe 'index' do 24 | subject do 25 | get :index 26 | end 27 | 28 | it { expect(subject.status).to be(200) } 29 | end 30 | 31 | describe 'update' do 32 | let(:item) { Item.create! } 33 | 34 | subject do 35 | put :update, :id => item.id.to_s, :title => 'Updated' 36 | end 37 | 38 | context 'when user has access' do 39 | before do 40 | item.share_with current_user, 'key' 41 | end 42 | 43 | it { expect(subject.status).to be(200) } 44 | 45 | it 'updates item' do 46 | subject 47 | item.reload 48 | expect(item.title).to eql('Updated') 49 | end 50 | end 51 | 52 | context 'when user does not have access' do 53 | it { expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) } 54 | end 55 | end 56 | end 57 | 58 | context 'when signed out' do 59 | it { expect(post(:create, :format => :json).status).to be(401) } 60 | end 61 | end -------------------------------------------------------------------------------- /spec/fixtures/priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIcr+AY81p67ACAggA 3 | MB0GCWCGSAFlAwQBAgQQ+MZ40SX/K9W8nxetEKPfHQSCAoBSGqUKsTPXNX/qpAoD 4 | cn9DvWZcCUQpWiLCtuaYp8trh51/OG8dj2+Mwq/xFFH+qlDM/QZZ2lEUEHSiDDOi 5 | ojBxtGiIDEKsHKDt9RKXij1FhVfW8TLaTYNoIJS1CnU3L+MG0sEAMyiReKJgPaBO 6 | uDYZ5aSfscPQ2BElzjOAEciWCF7Tc+8rn32pf4yGL2hximTxBGSVDiQdefKFH32C 7 | sujw5yzkHBStU5J4nlMYax7+yG48SecpP+EaR2n3kVWfM6Y1y85RLsVX4Yw91d5c 8 | 02q6/TUggAwHbAuBBC/3u6+S+sTXRqx+oXkRdw/0sGOAEUViDzwPBkiK01Tk44Uy 9 | VsyHHG4u/Hk0/lLspv++Ee9BL4+KIWjZImvnJ7t6f3UnwdA9nLp5wfrdf13F+ENW 10 | 1C3Gv+stu6SPJkpmo15BrXkM5gJtFrDn35kqGmvT+YA0TfRG8FI46LbwjbUDJhQ9 11 | S/g9Ks0Ps4MoyZpzi+3QD7DyxcnZ1wRGsPtTgIK4XKqQDTxvboe7/6d/D7TLRLcS 12 | 99GuC7QfcfH6O0yH4C1NJ/6qtphY8yLNsodg61qNd1CL/tgpEzDWaDWngMg8To0h 13 | 4elzhixvU9baXvTuQFvyCLlkh5AGCOJgqZChWkR64G+izvodUfjEggDpKfjEcNKy 14 | wITPbTHg0x6ibl72XGXiCU8+D4baxvThniwdMJAEWqmB3+2cvlU6dHkQzRxZHBEM 15 | dXzdv9xtiQfkuiMEyaYS7+72/GQ2ghSdZBUxB0qqq5ZbCz4AGLc/nGeP+DH6q57u 16 | ws8SDVgntQ9CB3mFaCyQQbRPhxcq1mNEnuJkDrZfrL8S7Nmi7anWPccTnZJ+97Qr 17 | yu9t 18 | -----END ENCRYPTED PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /spec/fixtures/pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCAXY6jq67YAXSzlZtpwO0xe3/k 3 | ZAEfW7roIdCQEDWGneUdnwer+EfkTA/7wQnZxq993x7DzhaWHeUnanFFkjFn+mDb 4 | lOTowPaLWl9CYo7JLCRY3hSgT9qci2VcBCqIsP7JJAdS/PGmKMNDZufnblqlfzfU 5 | 2shYSEmynVDgiDQITQIDAQAB 6 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /spec/javascripts/fixtures/priv.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIcr+AY81p67ACAggA 3 | MB0GCWCGSAFlAwQBAgQQ+MZ40SX/K9W8nxetEKPfHQSCAoBSGqUKsTPXNX/qpAoD 4 | cn9DvWZcCUQpWiLCtuaYp8trh51/OG8dj2+Mwq/xFFH+qlDM/QZZ2lEUEHSiDDOi 5 | ojBxtGiIDEKsHKDt9RKXij1FhVfW8TLaTYNoIJS1CnU3L+MG0sEAMyiReKJgPaBO 6 | uDYZ5aSfscPQ2BElzjOAEciWCF7Tc+8rn32pf4yGL2hximTxBGSVDiQdefKFH32C 7 | sujw5yzkHBStU5J4nlMYax7+yG48SecpP+EaR2n3kVWfM6Y1y85RLsVX4Yw91d5c 8 | 02q6/TUggAwHbAuBBC/3u6+S+sTXRqx+oXkRdw/0sGOAEUViDzwPBkiK01Tk44Uy 9 | VsyHHG4u/Hk0/lLspv++Ee9BL4+KIWjZImvnJ7t6f3UnwdA9nLp5wfrdf13F+ENW 10 | 1C3Gv+stu6SPJkpmo15BrXkM5gJtFrDn35kqGmvT+YA0TfRG8FI46LbwjbUDJhQ9 11 | S/g9Ks0Ps4MoyZpzi+3QD7DyxcnZ1wRGsPtTgIK4XKqQDTxvboe7/6d/D7TLRLcS 12 | 99GuC7QfcfH6O0yH4C1NJ/6qtphY8yLNsodg61qNd1CL/tgpEzDWaDWngMg8To0h 13 | 4elzhixvU9baXvTuQFvyCLlkh5AGCOJgqZChWkR64G+izvodUfjEggDpKfjEcNKy 14 | wITPbTHg0x6ibl72XGXiCU8+D4baxvThniwdMJAEWqmB3+2cvlU6dHkQzRxZHBEM 15 | dXzdv9xtiQfkuiMEyaYS7+72/GQ2ghSdZBUxB0qqq5ZbCz4AGLc/nGeP+DH6q57u 16 | ws8SDVgntQ9CB3mFaCyQQbRPhxcq1mNEnuJkDrZfrL8S7Nmi7anWPccTnZJ+97Qr 17 | yu9t 18 | -----END ENCRYPTED PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /spec/javascripts/fixtures/pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCAXY6jq67YAXSzlZtpwO0xe3/k 3 | ZAEfW7roIdCQEDWGneUdnwer+EfkTA/7wQnZxq993x7DzhaWHeUnanFFkjFn+mDb 4 | lOTowPaLWl9CYo7JLCRY3hSgT9qci2VcBCqIsP7JJAdS/PGmKMNDZufnblqlfzfU 5 | 2shYSEmynVDgiDQITQIDAQAB 6 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /spec/javascripts/models/item_key_spec.coffee: -------------------------------------------------------------------------------- 1 | #= require forge 2 | #= require models/item_key 3 | 4 | describe 'ItemKey', -> 5 | beforeEach -> 6 | @key = new ItemKey(ItemKey.generate()) 7 | 8 | describe 'encrypt/decrypt', -> 9 | it 'encrypts and decrypts the data', -> 10 | expect(@key.decrypt(@key.encrypt('topsecret'))).toEqual('topsecret') 11 | -------------------------------------------------------------------------------- /spec/javascripts/models/item_spec.coffee: -------------------------------------------------------------------------------- 1 | #= require models 2 | 3 | describe 'Item', -> 4 | beforeEach -> 5 | @keypair = 6 | encrypt: jasmine.createSpy('encrypt').andReturn('encrypted') 7 | decrypt: jasmine.createSpy('decrypt').andReturn('decrypted') 8 | 9 | @collection = {keypair: @keypair, url:'/items'} 10 | spyOn(jQuery, 'ajax') 11 | 12 | describe 'initialize', -> 13 | it 'generates a key', -> 14 | spyOn(ItemKey, 'generate').andReturn('generated key') 15 | 16 | item = new Item({}, collection: @collection) 17 | expect(item.get('key')).toEqual('encrypted') 18 | expect(ItemKey.generate).toHaveBeenCalled() 19 | expect(@keypair.encrypt).toHaveBeenCalledWith('generated key') 20 | 21 | it 'does not override existing key', -> 22 | item = new Item({key: 'existing key'}, collection: @collection) 23 | expect(item.get('key')).toEqual('existing key') 24 | 25 | describe 'set', -> 26 | beforeEach -> 27 | spyOn(ItemKey.prototype, 'encrypt').andReturn('encrypted-data') 28 | @item = new Item({}, {collection: @collection}) 29 | @item.set(data: {foo: 'bar'}) 30 | 31 | it 'encrypts data', -> 32 | expect(@item.get('encrypted_data')).toEqual('encrypted-data') 33 | 34 | it 'clears unencrypted data', -> 35 | expect(@item.get('data')).toBe(undefined) 36 | expect(@item.toJSON().data).toBe(undefined) 37 | 38 | -------------------------------------------------------------------------------- /spec/javascripts/models/keypair_authenticator_spec.coffee: -------------------------------------------------------------------------------- 1 | #= require jquery 2 | #= require underscore 3 | #= require forge 4 | #= require models/keypair 5 | #= require models/keypair_authenticator 6 | 7 | describe 'KeypairAuthenticator', -> 8 | beforeEach -> 9 | @deferred = jQuery.Deferred() 10 | KeypairAuthenticator.ajax = @ajax = jasmine.createSpy('ajax').andReturn(@deferred) 11 | 12 | @keypair = new Keypair("key") 13 | spyOn(@keypair, 'publicKeyPem').andReturn('public key') 14 | 15 | @authenticator = new KeypairAuthenticator(@keypair) 16 | spyOn(@authenticator, 'decryptChallenge').andReturn('decrypted challenge') 17 | 18 | describe 'request', -> 19 | it 'gets challenge from the server', -> 20 | @authenticator.request() 21 | 22 | request = @ajax.mostRecentCall.args[0] 23 | 24 | expect(request.url).toEqual('/auth/rsa') 25 | expect(request.type).toEqual('POST') 26 | expect(request.data).toEqual('public key') 27 | 28 | it 'returns authenticator', -> 29 | expect(@authenticator.request()).toBe(@authenticator) 30 | 31 | describe 'respond', -> 32 | it 'sends decrypted challenge response to server', -> 33 | @authenticator.request() 34 | @deferred.resolve('encrypted data') 35 | 36 | request = @ajax.mostRecentCall.args[0] 37 | 38 | expect(request.url).toEqual('/auth/rsa') 39 | expect(request.type).toEqual('PUT') 40 | expect(request.data).toEqual('decrypted challenge') 41 | 42 | it 'resolves with successful challenge', -> 43 | callback = jasmine.createSpy('done callback') 44 | @authenticator.done callback 45 | @authenticator.request() 46 | @deferred.resolve() 47 | 48 | expect(callback).toHaveBeenCalledWith('decrypted challenge') 49 | -------------------------------------------------------------------------------- /spec/javascripts/models/keypair_spec.coffee: -------------------------------------------------------------------------------- 1 | #= require jquery 2 | #= require forge 3 | #= require models/keypair 4 | 5 | describe 'Keypair', -> 6 | beforeEach -> 7 | Keypair.ajax = @ajax = jasmine.createSpy('ajax') 8 | Keypair.localStorage = @local = {} 9 | 10 | @privateKey = readFixtures('priv.pem.txt') 11 | @keypair = new Keypair(@privateKey) 12 | 13 | describe 'savePrivateKey', -> 14 | it 'saves private key to local storage', -> 15 | @keypair.savePrivateKey() 16 | expect(@local['privateKey']).toEqual(@privateKey) 17 | 18 | describe 'load', -> 19 | describe 'when keypair is not in local storage', -> 20 | it 'returns falsy', -> 21 | expect(Keypair.load()).not.toBeTruthy() 22 | 23 | describe 'when private key is set', -> 24 | beforeEach -> @local['privateKey'] = @privateKey 25 | 26 | it 'returns keypair with public/private key', -> 27 | keypair = Keypair.load(@privateKey) 28 | expect(keypair).toBeTruthy() 29 | expect(keypair.privateKeyPem).toBe(@privateKey) 30 | 31 | describe 'unlock', -> 32 | describe 'with the correct password', -> 33 | it 'returns true', -> 34 | expect(@keypair.unlock('testing')).toBe(true) 35 | 36 | it 'sets public key', -> 37 | @keypair.unlock('testing') 38 | expect(@keypair.publicKey).toBeTruthy() 39 | expect(@keypair.publicKey.encrypt).toBeTruthy() 40 | 41 | describe 'with an incorrect password', -> 42 | it 'returns false', -> 43 | expect(@keypair.unlock('wrong password')).toBe(false) 44 | -------------------------------------------------------------------------------- /spec/javascripts/routers/item_router_spec.coffee: -------------------------------------------------------------------------------- 1 | #= require forge 2 | #= require application 3 | #= require models/keypair 4 | #= require routers/item_router 5 | 6 | describe 'ItemRouter', -> 7 | describe 'items', -> 8 | it 'should set the keypair on the collection if it does not exist', -> 9 | app = Application 10 | key1 = new Keypair("private1") 11 | app.keypair = key1 12 | router = new ItemRouter(app: app) 13 | router.items() 14 | expect(router.itemsCollection.keypair).toEqual(key1) 15 | 16 | it 'should set the keypair on the collection to the new key if it does exist', -> 17 | app = Application 18 | key1 = new Keypair("private1") 19 | key2 = new Keypair("private2") 20 | app.keypair = key1 21 | router = new ItemRouter(app: app) 22 | router.items() 23 | # Key1 should be set properly on the collection 24 | expect(router.itemsCollection.keypair).toEqual(key1) 25 | # Simulate the user changing keypairs 26 | app.keypair = key2 27 | router.items() 28 | expect(router.itemsCollection.keypair).toEqual(key2) 29 | -------------------------------------------------------------------------------- /spec/javascripts/spec.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/spec/javascripts/spec.css -------------------------------------------------------------------------------- /spec/javascripts/spec.js.coffee: -------------------------------------------------------------------------------- 1 | #= require application 2 | #= require_tree ./ -------------------------------------------------------------------------------- /spec/javascripts/views/item/create_spec.coffee: -------------------------------------------------------------------------------- 1 | #= require application 2 | #= require ui 3 | 4 | describe 'Item.Views.Create', -> 5 | beforeEach -> 6 | @collection = {create: jasmine.createSpy('create')} 7 | @view = new Item.Views.Create(collection: @collection) 8 | @event = {preventDefault: jasmine.createSpy('preventDefault')} 9 | 10 | describe 'submit', -> 11 | it 'creates an item', -> 12 | @view.submit(@event) 13 | expect(@collection.create).toHaveBeenCalled() 14 | 15 | it 'cancels the normal form submission', -> 16 | @view.submit(@event) 17 | expect(@event.preventDefault).toHaveBeenCalled() 18 | 19 | it 'does not create an item if the passwords do not match', -> 20 | @view.passwordConfirmed = -> false 21 | @view.submit(@event) 22 | expect(@collection.create).not.toHaveBeenCalled() 23 | 24 | it 'cancels the normal form submission if the passwords do not match', -> 25 | @view.passwordConfirmed = -> false 26 | @view.submit(@event) 27 | expect(@event.preventDefault).toHaveBeenCalled() 28 | -------------------------------------------------------------------------------- /spec/models/item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Item do 4 | let(:item) { Item.new } 5 | let(:user) { User.new } 6 | 7 | describe 'share_with' do 8 | it 'creates a share' do 9 | share = item.share_with(user, 'mykey') 10 | expect(share).to be_instance_of(Share) 11 | expect(share).not_to be_new_record 12 | expect(share.key).to eql('mykey') 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /spec/models/rsa_challenge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RsaChallenge do 4 | let(:public_key) { fixture('pub.pem') } 5 | 6 | describe RsaChallenge::Request do 7 | subject do 8 | RsaChallenge::Request.new(public_key) 9 | end 10 | 11 | it 'finds user' do 12 | user = mock(:user) 13 | User.should_receive(:with_public_key).and_return(user) 14 | expect(subject.user).to eql(user) 15 | end 16 | end 17 | 18 | describe RsaChallenge::Response do 19 | let(:user) { double(:user, :id => 42) } 20 | let(:challenge) { RsaChallenge::Request.new(public_key) } 21 | 22 | before do 23 | User.stub!(:with_public_key).and_return(user) 24 | end 25 | 26 | describe 'valid?' do 27 | context 'with a decrypted challenge' do 28 | before do 29 | priv = OpenSSL::PKey::RSA.new(fixture('priv.pem'), 'testing') 30 | value = Base64.decode64(challenge.value) 31 | @decrypted = Base64.encode64(priv.private_decrypt(value)) 32 | end 33 | 34 | subject do 35 | RsaChallenge::Response.new(@decrypted) 36 | end 37 | 38 | it 'returns true' do 39 | expect(subject.valid?).to be_true 40 | end 41 | 42 | it 'sets user_id' do 43 | expect(subject.user_id).to be_nil 44 | subject.valid? 45 | expect(subject.user_id).to eql(user.id.to_s) 46 | end 47 | end 48 | 49 | context 'with an invalid challenge' do 50 | it 'returns false' do 51 | response = RsaChallenge::Response.new(challenge.value) 52 | expect(response.valid?).to be_false 53 | end 54 | end 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | describe '.with_public_key' do 5 | context 'when the user does not exist' do 6 | it 'sets fingerprint before saving' do 7 | key = double(:key, :fingerprint => 'fingerprint', :to_s => 'key') 8 | user = User.with_public_key(key) 9 | expect(user.fingerprint).to be_present 10 | end 11 | end 12 | end 13 | 14 | end -------------------------------------------------------------------------------- /spec/presenters/item_list_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ItemListPresenter do 4 | let(:user) { mock_user } 5 | subject { ItemListPresenter.new(user) } 6 | 7 | describe 'items' do 8 | let(:item) { Item.create! } 9 | 10 | it 'includes items shared with user' do 11 | item.share_with(user, 'key') 12 | expect(subject.items).to include(item) 13 | end 14 | 15 | it 'does not include items not shared' do 16 | expect(subject.items).not_to include(item) 17 | end 18 | end 19 | 20 | describe 'as_json' do 21 | let(:item) { Item.create!(:title => 'example.com') } 22 | 23 | before do 24 | item.share_with(user, 'thekey') 25 | end 26 | 27 | it 'includes item attributes' do 28 | item = subject.as_json.first 29 | expect(item['title']).to eql('example.com') 30 | end 31 | 32 | it 'includes key' do 33 | expect(subject.as_json.first['key']).to eql('thekey') 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /spec/presenters/item_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ItemPresenter do 4 | let(:item) { double(:item, :id => BSON::ObjectId.new).as_null_object } 5 | let(:presenter) { ItemPresenter.new(item) } 6 | end -------------------------------------------------------------------------------- /spec/presenters/user_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UserPresenter do 4 | let(:user) { mock_user } 5 | subject { UserPresenter.new(user) } 6 | 7 | describe 'to_json' do 8 | it 'includes public_key' do 9 | expect(subject.as_json).to have_key('public_key') 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | require 'rspec/autorun' 6 | 7 | # Requires supporting ruby files with custom matchers and macros, etc, 8 | # in spec/support/ and its subdirectories. 9 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 10 | 11 | RSpec.configure do |config| 12 | # If true, the base class of anonymous controllers will be inferred 13 | # automatically. This will be the default behavior in future versions of 14 | # rspec-rails. 15 | config.infer_base_class_for_anonymous_controllers = false 16 | 17 | # Run specs in random order to surface order dependencies. If you find an 18 | # order dependency and want to debug it, you can fix the order by providing 19 | # the seed, which is printed after each run. 20 | # --seed 1234 21 | config.order = "random" 22 | 23 | config.expect_with(:rspec) { |c| c.syntax = :expect } 24 | 25 | config.use_transactional_fixtures = false 26 | 27 | config.before do 28 | DatabaseCleaner.strategy = :truncation 29 | end 30 | 31 | config.before do 32 | DatabaseCleaner.start 33 | end 34 | 35 | config.after do 36 | DatabaseCleaner.clean 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/support/acceptance.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/rspec' 2 | require 'capybara/poltergeist' 3 | 4 | Capybara.default_driver = ENV['SELENIUM'] ? :selenium : :poltergeist 5 | Capybara.register_driver :selenium do |app| 6 | Capybara::Selenium::Driver.new(app, :browser => :chrome) 7 | end 8 | 9 | module AcceptanceSpecHelpers 10 | def generate_key 11 | User.create!(:public_key => fixture('pub.pem')) 12 | visit '/' 13 | page.execute_script "localStorage['privateKey'] = #{fixture('priv.pem').to_json}" 14 | end 15 | 16 | def unlock_key(passphrase = 'testing') 17 | fill_in 'passphrase', :with => passphrase 18 | click_button 'Unlock' 19 | end 20 | 21 | def create_item(attrs = {}) 22 | click_link '+' 23 | item_attrs(attrs).each do |key,value| 24 | fill_in key, :with => value 25 | end 26 | click_button 'Create' 27 | end 28 | 29 | def wait_forever(&block) 30 | wait_time = Capybara.default_wait_time 31 | Capybara.default_wait_time = 60 32 | block.call 33 | ensure 34 | Capybara.default_wait_time = wait_time 35 | end 36 | 37 | ### ATTRIBUTES 38 | 39 | def item_attrs(attrs = {}) 40 | { 41 | 'Title' => 'title', 42 | 'URL' => 'http://example.com', 43 | 'Username' => 'username', 44 | 'Password' => 'password', 45 | 'Confirm' => 'password' 46 | }.merge(attrs) 47 | end 48 | 49 | ### MATCHERS 50 | 51 | def be_unlocked 52 | have_content('+') # yeah, yeah, this sucks 53 | end 54 | end 55 | 56 | RSpec.configure do |config| 57 | config.include AcceptanceSpecHelpers, :capybara_feature => true 58 | config.after :each, :capybara_feature => true do 59 | page.execute_script 'localStorage.clear()' 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/support/controller_spec_helpers.rb: -------------------------------------------------------------------------------- 1 | module ControllerSpecHelpers 2 | attr_reader :current_user 3 | 4 | def sign_in_as(user) 5 | controller.stub(:current_user).and_return(user) 6 | @current_user = user 7 | end 8 | 9 | def raw_post(action, body) 10 | request.env['RAW_POST_DATA'] = body 11 | post action 12 | end 13 | 14 | end 15 | 16 | RSpec.configure do |config| 17 | config.include ControllerSpecHelpers, :type => 'controller' 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/fixtures.rb: -------------------------------------------------------------------------------- 1 | module FixtureSpecHelper 2 | def fixture(name) 3 | Rails.root.join('spec', 'fixtures', name).read 4 | end 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include FixtureSpecHelper 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/mocks.rb: -------------------------------------------------------------------------------- 1 | module Mocks 2 | def mock_user(attrs = {}) 3 | double(:user, user_attrs(attrs)).as_null_object 4 | end 5 | 6 | def user_attrs(attrs = {}) 7 | { 8 | :id => 1, 9 | :public_key => double(:public_key) 10 | }.merge(attrs) 11 | end 12 | end 13 | 14 | RSpec.configure do |config| 15 | config.include Mocks 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/webmock.rb: -------------------------------------------------------------------------------- 1 | require 'webmock/rspec' 2 | 3 | WebMock.disable_net_connect!(:allow_localhost => true) 4 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge.coffee: -------------------------------------------------------------------------------- 1 | #= require forge/util 2 | #= require forge/asn1 3 | #= require forge/sha1 4 | #= require forge/rsa 5 | #= require_tree ./forge -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/hmac.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hash-based Message Authentication Code implementation. Requires a message 3 | * digest object that can be obtained, for example, from forge.md.sha1 or 4 | * forge.md.md5. 5 | * 6 | * @author Dave Longley 7 | * 8 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. All rights reserved. 9 | */ 10 | (function() { 11 | 12 | // define forge 13 | if(typeof(window) !== 'undefined') { 14 | var forge = window.forge = window.forge || {}; 15 | forge.hmac = {}; 16 | } 17 | // define node.js module 18 | else if(typeof(module) !== 'undefined' && module.exports) { 19 | var forge = { 20 | md: require('./md'), 21 | util: require('./util') 22 | }; 23 | module.exports = forge.hmac = {}; 24 | } 25 | 26 | /* HMAC API */ 27 | var hmac = forge.hmac; 28 | 29 | /** 30 | * Creates an HMAC object that uses the given message digest object. 31 | * 32 | * @return an HMAC object. 33 | */ 34 | hmac.create = function() { 35 | // the hmac key to use 36 | var _key = null; 37 | 38 | // the message digest to use 39 | var _md = null; 40 | 41 | // the inner padding 42 | var _ipadding = null; 43 | 44 | // the outer padding 45 | var _opadding = null; 46 | 47 | // hmac context 48 | var ctx = {}; 49 | 50 | /** 51 | * Starts or restarts the HMAC with the given key and message digest. 52 | * 53 | * @param md the message digest to use, null to reuse the previous one, 54 | * a string to use builtin 'sha1', 'md5', 'sha256'. 55 | * @param key the key to use as a string, array of bytes, byte buffer, 56 | * or null to reuse the previous key. 57 | */ 58 | ctx.start = function(md, key) { 59 | if(md !== null) { 60 | if(md.constructor == String) { 61 | // create builtin message digest 62 | md = md.toLowerCase(); 63 | if(md in forge.md.algorithms) { 64 | _md = forge.md.algorithms[md].create(); 65 | } 66 | else { 67 | throw 'Unknown hash algorithm "' + md + '"'; 68 | } 69 | } 70 | else { 71 | // store message digest 72 | _md = md; 73 | } 74 | } 75 | 76 | if(key === null) { 77 | // reuse previous key 78 | key = _key; 79 | } 80 | else { 81 | // convert string into byte buffer 82 | if(key.constructor == String) { 83 | key = forge.util.createBuffer(key); 84 | } 85 | // convert byte array into byte buffer 86 | else if(key.constructor == Array) { 87 | var tmp = key; 88 | key = forge.util.createBuffer(); 89 | for(var i = 0; i < tmp.length; ++i) { 90 | key.putByte(tmp[i]); 91 | } 92 | } 93 | 94 | // if key is longer than blocksize, hash it 95 | var keylen = key.length(); 96 | if(keylen > _md.blockLength) { 97 | _md.start(); 98 | _md.update(key.bytes()); 99 | key = _md.digest(); 100 | } 101 | 102 | // mix key into inner and outer padding 103 | // ipadding = [0x36 * blocksize] ^ key 104 | // opadding = [0x5C * blocksize] ^ key 105 | _ipadding = forge.util.createBuffer(); 106 | _opadding = forge.util.createBuffer(); 107 | keylen = key.length(); 108 | for(var i = 0; i < keylen; ++i) { 109 | var tmp = key.at(i); 110 | _ipadding.putByte(0x36 ^ tmp); 111 | _opadding.putByte(0x5C ^ tmp); 112 | } 113 | 114 | // if key is shorter than blocksize, add additional padding 115 | if(keylen < _md.blockLength) { 116 | var tmp = _md.blockLength - keylen; 117 | for(var i = 0; i < tmp; ++i) { 118 | _ipadding.putByte(0x36); 119 | _opadding.putByte(0x5C); 120 | } 121 | } 122 | _key = key; 123 | _ipadding = _ipadding.bytes(); 124 | _opadding = _opadding.bytes(); 125 | } 126 | 127 | // digest is done like so: hash(opadding | hash(ipadding | message)) 128 | 129 | // prepare to do inner hash 130 | // hash(ipadding | message) 131 | _md.start(); 132 | _md.update(_ipadding); 133 | }; 134 | 135 | /** 136 | * Updates the HMAC with the given message bytes. 137 | * 138 | * @param bytes the bytes to update with. 139 | */ 140 | ctx.update = function(bytes) { 141 | _md.update(bytes); 142 | }; 143 | 144 | /** 145 | * Produces the Message Authentication Code (MAC). 146 | * 147 | * @return a byte buffer containing the digest value. 148 | */ 149 | ctx.getMac = function() { 150 | // digest is done like so: hash(opadding | hash(ipadding | message)) 151 | // here we do the outer hashing 152 | var inner = _md.digest().bytes(); 153 | _md.start(); 154 | _md.update(_opadding); 155 | _md.update(inner); 156 | return _md.digest(); 157 | }; 158 | // alias for getMac 159 | ctx.digest = ctx.getMac; 160 | 161 | return ctx; 162 | }; 163 | 164 | })(); 165 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/md5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Message Digest Algorithm 5 with 128-bit digest (MD5) implementation. 3 | * 4 | * This implementation is currently limited to message lengths (in bytes) that 5 | * are up to 32-bits in size. 6 | * 7 | * @author Dave Longley 8 | * 9 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. 10 | */ 11 | (function() { 12 | 13 | var md5 = {}; 14 | 15 | // define forge 16 | if(typeof(window) !== 'undefined') { 17 | var forge = window.forge = window.forge || {}; 18 | } 19 | // define node.js module 20 | else if(typeof(module) !== 'undefined' && module.exports) { 21 | var forge = { 22 | util: require('./util') 23 | }; 24 | module.exports = md5; 25 | } 26 | forge.md = forge.md || {}; 27 | forge.md.algorithms = forge.md.algorithms || {}; 28 | forge.md.md5 = forge.md.algorithms['md5'] = md5; 29 | 30 | // padding, constant tables for calculating md5 31 | var _padding = null; 32 | var _g = null; 33 | var _r = null; 34 | var _k = null; 35 | var _initialized = false; 36 | 37 | /** 38 | * Initializes the constant tables. 39 | */ 40 | var _init = function() { 41 | // create padding 42 | _padding = String.fromCharCode(128); 43 | _padding += forge.util.fillString(String.fromCharCode(0x00), 64); 44 | 45 | // g values 46 | _g = [ 47 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 48 | 1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 49 | 5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2, 50 | 0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9]; 51 | 52 | // rounds table 53 | _r = [ 54 | 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 55 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 56 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 57 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]; 58 | 59 | // get the result of abs(sin(i + 1)) as a 32-bit integer 60 | _k = new Array(64); 61 | for(var i = 0; i < 64; ++i) { 62 | _k[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000); 63 | } 64 | 65 | // now initialized 66 | _initialized = true; 67 | }; 68 | 69 | /** 70 | * Updates an MD5 state with the given byte buffer. 71 | * 72 | * @param s the MD5 state to update. 73 | * @param w the array to use to store words. 74 | * @param bytes the byte buffer to update with. 75 | */ 76 | var _update = function(s, w, bytes) { 77 | // consume 512 bit (64 byte) chunks 78 | var t, a, b, c, d, f, r, i; 79 | var len = bytes.length(); 80 | while(len >= 64) { 81 | // initialize hash value for this chunk 82 | a = s.h0; 83 | b = s.h1; 84 | c = s.h2; 85 | d = s.h3; 86 | 87 | // round 1 88 | for(i = 0; i < 16; ++i) { 89 | w[i] = bytes.getInt32Le(); 90 | f = d ^ (b & (c ^ d)); 91 | t = (a + f + _k[i] + w[i]); 92 | r = _r[i]; 93 | a = d; 94 | d = c; 95 | c = b; 96 | b += (t << r) | (t >>> (32 - r)); 97 | } 98 | // round 2 99 | for(; i < 32; ++i) { 100 | f = c ^ (d & (b ^ c)); 101 | t = (a + f + _k[i] + w[_g[i]]); 102 | r = _r[i]; 103 | a = d; 104 | d = c; 105 | c = b; 106 | b += (t << r) | (t >>> (32 - r)); 107 | } 108 | // round 3 109 | for(; i < 48; ++i) { 110 | f = b ^ c ^ d; 111 | t = (a + f + _k[i] + w[_g[i]]); 112 | r = _r[i]; 113 | a = d; 114 | d = c; 115 | c = b; 116 | b += (t << r) | (t >>> (32 - r)); 117 | } 118 | // round 4 119 | for(; i < 64; ++i) { 120 | f = c ^ (b | ~d); 121 | t = (a + f + _k[i] + w[_g[i]]); 122 | r = _r[i]; 123 | a = d; 124 | d = c; 125 | c = b; 126 | b += (t << r) | (t >>> (32 - r)); 127 | } 128 | 129 | // update hash state 130 | s.h0 = (s.h0 + a) & 0xFFFFFFFF; 131 | s.h1 = (s.h1 + b) & 0xFFFFFFFF; 132 | s.h2 = (s.h2 + c) & 0xFFFFFFFF; 133 | s.h3 = (s.h3 + d) & 0xFFFFFFFF; 134 | 135 | len -= 64; 136 | } 137 | }; 138 | 139 | /** 140 | * Creates an MD5 message digest object. 141 | * 142 | * @return a message digest object. 143 | */ 144 | md5.create = function() { 145 | // do initialization as necessary 146 | if(!_initialized) { 147 | _init(); 148 | } 149 | 150 | // MD5 state contains four 32-bit integers 151 | var _state = null; 152 | 153 | // input buffer 154 | var _input = forge.util.createBuffer(); 155 | 156 | // used for word storage 157 | var _w = new Array(16); 158 | 159 | // message digest object 160 | var md = { 161 | algorithm: 'md5', 162 | blockLength: 64, 163 | digestLength: 16, 164 | // length of message so far (does not including padding) 165 | messageLength: 0 166 | }; 167 | 168 | /** 169 | * Starts the digest. 170 | */ 171 | md.start = function() { 172 | md.messageLength = 0; 173 | _input = forge.util.createBuffer(); 174 | _state = { 175 | h0: 0x67452301, 176 | h1: 0xEFCDAB89, 177 | h2: 0x98BADCFE, 178 | h3: 0x10325476 179 | }; 180 | }; 181 | // start digest automatically for first time 182 | md.start(); 183 | 184 | /** 185 | * Updates the digest with the given message input. The given input can 186 | * treated as raw input (no encoding will be applied) or an encoding of 187 | * 'utf8' maybe given to encode the input using UTF-8. 188 | * 189 | * @param msg the message input to update with. 190 | * @param encoding the encoding to use (default: 'raw', other: 'utf8'). 191 | */ 192 | md.update = function(msg, encoding) { 193 | if(encoding === 'utf8') { 194 | msg = forge.util.encodeUtf8(msg); 195 | } 196 | 197 | // update message length 198 | md.messageLength += msg.length; 199 | 200 | // add bytes to input buffer 201 | _input.putBytes(msg); 202 | 203 | // process bytes 204 | _update(_state, _w, _input); 205 | 206 | // compact input buffer every 2K or if empty 207 | if(_input.read > 2048 || _input.length() === 0) { 208 | _input.compact(); 209 | } 210 | }; 211 | 212 | /** 213 | * Produces the digest. 214 | * 215 | * @return a byte buffer containing the digest value. 216 | */ 217 | md.digest = function() { 218 | /* Note: Here we copy the remaining bytes in the input buffer and 219 | add the appropriate MD5 padding. Then we do the final update 220 | on a copy of the state so that if the user wants to get 221 | intermediate digests they can do so. */ 222 | 223 | /* Determine the number of bytes that must be added to the message 224 | to ensure its length is congruent to 448 mod 512. In other words, 225 | a 64-bit integer that gives the length of the message will be 226 | appended to the message and whatever the length of the message is 227 | plus 64 bits must be a multiple of 512. So the length of the 228 | message must be congruent to 448 mod 512 because 512 - 64 = 448. 229 | 230 | In order to fill up the message length it must be filled with 231 | padding that begins with 1 bit followed by all 0 bits. Padding 232 | must *always* be present, so if the message length is already 233 | congruent to 448 mod 512, then 512 padding bits must be added. */ 234 | 235 | // 512 bits == 64 bytes, 448 bits == 56 bytes, 64 bits = 8 bytes 236 | // _padding starts with 1 byte with first bit is set in it which 237 | // is byte value 128, then there may be up to 63 other pad bytes 238 | var len = md.messageLength; 239 | var padBytes = forge.util.createBuffer(); 240 | padBytes.putBytes(_input.bytes()); 241 | padBytes.putBytes(_padding.substr(0, 64 - ((len + 8) % 64))); 242 | 243 | /* Now append length of the message. The length is appended in bits 244 | as a 64-bit number in little-endian format. Since we store the 245 | length in bytes, we must multiply it by 8 (or left shift by 3). So 246 | here store the high 3 bits in the high end of the second 32-bits of 247 | the 64-bit number and the lower 5 bits in the low end of the 248 | second 32-bits. */ 249 | padBytes.putInt32Le((len << 3) & 0xFFFFFFFF); 250 | padBytes.putInt32Le((len >>> 29) & 0xFF); 251 | var s2 = { 252 | h0: _state.h0, 253 | h1: _state.h1, 254 | h2: _state.h2, 255 | h3: _state.h3 256 | }; 257 | _update(s2, _w, padBytes); 258 | var rval = forge.util.createBuffer(); 259 | rval.putInt32Le(s2.h0); 260 | rval.putInt32Le(s2.h1); 261 | rval.putInt32Le(s2.h2); 262 | rval.putInt32Le(s2.h3); 263 | return rval; 264 | }; 265 | 266 | return md; 267 | }; 268 | 269 | })(); 270 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/mgf1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript implementation of mask generation function MGF1. 3 | * 4 | * @author Stefan Siegl 5 | * 6 | * Copyright (c) 2012 Stefan Siegl 7 | */ 8 | (function() { 9 | 10 | var mgf1 = {}; 11 | 12 | // define forge 13 | var forge = {}; 14 | if(typeof(window) !== 'undefined') { 15 | forge = window.forge = window.forge || {}; 16 | } 17 | // define node.js module 18 | else if(typeof(module) !== 'undefined' && module.exports) { 19 | forge = { 20 | util: require('./util') 21 | }; 22 | module.exports = mgf1; 23 | } 24 | 25 | forge.mgf = forge.mgf || {}; 26 | forge.mgf.mgf1 = mgf1; 27 | 28 | /** 29 | * Creates a MGF1 mask generation function object. 30 | * 31 | * @return a mask generation function object. 32 | */ 33 | mgf1.create = function(md) { 34 | var mgf = { 35 | /** 36 | * Generate mask of specified length. 37 | * 38 | * @param {String} seed The seed for mask generation. 39 | * @param maskLen Number of bytes to generate. 40 | * @return {String} The generated mask. 41 | */ 42 | generate: function(seed, maskLen) { 43 | /* 2. Let T be the empty octet string. */ 44 | var t = new forge.util.ByteBuffer(); 45 | 46 | /* 3. For counter from 0 to maskLen / hLen - 1, do the following: */ 47 | var len = Math.ceil(maskLen / md.digestLength); 48 | for(var i = 0; i < len; i++) { 49 | /* a. Convert counter to an octet string C of length 4 octets */ 50 | var c = new forge.util.ByteBuffer(); 51 | c.putInt32(i); 52 | 53 | /* b. Concatenate the hash of the seed mgfSeed and C to the octet 54 | * string T: */ 55 | md.start(); 56 | md.update(seed + c.getBytes()); 57 | t.putBuffer(md.digest()); 58 | } 59 | 60 | /* Output the leading maskLen octets of T as the octet string mask. */ 61 | t.truncate(t.length() - maskLen); 62 | return t.getBytes(); 63 | } 64 | }; 65 | 66 | return mgf; 67 | }; 68 | 69 | })(); 70 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/oids.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Object IDs for ASN.1. 3 | * 4 | * @author Dave Longley 5 | * 6 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. 7 | */ 8 | (function() { 9 | 10 | var oids = {}; 11 | 12 | // define forge 13 | if(typeof(window) !== 'undefined') { 14 | var forge = window.forge = window.forge || {}; 15 | } 16 | // define node.js module 17 | else if(typeof(module) !== 'undefined' && module.exports) { 18 | var forge = {}; 19 | module.exports = oids; 20 | } 21 | forge.pki = forge.pki || {}; 22 | forge.pki.oids = oids; 23 | 24 | // algorithm OIDs 25 | oids['1.2.840.113549.1.1.1'] = 'rsaEncryption'; 26 | oids['rsaEncryption'] = '1.2.840.113549.1.1.1'; 27 | // Note: md2 & md4 not implemented 28 | //oids['1.2.840.113549.1.1.2'] = 'md2withRSAEncryption'; 29 | //oids['md2withRSAEncryption'] = '1.2.840.113549.1.1.2'; 30 | //oids['1.2.840.113549.1.1.3'] = 'md4withRSAEncryption'; 31 | //oids['md4withRSAEncryption'] = '1.2.840.113549.1.1.3'; 32 | oids['1.2.840.113549.1.1.4'] = 'md5withRSAEncryption'; 33 | oids['md5withRSAEncryption'] = '1.2.840.113549.1.1.4'; 34 | oids['1.2.840.113549.1.1.5'] = 'sha1withRSAEncryption'; 35 | oids['sha1withRSAEncryption'] = '1.2.840.113549.1.1.5'; 36 | oids['1.2.840.113549.1.1.7'] = 'RSAES-OAEP'; 37 | oids['RSAES-OAEP'] = '1.2.840.113549.1.1.7'; 38 | oids['1.2.840.113549.1.1.8'] = 'mgf1'; 39 | oids['mgf1'] = '1.2.840.113549.1.1.8'; 40 | oids['1.2.840.113549.1.1.9'] = 'pSpecified'; 41 | oids['pSpecified'] = '1.2.840.113549.1.1.9'; 42 | oids['1.2.840.113549.1.1.10'] = 'RSASSA-PSS'; 43 | oids['RSASSA-PSS'] = '1.2.840.113549.1.1.10'; 44 | oids['1.2.840.113549.1.1.11'] = 'sha256WithRSAEncryption'; 45 | oids['sha256WithRSAEncryption'] = '1.2.840.113549.1.1.11'; 46 | oids['1.2.840.113549.1.1.12'] = 'sha384WithRSAEncryption'; 47 | oids['sha384WithRSAEncryption'] = '1.2.840.113549.1.1.12'; 48 | oids['1.2.840.113549.1.1.13'] = 'sha512WithRSAEncryption'; 49 | oids['sha512WithRSAEncryption'] = '1.2.840.113549.1.1.13'; 50 | 51 | oids['1.3.14.3.2.26'] = 'sha1'; 52 | oids['sha1'] = '1.3.14.3.2.26'; 53 | oids['2.16.840.1.101.3.4.2.1'] = 'sha256'; 54 | oids['sha256'] = '2.16.840.1.101.3.4.2.1'; 55 | oids['2.16.840.1.101.3.4.2.2'] = 'sha384'; 56 | oids['sha384'] = '2.16.840.1.101.3.4.2.2'; 57 | oids['2.16.840.1.101.3.4.2.3'] = 'sha512'; 58 | oids['sha512'] = '2.16.840.1.101.3.4.2.3'; 59 | oids['1.2.840.113549.2.5'] = 'md5'; 60 | oids['md5'] = '1.2.840.113549.2.5'; 61 | 62 | // pkcs#7 content types 63 | oids['1.2.840.113549.1.7.1'] = 'data'; 64 | oids['data'] = '1.2.840.113549.1.7.1'; 65 | oids['1.2.840.113549.1.7.2'] = 'signedData'; 66 | oids['signedData'] = '1.2.840.113549.1.7.2'; 67 | oids['1.2.840.113549.1.7.3'] = 'envelopedData'; 68 | oids['envelopedData'] = '1.2.840.113549.1.7.3'; 69 | oids['1.2.840.113549.1.7.4'] = 'signedAndEnvelopedData'; 70 | oids['signedAndEnvelopedData'] = '1.2.840.113549.1.7.4'; 71 | oids['1.2.840.113549.1.7.5'] = 'digestedData'; 72 | oids['digestedData'] = '1.2.840.113549.1.7.5'; 73 | oids['1.2.840.113549.1.7.6'] = 'encryptedData'; 74 | oids['encryptedData'] = '1.2.840.113549.1.7.6'; 75 | 76 | // pkcs#9 oids 77 | oids['1.2.840.113549.1.9.20'] = 'friendlyName'; 78 | oids['friendlyName'] = '1.2.840.113549.1.9.20'; 79 | oids['1.2.840.113549.1.9.21'] = 'localKeyId'; 80 | oids['localKeyId'] = '1.2.840.113549.1.9.21'; 81 | oids['1.2.840.113549.1.9.22.1'] = 'x509Certificate'; 82 | oids['x509Certificate'] = '1.2.840.113549.1.9.22.1'; 83 | 84 | // pkcs#12 safe bags 85 | oids['1.2.840.113549.1.12.10.1.1'] = 'keyBag'; 86 | oids['keyBag'] = '1.2.840.113549.1.12.10.1.1'; 87 | oids['1.2.840.113549.1.12.10.1.2'] = 'pkcs8ShroudedKeyBag'; 88 | oids['pkcs8ShroudedKeyBag'] = '1.2.840.113549.1.12.10.1.2'; 89 | oids['1.2.840.113549.1.12.10.1.3'] = 'certBag'; 90 | oids['certBag'] = '1.2.840.113549.1.12.10.1.3'; 91 | oids['1.2.840.113549.1.12.10.1.4'] = 'crlBag'; 92 | oids['crlBag'] = '1.2.840.113549.1.12.10.1.4'; 93 | oids['1.2.840.113549.1.12.10.1.5'] = 'secretBag'; 94 | oids['secretBag'] = '1.2.840.113549.1.12.10.1.5'; 95 | oids['1.2.840.113549.1.12.10.1.6'] = 'safeContentsBag'; 96 | oids['safeContentsBag'] = '1.2.840.113549.1.12.10.1.6'; 97 | 98 | // password-based-encryption for pkcs#12 99 | oids['1.2.840.113549.1.5.13'] = 'pkcs5PBES2'; 100 | oids['pkcs5PBES2'] = '1.2.840.113549.1.5.13'; 101 | oids['1.2.840.113549.1.5.12'] = 'pkcs5PBKDF2'; 102 | oids['pkcs5PBKDF2'] = '1.2.840.113549.1.5.12'; 103 | 104 | oids['1.2.840.113549.1.12.1.1'] = 'pbeWithSHAAnd128BitRC4'; 105 | oids['pbeWithSHAAnd128BitRC4'] = '1.2.840.113549.1.12.1.1'; 106 | oids['1.2.840.113549.1.12.1.2'] = 'pbeWithSHAAnd40BitRC4'; 107 | oids['pbeWithSHAAnd40BitRC4'] = '1.2.840.113549.1.12.1.2'; 108 | oids['1.2.840.113549.1.12.1.3'] = 'pbeWithSHAAnd3-KeyTripleDES-CBC'; 109 | oids['pbeWithSHAAnd3-KeyTripleDES-CBC'] = '1.2.840.113549.1.12.1.3'; 110 | oids['1.2.840.113549.1.12.1.4'] = 'pbeWithSHAAnd2-KeyTripleDES-CBC'; 111 | oids['pbeWithSHAAnd2-KeyTripleDES-CBC'] = '1.2.840.113549.1.12.1.4'; 112 | oids['1.2.840.113549.1.12.1.5'] = 'pbeWithSHAAnd128BitRC2-CBC'; 113 | oids['pbeWithSHAAnd128BitRC2-CBC'] = '1.2.840.113549.1.12.1.5'; 114 | oids['1.2.840.113549.1.12.1.6'] = 'pbewithSHAAnd40BitRC2-CBC'; 115 | oids['pbewithSHAAnd40BitRC2-CBC'] = '1.2.840.113549.1.12.1.6'; 116 | 117 | // symmetric key algorithm oids 118 | oids['1.2.840.113549.3.7'] = 'des-EDE3-CBC'; 119 | oids['des-EDE3-CBC'] = '1.2.840.113549.3.7'; 120 | oids['2.16.840.1.101.3.4.1.2'] = 'aes128-CBC'; 121 | oids['aes128-CBC'] = '2.16.840.1.101.3.4.1.2'; 122 | oids['2.16.840.1.101.3.4.1.22'] = 'aes192-CBC'; 123 | oids['aes192-CBC'] = '2.16.840.1.101.3.4.1.22'; 124 | oids['2.16.840.1.101.3.4.1.42'] = 'aes256-CBC'; 125 | oids['aes256-CBC'] = '2.16.840.1.101.3.4.1.42'; 126 | 127 | // certificate issuer/subject OIDs 128 | oids['2.5.4.3'] = 'commonName'; 129 | oids['commonName'] = '2.5.4.3'; 130 | oids['2.5.4.5'] = 'serialName'; 131 | oids['serialName'] = '2.5.4.5'; 132 | oids['2.5.4.6'] = 'countryName'; 133 | oids['countryName'] = '2.5.4.6'; 134 | oids['2.5.4.7'] = 'localityName'; 135 | oids['localityName'] = '2.5.4.7'; 136 | oids['2.5.4.8'] = 'stateOrProvinceName'; 137 | oids['stateOrProvinceName'] = '2.5.4.8'; 138 | oids['2.5.4.10'] = 'organizationName'; 139 | oids['organizationName'] = '2.5.4.10'; 140 | oids['2.5.4.11'] = 'organizationalUnitName'; 141 | oids['organizationalUnitName'] = '2.5.4.11'; 142 | oids['1.2.840.113549.1.9.1'] = 'emailAddress'; 143 | oids['emailAddress'] = '1.2.840.113549.1.9.1'; 144 | 145 | // X.509 extension OIDs 146 | oids['2.5.29.1'] = 'authorityKeyIdentifier'; // deprecated, use .35 147 | oids['2.5.29.2'] = 'keyAttributes'; // obsolete use .37 or .15 148 | oids['2.5.29.3'] = 'certificatePolicies'; // deprecated, use .32 149 | oids['2.5.29.4'] = 'keyUsageRestriction'; // obsolete use .37 or .15 150 | oids['2.5.29.5'] = 'policyMapping'; // deprecated use .33 151 | oids['2.5.29.6'] = 'subtreesConstraint'; // obsolete use .30 152 | oids['2.5.29.7'] = 'subjectAltName'; // deprecated use .17 153 | oids['2.5.29.8'] = 'issuerAltName'; // deprecated use .18 154 | oids['2.5.29.9'] = 'subjectDirectoryAttributes'; 155 | oids['2.5.29.10'] = 'basicConstraints'; // deprecated use .19 156 | oids['2.5.29.11'] = 'nameConstraints'; // deprecated use .30 157 | oids['2.5.29.12'] = 'policyConstraints'; // deprecated use .36 158 | oids['2.5.29.13'] = 'basicConstraints'; // deprecated use .19 159 | oids['2.5.29.14'] = 'subjectKeyIdentifier'; 160 | oids['subjectKeyIdentifier'] = '2.5.29.14'; 161 | oids['2.5.29.15'] = 'keyUsage'; 162 | oids['keyUsage'] = '2.5.29.15'; 163 | oids['2.5.29.16'] = 'privateKeyUsagePeriod'; 164 | oids['2.5.29.17'] = 'subjectAltName'; 165 | oids['subjectAltName'] = '2.5.29.17'; 166 | oids['2.5.29.18'] = 'issuerAltName'; 167 | oids['issuerAltName'] = '2.5.29.18'; 168 | oids['2.5.29.19'] = 'basicConstraints'; 169 | oids['basicConstraints'] = '2.5.29.19'; 170 | oids['2.5.29.20'] = 'cRLNumber'; 171 | oids['2.5.29.21'] = 'cRLReason'; 172 | oids['2.5.29.22'] = 'expirationDate'; 173 | oids['2.5.29.23'] = 'instructionCode'; 174 | oids['2.5.29.24'] = 'invalidityDate'; 175 | oids['2.5.29.25'] = 'cRLDistributionPoints'; // deprecated use .31 176 | oids['2.5.29.26'] = 'issuingDistributionPoint'; // deprecated use .28 177 | oids['2.5.29.27'] = 'deltaCRLIndicator'; 178 | oids['2.5.29.28'] = 'issuingDistributionPoint'; 179 | oids['2.5.29.29'] = 'certificateIssuer'; 180 | oids['2.5.29.30'] = 'nameConstraints'; 181 | oids['2.5.29.31'] = 'cRLDistributionPoints'; 182 | oids['2.5.29.32'] = 'certificatePolicies'; 183 | oids['2.5.29.33'] = 'policyMappings'; 184 | oids['2.5.29.34'] = 'policyConstraints'; // deprecated use .36 185 | oids['2.5.29.35'] = 'authorityKeyIdentifier'; 186 | oids['2.5.29.36'] = 'policyConstraints'; 187 | oids['2.5.29.37'] = 'extKeyUsage'; 188 | oids['extKeyUsage'] = '2.5.29.37'; 189 | oids['2.5.29.46'] = 'freshestCRL'; 190 | oids['2.5.29.54'] = 'inhibitAnyPolicy'; 191 | 192 | })(); 193 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/pbkdf2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Password-Based Key-Derivation Function #2 implementation. 3 | * 4 | * See RFC 2898 for details. 5 | * 6 | * @author Dave Longley 7 | * 8 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. 9 | */ 10 | (function() { 11 | 12 | // define forge 13 | if(typeof(window) !== 'undefined') { 14 | var forge = window.forge = window.forge || {}; 15 | forge.pkcs5 = {}; 16 | } 17 | // define node.js module 18 | else if(typeof(module) !== 'undefined' && module.exports) { 19 | var forge = { 20 | hmac: require('./hmac'), 21 | md: require('./md'), 22 | util: require('./util') 23 | }; 24 | module.exports = forge.pkcs5 = {}; 25 | } 26 | 27 | var pkcs5 = forge.pkcs5; 28 | 29 | /** 30 | * Derives a key from a password. 31 | * 32 | * @param p the password as a string of bytes. 33 | * @param s the salt as a string of bytes. 34 | * @param c the iteration count, a positive integer. 35 | * @param dkLen the intended length, in bytes, of the derived key, 36 | * (max: 2^32 - 1) * hash length of the PRF. 37 | * @param md the message digest to use in the PRF, defaults to SHA-1. 38 | * 39 | * @return the derived key, as a string of bytes. 40 | */ 41 | pkcs5.pbkdf2 = function(p, s, c, dkLen, md) { 42 | // default prf to SHA-1 43 | if(typeof(md) === 'undefined' || md === null) { 44 | md = forge.md.sha1.create(); 45 | } 46 | 47 | var hLen = md.digestLength; 48 | 49 | /* 1. If dkLen > (2^32 - 1) * hLen, output "derived key too long" and 50 | stop. */ 51 | if(dkLen > (0xFFFFFFFF * hLen)) { 52 | throw { 53 | message: 'Derived key is too long.' 54 | }; 55 | } 56 | 57 | /* 2. Let len be the number of hLen-octet blocks in the derived key, 58 | rounding up, and let r be the number of octets in the last 59 | block: 60 | 61 | len = CEIL(dkLen / hLen), 62 | r = dkLen - (len - 1) * hLen. */ 63 | var len = Math.ceil(dkLen / hLen); 64 | var r = dkLen - (len - 1) * hLen; 65 | 66 | /* 3. For each block of the derived key apply the function F defined 67 | below to the password P, the salt S, the iteration count c, and 68 | the block index to compute the block: 69 | 70 | T_1 = F(P, S, c, 1), 71 | T_2 = F(P, S, c, 2), 72 | ... 73 | T_len = F(P, S, c, len), 74 | 75 | where the function F is defined as the exclusive-or sum of the 76 | first c iterates of the underlying pseudorandom function PRF 77 | applied to the password P and the concatenation of the salt S 78 | and the block index i: 79 | 80 | F(P, S, c, i) = u_1 XOR u_2 XOR ... XOR u_c 81 | 82 | where 83 | 84 | u_1 = PRF(P, S || INT(i)), 85 | u_2 = PRF(P, u_1), 86 | ... 87 | u_c = PRF(P, u_{c-1}). 88 | 89 | Here, INT(i) is a four-octet encoding of the integer i, most 90 | significant octet first. */ 91 | var prf = forge.hmac.create(); 92 | prf.start(md, p); 93 | var dk = ''; 94 | var xor, u_c, u_c1; 95 | for(var i = 1; i <= len; ++i) { 96 | // PRF(P, S || INT(i)) (first iteration) 97 | prf.update(s); 98 | prf.update(forge.util.int32ToBytes(i)); 99 | xor = u_c1 = prf.digest().getBytes(); 100 | 101 | // PRF(P, u_{c-1}) (other iterations) 102 | for(var j = 2; j <= c; ++j) { 103 | prf.start(null, null); 104 | prf.update(u_c1); 105 | u_c = prf.digest().getBytes(); 106 | // F(p, s, c, i) 107 | xor = forge.util.xorBytes(xor, u_c, hLen); 108 | u_c1 = u_c; 109 | } 110 | 111 | /* 4. Concatenate the blocks and extract the first dkLen octets to 112 | produce a derived key DK: 113 | 114 | DK = T_1 || T_2 || ... || T_len<0..r-1> */ 115 | dk += (i < len) ? xor : xor.substr(0, r); 116 | } 117 | 118 | /* 5. Output the derived key DK. */ 119 | return dk; 120 | }; 121 | 122 | })(); 123 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/prng.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A javascript implementation of a cryptographically-secure 3 | * Pseudo Random Number Generator (PRNG). The Fortuna algorithm is mostly 4 | * followed here. SHA-1 is used instead of SHA-256. 5 | * 6 | * @author Dave Longley 7 | * 8 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. 9 | */ 10 | (function() { 11 | 12 | // define forge 13 | if(typeof(window) !== 'undefined') { 14 | var forge = window.forge = window.forge || {}; 15 | forge.prng = {}; 16 | } 17 | // define node.js module 18 | else if(typeof(module) !== 'undefined' && module.exports) { 19 | var forge = { 20 | md: require('./md'), 21 | util: require('./util') 22 | }; 23 | forge.md.sha1.create(); 24 | module.exports = forge.prng = {}; 25 | } 26 | 27 | /* PRNG API */ 28 | var prng = forge.prng; 29 | 30 | /** 31 | * Creates a new PRNG context. 32 | * 33 | * A PRNG plugin must be passed in that will provide: 34 | * 35 | * 1. A function that initializes the key and seed of a PRNG context. It 36 | * will be given a 16 byte key and a 16 byte seed. Any key expansion 37 | * or transformation of the seed from a byte string into an array of 38 | * integers (or similar) should be performed. 39 | * 2. The cryptographic function used by the generator. It takes a key and 40 | * a seed. 41 | * 3. A seed increment function. It takes the seed and return seed + 1. 42 | * 4. An api to create a message digest. 43 | * 44 | * For an example, see random.js. 45 | * 46 | * @param plugin the PRNG plugin to use. 47 | */ 48 | prng.create = function(plugin) { 49 | var ctx = { 50 | plugin: plugin, 51 | key: null, 52 | seed: null, 53 | time: null, 54 | // number of reseeds so far 55 | reseeds: 0, 56 | // amount of data generated so far 57 | generated: 0 58 | }; 59 | 60 | // create 32 entropy pools (each is a message digest) 61 | var md = plugin.md; 62 | var pools = new Array(32); 63 | for(var i = 0; i < 32; ++i) { 64 | pools[i] = md.create(); 65 | } 66 | ctx.pools = pools; 67 | 68 | // entropy pools are written to cyclically, starting at index 0 69 | ctx.pool = 0; 70 | 71 | /** 72 | * Generates random bytes. 73 | * 74 | * @param count the number of random bytes to generate. 75 | * 76 | * @return count random bytes as a string. 77 | */ 78 | ctx.generate = function(count) { 79 | // do first seed if necessary 80 | if(ctx.key === null) { 81 | _reseed(); 82 | } 83 | 84 | // simple generator using counter-based CBC 85 | var cipher = ctx.plugin.cipher; 86 | var increment = ctx.plugin.increment; 87 | var formatKey = ctx.plugin.formatKey; 88 | var formatSeed = ctx.plugin.formatSeed; 89 | var b = forge.util.createBuffer(); 90 | while(b.length() < count) { 91 | // generate the random bytes 92 | var bytes = cipher(ctx.key, ctx.seed); 93 | ctx.generated += bytes.length; 94 | b.putBytes(bytes); 95 | 96 | // generate bytes for a new key and seed 97 | ctx.key = formatKey(cipher(ctx.key, increment(ctx.seed))); 98 | ctx.seed = formatSeed(cipher(ctx.key, ctx.seed)); 99 | 100 | // if amount of data generated is greater than 1 MiB, reseed 101 | if(ctx.generated >= 1048576) { 102 | // only do reseed at most 10 times/second (every 100 ms) 103 | var now = +new Date(); 104 | if(now - ctx.time < 100) { 105 | _reseed(); 106 | } 107 | } 108 | } 109 | 110 | return b.getBytes(count); 111 | }; 112 | 113 | /** 114 | * Private function that reseeds a generator. 115 | */ 116 | function _reseed() { 117 | // not enough seed data... but we need to get going so just 118 | // be sad and add some weak random data 119 | if(ctx.pools[0].messageLength < 32) { 120 | /* Draws from Park-Miller "minimal standard" 31 bit PRNG, 121 | implemented with David G. Carta's optimization: with 32 bit math 122 | and without division (Public Domain). */ 123 | var needed = (32 - ctx.pools[0].messageLength) << 5; 124 | var b = ''; 125 | var hi, lo, next; 126 | var seed = Math.floor(Math.random() * 0xFFFF); 127 | while(b.length < needed) { 128 | lo = 16807 * (seed & 0xFFFF); 129 | hi = 16807 * (seed >> 16); 130 | lo += (hi & 0x7FFF) << 16; 131 | lo += hi >> 15; 132 | lo = (lo & 0x7FFFFFFF) + (lo >> 31); 133 | seed = lo & 0xFFFFFFFF; 134 | 135 | // consume lower 3 bytes of seed 136 | for(var i = 0; i < 3; ++i) { 137 | // throw in more pseudo random 138 | next = seed >>> (i << 3); 139 | next ^= Math.floor(Math.random() * 0xFF); 140 | b += String.fromCharCode(next & 0xFF); 141 | } 142 | } 143 | // will automatically reseed in collect 144 | ctx.collect(b); 145 | } 146 | else { 147 | // create a SHA-1 message digest 148 | var md = forge.md.sha1.create(); 149 | 150 | // digest pool 0's entropy and restart it 151 | md.update(ctx.pools[0].digest().getBytes()); 152 | ctx.pools[0].start(); 153 | 154 | // digest the entropy of other pools whose index k meet the 155 | // condition '2^k mod n == 0' where n is the number of reseeds 156 | var k = 1; 157 | for(var i = 1; i < 32; ++i) { 158 | // prevent signed numbers from being used 159 | k = (k == 31) ? 2147483648 : (k << 2); 160 | if(k % ctx.reseeds === 0) { 161 | md.update(ctx.pools[i].digest().getBytes()); 162 | ctx.pools[i].start(); 163 | } 164 | } 165 | 166 | // get digest for key bytes and iterate again for seed bytes 167 | var keyBytes = md.digest().getBytes(); 168 | md.start(); 169 | md.update(keyBytes); 170 | var seedBytes = md.digest().getBytes(); 171 | 172 | // update 173 | ctx.key = ctx.plugin.formatKey(keyBytes); 174 | ctx.seed = ctx.plugin.formatSeed(seedBytes); 175 | ++ctx.reseeds; 176 | ctx.generated = 0; 177 | ctx.time = +new Date(); 178 | } 179 | } 180 | 181 | /** 182 | * Adds entropy to a prng ctx's accumulator. 183 | * 184 | * @param bytes the bytes of entropy as a string. 185 | */ 186 | ctx.collect = function(bytes) { 187 | // iterate over pools distributing entropy cyclically 188 | var count = bytes.length; 189 | for(var i = 0; i < count; ++i) { 190 | ctx.pools[ctx.pool].update(bytes.substr(i, 1)); 191 | ctx.pool = (ctx.pool === 31) ? 0 : ctx.pool + 1; 192 | } 193 | 194 | // do reseed if pool 0 has at least 32 bytes (enough to create a new 195 | // key and seed) 196 | if(ctx.pools[0].messageLength >= 32) { 197 | // only do reseed at most 10 times/second (every 100 ms) 198 | var now = +new Date(); 199 | if(ctx.time === null || (now - ctx.time < 100)) { 200 | _reseed(); 201 | } 202 | } 203 | }; 204 | 205 | /** 206 | * Collects an integer of n bits. 207 | * 208 | * @param i the integer entropy. 209 | * @param n the number of bits in the integer. 210 | */ 211 | ctx.collectInt = function(i, n) { 212 | var bytes = ''; 213 | do { 214 | n -= 8; 215 | bytes += String.fromCharCode((i >> n) & 0xFF); 216 | } 217 | while(n > 0); 218 | ctx.collect(bytes); 219 | }; 220 | 221 | return ctx; 222 | }; 223 | 224 | })(); 225 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/pss.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript implementation of PKCS#1 PSS signature padding. 3 | * 4 | * @author Stefan Siegl 5 | * 6 | * Copyright (c) 2012 Stefan Siegl 7 | */ 8 | (function() { 9 | 10 | // define forge 11 | var forge = {}; 12 | if(typeof(window) !== 'undefined') { 13 | forge = window.forge = window.forge || {}; 14 | } 15 | // define node.js module 16 | else if(typeof(module) !== 'undefined' && module.exports) { 17 | forge = { 18 | random: require('./random'), 19 | util: require('./util') 20 | }; 21 | module.exports = forge.pss = {}; 22 | } 23 | 24 | // shortcut for PSS API 25 | var pss = forge.pss = forge.pss || {}; 26 | 27 | /** 28 | * Creates a PSS signature scheme object. 29 | * 30 | * @param hash hash function to use, a Forge md instance 31 | * @param mgf mask generation function to use, a Forge mgf instance 32 | * @param sLen length of the salt in octets 33 | * @return a signature scheme object. 34 | */ 35 | pss.create = function(hash, mgf, sLen) { 36 | var hLen = hash.digestLength; 37 | var pssobj = {}; 38 | 39 | /** 40 | * Verify PSS signature 41 | * 42 | * This function implements EMSA-PSS-VERIFY as per RFC 3447, section 9.1.2 43 | * 44 | * @param {String} mHash The message digest hash to compare against 45 | * the signature. 46 | * @param {String} em The encoded message (RSA decryption result). 47 | * @param modsBits Length of the RSA modulus in bits. 48 | * @return true if the signature was verified, false if not. 49 | */ 50 | pssobj.verify = function(mHash, em, modBits) { 51 | var i; 52 | var emBits = modBits - 1; 53 | var emLen = Math.ceil(emBits / 8); 54 | 55 | /* c. Convert the message representative m to an encoded message EM 56 | * of length emLen = (modBits - 1) / 8 octets, where modBits 57 | * is the length in bits of the RSA modulus n */ 58 | em = em.substr(-emLen); 59 | 60 | /* 3. If emLen < hLen + sLen + 2, output "inconsistent" and stop. */ 61 | if(emLen < hLen + sLen + 2) { 62 | throw { 63 | message: 'Inconsistent parameters to PSS signature verification.' 64 | }; 65 | } 66 | 67 | /* 4. If the rightmost octet of EM does not have hexadecimal value 68 | * 0xbc, output "inconsistent" and stop. */ 69 | if(em.charCodeAt(emLen - 1) !== 0xbc) { 70 | throw { 71 | message: 'Encoded message does not end in 0xBC.' 72 | }; 73 | } 74 | 75 | /* 5. Let maskedDB be the leftmost emLen - hLen - 1 octets of EM, and 76 | * let H be the next hLen octets. */ 77 | var maskLen = emLen - hLen - 1; 78 | var maskedDB = em.substr(0, maskLen); 79 | var h = em.substr(maskLen, hLen); 80 | 81 | /* 6. If the leftmost 8emLen - emBits bits of the leftmost octet in 82 | * maskedDB are not all equal to zero, output "inconsistent" and stop. */ 83 | var mask = (0xFF00 >> (8 * emLen - emBits)) & 0xFF; 84 | if((maskedDB.charCodeAt(0) & mask) !== 0) { 85 | throw { 86 | message: 'Bits beyond keysize not zero as expected.' 87 | }; 88 | } 89 | 90 | /* 7. Let dbMask = MGF(H, emLen - hLen - 1). */ 91 | var dbMask = mgf.generate(h, maskLen); 92 | 93 | /* 8. Let DB = maskedDB \xor dbMask. */ 94 | var db = ''; 95 | for(i = 0; i < maskLen; i ++) { 96 | db += String.fromCharCode(maskedDB.charCodeAt(i) ^ dbMask.charCodeAt(i)); 97 | } 98 | 99 | /* 9. Set the leftmost 8emLen - emBits bits of the leftmost octet 100 | * in DB to zero. */ 101 | db = String.fromCharCode(db.charCodeAt(0) & ~mask) + db.substr(1); 102 | 103 | /* 10. If the emLen - hLen - sLen - 2 leftmost octets of DB are not zero 104 | * or if the octet at position emLen - hLen - sLen - 1 (the leftmost 105 | * position is "position 1") does not have hexadecimal value 0x01, 106 | * output "inconsistent" and stop. */ 107 | var checkLen = emLen - hLen - sLen - 2; 108 | for(i = 0; i < checkLen; i ++) { 109 | if(db.charCodeAt(i) !== 0x00) { 110 | throw { 111 | message: 'Leftmost octets not zero as expected' 112 | }; 113 | } 114 | } 115 | 116 | if(db.charCodeAt(checkLen) !== 0x01) { 117 | throw { 118 | message: 'Inconsistent PSS signature, 0x01 marker not found' 119 | }; 120 | } 121 | 122 | /* 11. Let salt be the last sLen octets of DB. */ 123 | var salt = db.substr(-sLen); 124 | 125 | /* 12. Let M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt */ 126 | var m_ = new forge.util.ByteBuffer(); 127 | m_.fillWithByte(0, 8); 128 | m_.putBytes(mHash); 129 | m_.putBytes(salt); 130 | 131 | /* 13. Let H' = Hash(M'), an octet string of length hLen. */ 132 | hash.start(); 133 | hash.update(m_.getBytes()); 134 | var h_ = hash.digest().getBytes(); 135 | 136 | /* 14. If H = H', output "consistent." Otherwise, output "inconsistent." */ 137 | return h === h_; 138 | }; 139 | 140 | /** 141 | * Encode PSS signature. 142 | * 143 | * This function implements EMSA-PSS-ENCODE as per RFC 3447, section 9.1.1 144 | * 145 | * @param md the message digest object with the hash to sign. 146 | * @param modsBits Length of the RSA modulus in bits. 147 | * @return the encoded message, string of length (modBits - 1) / 8 148 | */ 149 | pssobj.encode = function(md, modBits) { 150 | var i; 151 | var emBits = modBits - 1; 152 | var emLen = Math.ceil(emBits / 8); 153 | 154 | /* 2. Let mHash = Hash(M), an octet string of length hLen. */ 155 | var mHash = md.digest().getBytes(); 156 | 157 | /* 3. If emLen < hLen + sLen + 2, output "encoding error" and stop. */ 158 | if(emLen < hLen + sLen + 2) { 159 | throw { 160 | message: 'Message is too long to encrypt' 161 | }; 162 | } 163 | 164 | /* 4. Generate a random octet string salt of length sLen; if sLen = 0, 165 | * then salt is the empty string. */ 166 | var salt = forge.random.getBytes(sLen); 167 | 168 | /* 5. Let M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt; */ 169 | var m_ = new forge.util.ByteBuffer(); 170 | m_.fillWithByte(0, 8); 171 | m_.putBytes(mHash); 172 | m_.putBytes(salt); 173 | 174 | /* 6. Let H = Hash(M'), an octet string of length hLen. */ 175 | hash.start(); 176 | hash.update(m_.getBytes()); 177 | var h = hash.digest().getBytes(); 178 | 179 | /* 7. Generate an octet string PS consisting of emLen - sLen - hLen - 2 180 | * zero octets. The length of PS may be 0. */ 181 | var ps = new forge.util.ByteBuffer(); 182 | ps.fillWithByte(0, emLen - sLen - hLen - 2); 183 | 184 | /* 8. Let DB = PS || 0x01 || salt; DB is an octet string of length 185 | * emLen - hLen - 1. */ 186 | ps.putByte(0x01); 187 | ps.putBytes(salt); 188 | var db = ps.getBytes(); 189 | 190 | /* 9. Let dbMask = MGF(H, emLen - hLen - 1). */ 191 | var maskLen = emLen - hLen - 1; 192 | var dbMask = mgf.generate(h, maskLen); 193 | 194 | /* 10. Let maskedDB = DB \xor dbMask. */ 195 | var maskedDB = ''; 196 | for(i = 0; i < maskLen; i ++) { 197 | maskedDB += String.fromCharCode(db.charCodeAt(i) ^ dbMask.charCodeAt(i)); 198 | } 199 | 200 | /* 11. Set the leftmost 8emLen - emBits bits of the leftmost octet in 201 | * maskedDB to zero. */ 202 | var mask = (0xFF00 >> (8 * emLen - emBits)) & 0xFF; 203 | maskedDB = String.fromCharCode(maskedDB.charCodeAt(0) & ~mask) + 204 | maskedDB.substr(1); 205 | 206 | /* 12. Let EM = maskedDB || H || 0xbc. 207 | * 13. Output EM. */ 208 | return maskedDB + h + String.fromCharCode(0xbc); 209 | }; 210 | 211 | return pssobj; 212 | }; 213 | 214 | })(); 215 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/random.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An API for getting cryptographically-secure random bytes. The bytes are 3 | * generated using the Fortuna algorithm devised by Bruce Schneier and 4 | * Niels Ferguson. 5 | * 6 | * Getting strong random bytes is not yet easy to do in javascript. The only 7 | * truish random entropy that can be collected is from the mouse, keyboard, or 8 | * from timing with respect to page loads, etc. This generator makes a poor 9 | * attempt at providing random bytes when those sources haven't yet provided 10 | * enough entropy to initially seed or to reseed the PRNG. 11 | * 12 | * @author Dave Longley 13 | * 14 | * Copyright (c) 2009-2012 Digital Bazaar, Inc. 15 | */ 16 | (function($) { 17 | 18 | // define forge 19 | if(typeof(window) !== 'undefined') { 20 | var forge = window.forge = window.forge || {}; 21 | forge.random = {}; 22 | } 23 | // define node.js module 24 | else if(typeof(module) !== 'undefined' && module.exports) { 25 | var forge = { 26 | aes: require('./aes'), 27 | md: require('./md'), 28 | prng: require('./prng'), 29 | util: require('./util') 30 | }; 31 | module.exports = forge.random = {}; 32 | } 33 | 34 | // the default prng plugin, uses AES-128 35 | var prng_aes = {}; 36 | var _prng_aes_output = new Array(4); 37 | var _prng_aes_buffer = forge.util.createBuffer(); 38 | prng_aes.formatKey = function(key) { 39 | // convert the key into 32-bit integers 40 | var tmp = forge.util.createBuffer(key); 41 | key = new Array(4); 42 | key[0] = tmp.getInt32(); 43 | key[1] = tmp.getInt32(); 44 | key[2] = tmp.getInt32(); 45 | key[3] = tmp.getInt32(); 46 | 47 | // return the expanded key 48 | return forge.aes._expandKey(key, false); 49 | }; 50 | prng_aes.formatSeed = function(seed) { 51 | // convert seed into 32-bit integers 52 | tmp = forge.util.createBuffer(seed); 53 | seed = new Array(4); 54 | seed[0] = tmp.getInt32(); 55 | seed[1] = tmp.getInt32(); 56 | seed[2] = tmp.getInt32(); 57 | seed[3] = tmp.getInt32(); 58 | return seed; 59 | }; 60 | prng_aes.cipher = function(key, seed) { 61 | forge.aes._updateBlock(key, seed, _prng_aes_output, false); 62 | _prng_aes_buffer.putInt32(_prng_aes_output[0]); 63 | _prng_aes_buffer.putInt32(_prng_aes_output[1]); 64 | _prng_aes_buffer.putInt32(_prng_aes_output[2]); 65 | _prng_aes_buffer.putInt32(_prng_aes_output[3]); 66 | return _prng_aes_buffer.getBytes(); 67 | }; 68 | prng_aes.increment = function(seed) { 69 | // FIXME: do we care about carry or signed issues? 70 | ++seed[3]; 71 | return seed; 72 | }; 73 | prng_aes.md = forge.md.sha1; 74 | 75 | // create default prng context 76 | var _ctx = forge.prng.create(prng_aes); 77 | 78 | // get load time entropy 79 | _ctx.collectInt(+new Date(), 32); 80 | 81 | // add some entropy from navigator object 82 | if(typeof(navigator) !== 'undefined') { 83 | var _navBytes = ''; 84 | for(var key in navigator) { 85 | try { 86 | if(typeof(navigator[key]) == 'string') { 87 | _navBytes += navigator[key]; 88 | } 89 | } catch(e) { 90 | /* Some navigator keys might not be accessible, e.g. the geolocation 91 | attribute throws an exception if touched in Mozilla chrome:// context. 92 | 93 | Silently ignore this and just don't use this as a source of entropy. */ 94 | } 95 | } 96 | _ctx.collect(_navBytes); 97 | _navBytes = null; 98 | } 99 | 100 | // add mouse and keyboard collectors if jquery is available 101 | if($) { 102 | // set up mouse entropy capture 103 | $().mousemove(function(e) { 104 | // add mouse coords 105 | _ctx.collectInt(e.clientX, 16); 106 | _ctx.collectInt(e.clientY, 16); 107 | }); 108 | 109 | // set up keyboard entropy capture 110 | $().keypress(function(e) { 111 | _ctx.collectInt(e.charCode, 8); 112 | }); 113 | } 114 | 115 | /* Random API */ 116 | 117 | /** 118 | * Gets random bytes. This method tries to make the bytes more 119 | * unpredictable by drawing from data that can be collected from 120 | * the user of the browser, ie mouse movement. 121 | * 122 | * @param count the number of random bytes to get. 123 | * 124 | * @return the random bytes in a string. 125 | */ 126 | forge.random.getBytes = function(count) { 127 | return _ctx.generate(count); 128 | }; 129 | 130 | })(typeof(jQuery) !== 'undefined' ? jQuery : null); 131 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/rc2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RC2 implementation. 3 | * 4 | * @author Stefan Siegl 5 | * 6 | * Copyright (c) 2012 Stefan Siegl 7 | * 8 | * Information on the RC2 cipher is available from RFC #2268, 9 | * http://www.ietf.org/rfc/rfc2268.txt 10 | */ 11 | 12 | (function() 13 | { 14 | 15 | // define forge 16 | var forge = {}; 17 | if(typeof(window) !== 'undefined') 18 | { 19 | forge = window.forge = window.forge || {}; 20 | forge.rc2 = {}; 21 | } 22 | // define node.js module 23 | else if(typeof(module) !== 'undefined' && module.exports) 24 | { 25 | forge = 26 | { 27 | util: require('./util') 28 | }; 29 | module.exports = forge.rc2 = {}; 30 | } 31 | 32 | var piTable = [ 33 | 0xd9, 0x78, 0xf9, 0xc4, 0x19, 0xdd, 0xb5, 0xed, 0x28, 0xe9, 0xfd, 0x79, 0x4a, 0xa0, 0xd8, 0x9d, 34 | 0xc6, 0x7e, 0x37, 0x83, 0x2b, 0x76, 0x53, 0x8e, 0x62, 0x4c, 0x64, 0x88, 0x44, 0x8b, 0xfb, 0xa2, 35 | 0x17, 0x9a, 0x59, 0xf5, 0x87, 0xb3, 0x4f, 0x13, 0x61, 0x45, 0x6d, 0x8d, 0x09, 0x81, 0x7d, 0x32, 36 | 0xbd, 0x8f, 0x40, 0xeb, 0x86, 0xb7, 0x7b, 0x0b, 0xf0, 0x95, 0x21, 0x22, 0x5c, 0x6b, 0x4e, 0x82, 37 | 0x54, 0xd6, 0x65, 0x93, 0xce, 0x60, 0xb2, 0x1c, 0x73, 0x56, 0xc0, 0x14, 0xa7, 0x8c, 0xf1, 0xdc, 38 | 0x12, 0x75, 0xca, 0x1f, 0x3b, 0xbe, 0xe4, 0xd1, 0x42, 0x3d, 0xd4, 0x30, 0xa3, 0x3c, 0xb6, 0x26, 39 | 0x6f, 0xbf, 0x0e, 0xda, 0x46, 0x69, 0x07, 0x57, 0x27, 0xf2, 0x1d, 0x9b, 0xbc, 0x94, 0x43, 0x03, 40 | 0xf8, 0x11, 0xc7, 0xf6, 0x90, 0xef, 0x3e, 0xe7, 0x06, 0xc3, 0xd5, 0x2f, 0xc8, 0x66, 0x1e, 0xd7, 41 | 0x08, 0xe8, 0xea, 0xde, 0x80, 0x52, 0xee, 0xf7, 0x84, 0xaa, 0x72, 0xac, 0x35, 0x4d, 0x6a, 0x2a, 42 | 0x96, 0x1a, 0xd2, 0x71, 0x5a, 0x15, 0x49, 0x74, 0x4b, 0x9f, 0xd0, 0x5e, 0x04, 0x18, 0xa4, 0xec, 43 | 0xc2, 0xe0, 0x41, 0x6e, 0x0f, 0x51, 0xcb, 0xcc, 0x24, 0x91, 0xaf, 0x50, 0xa1, 0xf4, 0x70, 0x39, 44 | 0x99, 0x7c, 0x3a, 0x85, 0x23, 0xb8, 0xb4, 0x7a, 0xfc, 0x02, 0x36, 0x5b, 0x25, 0x55, 0x97, 0x31, 45 | 0x2d, 0x5d, 0xfa, 0x98, 0xe3, 0x8a, 0x92, 0xae, 0x05, 0xdf, 0x29, 0x10, 0x67, 0x6c, 0xba, 0xc9, 46 | 0xd3, 0x00, 0xe6, 0xcf, 0xe1, 0x9e, 0xa8, 0x2c, 0x63, 0x16, 0x01, 0x3f, 0x58, 0xe2, 0x89, 0xa9, 47 | 0x0d, 0x38, 0x34, 0x1b, 0xab, 0x33, 0xff, 0xb0, 0xbb, 0x48, 0x0c, 0x5f, 0xb9, 0xb1, 0xcd, 0x2e, 48 | 0xc5, 0xf3, 0xdb, 0x47, 0xe5, 0xa5, 0x9c, 0x77, 0x0a, 0xa6, 0x20, 0x68, 0xfe, 0x7f, 0xc1, 0xad 49 | ]; 50 | 51 | var s = [1, 2, 3, 5]; 52 | 53 | 54 | /** 55 | * Rotate a word left by given number of bits. 56 | * 57 | * Bits that are shifted out on the left are put back in on the right 58 | * hand side. 59 | * 60 | * @param word The word to shift left. 61 | * @param bits The number of bits to shift by. 62 | * @return The rotated word. 63 | */ 64 | var rol = function(word, bits) { 65 | return ((word << bits) & 0xffff) | ((word & 0xffff) >> (16 - bits)); 66 | }; 67 | 68 | /** 69 | * Rotate a word right by given number of bits. 70 | * 71 | * Bits that are shifted out on the right are put back in on the left 72 | * hand side. 73 | * 74 | * @param word The word to shift right. 75 | * @param bits The number of bits to shift by. 76 | * @return The rotated word. 77 | */ 78 | var ror = function(word, bits) { 79 | return ((word & 0xffff) >> bits) | ((word << (16 - bits)) & 0xffff); 80 | }; 81 | 82 | 83 | 84 | 85 | /* RC2 API */ 86 | 87 | /** 88 | * Perform RC2 key expansion as per RFC #2268, section 2. 89 | * 90 | * @param key variable-length user key (between 1 and 128 bytes) 91 | * @param effKeyBits number of effective key bits (default: 128) 92 | * @return the expanded RC2 key (ByteBuffer of 128 bytes) 93 | */ 94 | forge.rc2.expandKey = function(key, effKeyBits) { 95 | if(key.constructor == String) { 96 | key = forge.util.createBuffer(key); 97 | } 98 | effKeyBits = effKeyBits || 128; 99 | 100 | /* introduce variables that match the names used in RFC #2268 */ 101 | var L = key; 102 | var T = key.length(); 103 | var T1 = effKeyBits; 104 | var T8 = Math.ceil(T1 / 8); 105 | var TM = 0xff >> (T1 & 0x07); 106 | var i; 107 | 108 | for(i = T; i < 128; i ++) { 109 | L.putByte(piTable[(L.at(i - 1) + L.at(i - T)) & 0xff]); 110 | } 111 | 112 | L.setAt(128 - T8, piTable[L.at(128 - T8) & TM]); 113 | 114 | for(i = 127 - T8; i >= 0; i --) { 115 | L.setAt(i, piTable[L.at(i + 1) ^ L.at(i + T8)]); 116 | } 117 | 118 | return L; 119 | }; 120 | 121 | 122 | /** 123 | * Creates a RC2 cipher object. 124 | * 125 | * @param key the symmetric key to use (as base for key generation). 126 | * @param bits the number of effective key bits. 127 | * @param encrypt false for decryption, true for encryption. 128 | * 129 | * @return the cipher. 130 | */ 131 | var createCipher = function(key, bits, encrypt) 132 | { 133 | var _finish = false, _input = null, _output = null, _iv = null; 134 | var mixRound, mashRound; 135 | var i, j, K = []; 136 | 137 | /* Expand key and fill into K[] Array */ 138 | key = forge.rc2.expandKey(key, bits); 139 | for(i = 0; i < 64; i ++) { 140 | K.push(key.getInt16Le()); 141 | } 142 | 143 | if(encrypt) { 144 | /** 145 | * Perform one mixing round "in place". 146 | * 147 | * @param R Array of four words to perform mixing on. 148 | */ 149 | mixRound = function(R) { 150 | for(i = 0; i < 4; i++) { 151 | R[i] += K[j] + (R[(i + 3) % 4] & R[(i + 2) % 4]) + 152 | ((~R[(i + 3) % 4]) & R[(i + 1) % 4]); 153 | R[i] = rol(R[i], s[i]); 154 | j ++; 155 | } 156 | }; 157 | 158 | /** 159 | * Perform one mashing round "in place". 160 | * 161 | * @param R Array of four words to perform mashing on. 162 | */ 163 | mashRound = function(R) { 164 | for(i = 0; i < 4; i ++) { 165 | R[i] += K[R[(i + 3) % 4] & 63]; 166 | } 167 | }; 168 | } else { 169 | /** 170 | * Perform one r-mixing round "in place". 171 | * 172 | * @param R Array of four words to perform mixing on. 173 | */ 174 | mixRound = function(R) { 175 | for(i = 3; i >= 0; i--) { 176 | R[i] = ror(R[i], s[i]); 177 | R[i] -= K[j] + (R[(i + 3) % 4] & R[(i + 2) % 4]) + 178 | ((~R[(i + 3) % 4]) & R[(i + 1) % 4]); 179 | j --; 180 | } 181 | }; 182 | 183 | /** 184 | * Perform one r-mashing round "in place". 185 | * 186 | * @param R Array of four words to perform mashing on. 187 | */ 188 | mashRound = function(R) { 189 | for(i = 3; i >= 0; i--) { 190 | R[i] -= K[R[(i + 3) % 4] & 63]; 191 | } 192 | }; 193 | } 194 | 195 | /** 196 | * Run the specified cipher execution plan. 197 | * 198 | * This function takes four words from the input buffer, applies the IV on 199 | * it (if requested) and runs the provided execution plan. 200 | * 201 | * The plan must be put together in form of a array of arrays. Where the 202 | * outer one is simply a list of steps to perform and the inner one needs 203 | * to have two elements: the first one telling how many rounds to perform, 204 | * the second one telling what to do (i.e. the function to call). 205 | * 206 | * @param {Array} plan The plan to execute. 207 | */ 208 | var runPlan = function(plan) { 209 | var R = []; 210 | 211 | /* Get data from input buffer and fill the four words into R */ 212 | for(i = 0; i < 4; i ++) { 213 | var val = _input.getInt16Le(); 214 | 215 | if(_iv !== null) { 216 | if(encrypt) { 217 | /* We're encrypting, apply the IV first. */ 218 | val ^= _iv.getInt16Le(); 219 | } else { 220 | /* We're decryption, keep cipher text for next block. */ 221 | _iv.putInt16Le(val); 222 | } 223 | } 224 | 225 | R.push(val & 0xffff); 226 | } 227 | 228 | /* Reset global "j" variable as per spec. */ 229 | j = encrypt ? 0 : 63; 230 | 231 | /* Run execution plan. */ 232 | for(var ptr = 0; ptr < plan.length; ptr ++) { 233 | for(var ctr = 0; ctr < plan[ptr][0]; ctr ++) { 234 | plan[ptr][1](R); 235 | } 236 | } 237 | 238 | /* Write back result to output buffer. */ 239 | for(i = 0; i < 4; i ++) { 240 | if(_iv !== null) { 241 | if(encrypt) { 242 | /* We're encrypting in CBC-mode, feed back encrypted bytes into 243 | IV buffer to carry it forward to next block. */ 244 | _iv.putInt16Le(R[i]); 245 | } else { 246 | R[i] ^= _iv.getInt16Le(); 247 | } 248 | } 249 | 250 | _output.putInt16Le(R[i]); 251 | } 252 | }; 253 | 254 | 255 | /* Create cipher object */ 256 | var cipher = null; 257 | cipher = { 258 | /** 259 | * Starts or restarts the encryption or decryption process, whichever 260 | * was previously configured. 261 | * 262 | * To use the cipher in CBC mode, iv may be given either as a string 263 | * of bytes, or as a byte buffer. For ECB mode, give null as iv. 264 | * 265 | * @param iv the initialization vector to use, null for ECB mode. 266 | * @param output the output the buffer to write to, null to create one. 267 | */ 268 | start: function(iv, output) { 269 | if(iv) { 270 | /* CBC mode */ 271 | if(key.constructor == String && iv.length == 8) { 272 | iv = forge.util.createBuffer(iv); 273 | } 274 | } 275 | 276 | _finish = false; 277 | _input = forge.util.createBuffer(); 278 | _output = output || new forge.util.createBuffer(); 279 | _iv = iv; 280 | 281 | cipher.output = _output; 282 | }, 283 | 284 | /** 285 | * Updates the next block. 286 | * 287 | * @param input the buffer to read from. 288 | */ 289 | update: function(input) { 290 | if(!_finish) { 291 | // not finishing, so fill the input buffer with more input 292 | _input.putBuffer(input); 293 | } 294 | 295 | while(_input.length() >= 8) { 296 | runPlan([ 297 | [ 5, mixRound ], 298 | [ 1, mashRound ], 299 | [ 6, mixRound ], 300 | [ 1, mashRound ], 301 | [ 5, mixRound ] 302 | ]); 303 | } 304 | }, 305 | 306 | /** 307 | * Finishes encrypting or decrypting. 308 | * 309 | * @param pad a padding function to use, null for PKCS#7 padding, 310 | * signature(blockSize, buffer, decrypt). 311 | * 312 | * @return true if successful, false on error. 313 | */ 314 | finish: function(pad) { 315 | var rval = true; 316 | 317 | if(encrypt) { 318 | if(pad) { 319 | rval = pad(8, _input, !encrypt); 320 | } else { 321 | // add PKCS#7 padding to block (each pad byte is the 322 | // value of the number of pad bytes) 323 | var padding = (_input.length() == 8) ? 8 : (8 - _input.length()); 324 | _input.fillWithByte(padding, padding); 325 | } 326 | } 327 | 328 | if(rval) { 329 | // do final update 330 | _finish = true; 331 | cipher.update(); 332 | } 333 | 334 | if(!encrypt) { 335 | // check for error: input data not a multiple of block size 336 | rval = (_input.length() === 0); 337 | if(rval) { 338 | if(pad) { 339 | rval = pad(8, _output, !encrypt); 340 | } else { 341 | // ensure padding byte count is valid 342 | var len = _output.length(); 343 | var count = _output.at(len - 1); 344 | 345 | if(count > len) { 346 | rval = false; 347 | } else { 348 | // trim off padding bytes 349 | _output.truncate(count); 350 | } 351 | } 352 | } 353 | } 354 | 355 | return rval; 356 | } 357 | }; 358 | 359 | return cipher; 360 | }; 361 | 362 | 363 | /** 364 | * Creates an RC2 cipher object to encrypt data in ECB or CBC mode using the 365 | * given symmetric key. The output will be stored in the 'output' member 366 | * of the returned cipher. 367 | * 368 | * The key and iv may be given as a string of bytes or a byte buffer. 369 | * The cipher is initialized to use 128 effective key bits. 370 | * 371 | * @param key the symmetric key to use. 372 | * @param iv the initialization vector to use. 373 | * @param output the buffer to write to, null to create one. 374 | * 375 | * @return the cipher. 376 | */ 377 | forge.rc2.startEncrypting = function(key, iv, output) { 378 | var cipher = forge.rc2.createEncryptionCipher(key, 128); 379 | cipher.start(iv, output); 380 | return cipher; 381 | }; 382 | 383 | /** 384 | * Creates an RC2 cipher object to encrypt data in ECB or CBC mode using the 385 | * given symmetric key. 386 | * 387 | * The key may be given as a string of bytes or a byte buffer. 388 | * 389 | * To start encrypting call start() on the cipher with an iv and optional 390 | * output buffer. 391 | * 392 | * @param key the symmetric key to use. 393 | * 394 | * @return the cipher. 395 | */ 396 | forge.rc2.createEncryptionCipher = function(key, bits) { 397 | return createCipher(key, bits, true); 398 | }; 399 | 400 | /** 401 | * Creates an RC2 cipher object to decrypt data in ECB or CBC mode using the 402 | * given symmetric key. The output will be stored in the 'output' member 403 | * of the returned cipher. 404 | * 405 | * The key and iv may be given as a string of bytes or a byte buffer. 406 | * The cipher is initialized to use 128 effective key bits. 407 | * 408 | * @param key the symmetric key to use. 409 | * @param iv the initialization vector to use. 410 | * @param output the buffer to write to, null to create one. 411 | * 412 | * @return the cipher. 413 | */ 414 | forge.rc2.startDecrypting = function(key, iv, output) { 415 | var cipher = forge.rc2.createDecryptionCipher(key, 128); 416 | cipher.start(iv, output); 417 | return cipher; 418 | }; 419 | 420 | /** 421 | * Creates an RC2 cipher object to decrypt data in ECB or CBC mode using the 422 | * given symmetric key. 423 | * 424 | * The key may be given as a string of bytes or a byte buffer. 425 | * 426 | * To start decrypting call start() on the cipher with an iv and optional 427 | * output buffer. 428 | * 429 | * @param key the symmetric key to use. 430 | * 431 | * @return the cipher. 432 | */ 433 | forge.rc2.createDecryptionCipher = function(key, bits) { 434 | return createCipher(key, bits, false); 435 | }; 436 | })(); 437 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/sha1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Secure Hash Algorithm with 160-bit digest (SHA-1) implementation. 3 | * 4 | * This implementation is currently limited to message lengths (in bytes) that 5 | * are up to 32-bits in size. 6 | * 7 | * @author Dave Longley 8 | * 9 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. 10 | */ 11 | (function() { 12 | 13 | var sha1 = {}; 14 | 15 | // define forge 16 | if(typeof(window) !== 'undefined') { 17 | var forge = window.forge = window.forge || {}; 18 | } 19 | // define node.js module 20 | else if(typeof(module) !== 'undefined' && module.exports) { 21 | var forge = { 22 | util: require('./util') 23 | }; 24 | module.exports = sha1; 25 | } 26 | forge.md = forge.md || {}; 27 | forge.md.algorithms = forge.md.algorithms || {}; 28 | forge.md.sha1 = forge.md.algorithms['sha1'] = sha1; 29 | 30 | // sha-1 padding bytes not initialized yet 31 | var _padding = null; 32 | var _initialized = false; 33 | 34 | /** 35 | * Initializes the constant tables. 36 | */ 37 | var _init = function() { 38 | // create padding 39 | _padding = String.fromCharCode(128); 40 | _padding += forge.util.fillString(String.fromCharCode(0x00), 64); 41 | 42 | // now initialized 43 | _initialized = true; 44 | }; 45 | 46 | /** 47 | * Updates a SHA-1 state with the given byte buffer. 48 | * 49 | * @param s the SHA-1 state to update. 50 | * @param w the array to use to store words. 51 | * @param bytes the byte buffer to update with. 52 | */ 53 | var _update = function(s, w, bytes) { 54 | // consume 512 bit (64 byte) chunks 55 | var t, a, b, c, d, e, f, i; 56 | var len = bytes.length(); 57 | while(len >= 64) { 58 | // the w array will be populated with sixteen 32-bit big-endian words 59 | // and then extended into 80 32-bit words according to SHA-1 algorithm 60 | // and for 32-79 using Max Locktyukhin's optimization 61 | 62 | // initialize hash value for this chunk 63 | a = s.h0; 64 | b = s.h1; 65 | c = s.h2; 66 | d = s.h3; 67 | e = s.h4; 68 | 69 | // round 1 70 | for(i = 0; i < 16; ++i) { 71 | t = bytes.getInt32(); 72 | w[i] = t; 73 | f = d ^ (b & (c ^ d)); 74 | t = ((a << 5) | (a >>> 27)) + f + e + 0x5A827999 + t; 75 | e = d; 76 | d = c; 77 | c = (b << 30) | (b >>> 2); 78 | b = a; 79 | a = t; 80 | } 81 | for(; i < 20; ++i) { 82 | t = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]); 83 | t = (t << 1) | (t >>> 31); 84 | w[i] = t; 85 | f = d ^ (b & (c ^ d)); 86 | t = ((a << 5) | (a >>> 27)) + f + e + 0x5A827999 + t; 87 | e = d; 88 | d = c; 89 | c = (b << 30) | (b >>> 2); 90 | b = a; 91 | a = t; 92 | } 93 | // round 2 94 | for(; i < 32; ++i) { 95 | t = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]); 96 | t = (t << 1) | (t >>> 31); 97 | w[i] = t; 98 | f = b ^ c ^ d; 99 | t = ((a << 5) | (a >>> 27)) + f + e + 0x6ED9EBA1 + t; 100 | e = d; 101 | d = c; 102 | c = (b << 30) | (b >>> 2); 103 | b = a; 104 | a = t; 105 | } 106 | for(; i < 40; ++i) { 107 | t = (w[i - 6] ^ w[i - 16] ^ w[i - 28] ^ w[i - 32]); 108 | t = (t << 2) | (t >>> 30); 109 | w[i] = t; 110 | f = b ^ c ^ d; 111 | t = ((a << 5) | (a >>> 27)) + f + e + 0x6ED9EBA1 + t; 112 | e = d; 113 | d = c; 114 | c = (b << 30) | (b >>> 2); 115 | b = a; 116 | a = t; 117 | } 118 | // round 3 119 | for(; i < 60; ++i) { 120 | t = (w[i - 6] ^ w[i - 16] ^ w[i - 28] ^ w[i - 32]); 121 | t = (t << 2) | (t >>> 30); 122 | w[i] = t; 123 | f = (b & c) | (d & (b ^ c)); 124 | t = ((a << 5) | (a >>> 27)) + f + e + 0x8F1BBCDC + t; 125 | e = d; 126 | d = c; 127 | c = (b << 30) | (b >>> 2); 128 | b = a; 129 | a = t; 130 | } 131 | // round 4 132 | for(; i < 80; ++i) { 133 | t = (w[i - 6] ^ w[i - 16] ^ w[i - 28] ^ w[i - 32]); 134 | t = (t << 2) | (t >>> 30); 135 | w[i] = t; 136 | f = b ^ c ^ d; 137 | t = ((a << 5) | (a >>> 27)) + f + e + 0xCA62C1D6 + t; 138 | e = d; 139 | d = c; 140 | c = (b << 30) | (b >>> 2); 141 | b = a; 142 | a = t; 143 | } 144 | 145 | // update hash state 146 | s.h0 += a; 147 | s.h1 += b; 148 | s.h2 += c; 149 | s.h3 += d; 150 | s.h4 += e; 151 | 152 | len -= 64; 153 | } 154 | }; 155 | 156 | /** 157 | * Creates a SHA-1 message digest object. 158 | * 159 | * @return a message digest object. 160 | */ 161 | sha1.create = function() { 162 | // do initialization as necessary 163 | if(!_initialized) { 164 | _init(); 165 | } 166 | 167 | // SHA-1 state contains five 32-bit integers 168 | var _state = null; 169 | 170 | // input buffer 171 | var _input = forge.util.createBuffer(); 172 | 173 | // used for word storage 174 | var _w = new Array(80); 175 | 176 | // message digest object 177 | var md = { 178 | algorithm: 'sha1', 179 | blockLength: 64, 180 | digestLength: 20, 181 | // length of message so far (does not including padding) 182 | messageLength: 0 183 | }; 184 | 185 | /** 186 | * Starts the digest. 187 | */ 188 | md.start = function() { 189 | md.messageLength = 0; 190 | _input = forge.util.createBuffer(); 191 | _state = { 192 | h0: 0x67452301, 193 | h1: 0xEFCDAB89, 194 | h2: 0x98BADCFE, 195 | h3: 0x10325476, 196 | h4: 0xC3D2E1F0 197 | }; 198 | }; 199 | // start digest automatically for first time 200 | md.start(); 201 | 202 | /** 203 | * Updates the digest with the given message input. The given input can 204 | * treated as raw input (no encoding will be applied) or an encoding of 205 | * 'utf8' maybe given to encode the input using UTF-8. 206 | * 207 | * @param msg the message input to update with. 208 | * @param encoding the encoding to use (default: 'raw', other: 'utf8'). 209 | */ 210 | md.update = function(msg, encoding) { 211 | if(encoding === 'utf8') { 212 | msg = forge.util.encodeUtf8(msg); 213 | } 214 | 215 | // update message length 216 | md.messageLength += msg.length; 217 | 218 | // add bytes to input buffer 219 | _input.putBytes(msg); 220 | 221 | // process bytes 222 | _update(_state, _w, _input); 223 | 224 | // compact input buffer every 2K or if empty 225 | if(_input.read > 2048 || _input.length() === 0) { 226 | _input.compact(); 227 | } 228 | }; 229 | 230 | /** 231 | * Produces the digest. 232 | * 233 | * @return a byte buffer containing the digest value. 234 | */ 235 | md.digest = function() { 236 | /* Note: Here we copy the remaining bytes in the input buffer and 237 | add the appropriate SHA-1 padding. Then we do the final update 238 | on a copy of the state so that if the user wants to get 239 | intermediate digests they can do so. */ 240 | 241 | /* Determine the number of bytes that must be added to the message 242 | to ensure its length is congruent to 448 mod 512. In other words, 243 | a 64-bit integer that gives the length of the message will be 244 | appended to the message and whatever the length of the message is 245 | plus 64 bits must be a multiple of 512. So the length of the 246 | message must be congruent to 448 mod 512 because 512 - 64 = 448. 247 | 248 | In order to fill up the message length it must be filled with 249 | padding that begins with 1 bit followed by all 0 bits. Padding 250 | must *always* be present, so if the message length is already 251 | congruent to 448 mod 512, then 512 padding bits must be added. */ 252 | 253 | // 512 bits == 64 bytes, 448 bits == 56 bytes, 64 bits = 8 bytes 254 | // _padding starts with 1 byte with first bit is set in it which 255 | // is byte value 128, then there may be up to 63 other pad bytes 256 | var len = md.messageLength; 257 | var padBytes = forge.util.createBuffer(); 258 | padBytes.putBytes(_input.bytes()); 259 | padBytes.putBytes(_padding.substr(0, 64 - ((len + 8) % 64))); 260 | 261 | /* Now append length of the message. The length is appended in bits 262 | as a 64-bit number in big-endian order. Since we store the length 263 | in bytes, we must multiply it by 8 (or left shift by 3). So here 264 | store the high 3 bits in the low end of the first 32-bits of the 265 | 64-bit number and the lower 5 bits in the high end of the second 266 | 32-bits. */ 267 | padBytes.putInt32((len >>> 29) & 0xFF); 268 | padBytes.putInt32((len << 3) & 0xFFFFFFFF); 269 | var s2 = { 270 | h0: _state.h0, 271 | h1: _state.h1, 272 | h2: _state.h2, 273 | h3: _state.h3, 274 | h4: _state.h4 275 | }; 276 | _update(s2, _w, padBytes); 277 | var rval = forge.util.createBuffer(); 278 | rval.putInt32(s2.h0); 279 | rval.putInt32(s2.h1); 280 | rval.putInt32(s2.h2); 281 | rval.putInt32(s2.h3); 282 | rval.putInt32(s2.h4); 283 | return rval; 284 | }; 285 | 286 | return md; 287 | }; 288 | 289 | })(); 290 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/forge/sha256.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Secure Hash Algorithm with 256-bit digest (SHA-256) implementation. 3 | * 4 | * See FIPS 180-2 for details. 5 | * 6 | * This implementation is currently limited to message lengths (in bytes) that 7 | * are up to 32-bits in size. 8 | * 9 | * @author Dave Longley 10 | * 11 | * Copyright (c) 2010-2012 Digital Bazaar, Inc. 12 | */ 13 | (function() { 14 | 15 | var sha256 = {}; 16 | 17 | // define forge 18 | if(typeof(window) !== 'undefined') { 19 | var forge = window.forge = window.forge || {}; 20 | } 21 | else if(typeof(module) !== 'undefined' && module.exports) { 22 | var forge = { 23 | util: require('./util') 24 | }; 25 | module.exports = sha256 = {}; 26 | } 27 | forge.md = forge.md || {}; 28 | forge.md.algorithms = forge.md.algorithms || {}; 29 | forge.md.sha256 = forge.md.algorithms['sha256'] = sha256; 30 | 31 | // sha-256 padding bytes not initialized yet 32 | var _padding = null; 33 | var _initialized = false; 34 | 35 | // table of constants 36 | var _k = null; 37 | 38 | /** 39 | * Initializes the constant tables. 40 | */ 41 | var _init = function() { 42 | // create padding 43 | _padding = String.fromCharCode(128); 44 | _padding += forge.util.fillString(String.fromCharCode(0x00), 64); 45 | 46 | // create K table for SHA-256 47 | _k = [ 48 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 49 | 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 50 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 51 | 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 52 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 53 | 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 54 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 55 | 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 56 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 57 | 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 58 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 59 | 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 60 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 61 | 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 62 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 63 | 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]; 64 | 65 | // now initialized 66 | _initialized = true; 67 | }; 68 | 69 | /** 70 | * Updates a SHA-256 state with the given byte buffer. 71 | * 72 | * @param s the SHA-256 state to update. 73 | * @param w the array to use to store words. 74 | * @param bytes the byte buffer to update with. 75 | */ 76 | var _update = function(s, w, bytes) { 77 | // consume 512 bit (64 byte) chunks 78 | var t1, t2, s0, s1, ch, maj, i, a, b, c, d, e, f, g, h; 79 | var len = bytes.length(); 80 | while(len >= 64) { 81 | // the w array will be populated with sixteen 32-bit big-endian words 82 | // and then extended into 64 32-bit words according to SHA-256 83 | for(i = 0; i < 16; ++i) { 84 | w[i] = bytes.getInt32(); 85 | } 86 | for(; i < 64; ++i) { 87 | // XOR word 2 words ago rot right 17, rot right 19, shft right 10 88 | t1 = w[i - 2]; 89 | t1 = 90 | ((t1 >>> 17) | (t1 << 15)) ^ 91 | ((t1 >>> 19) | (t1 << 13)) ^ 92 | (t1 >>> 10); 93 | // XOR word 15 words ago rot right 7, rot right 18, shft right 3 94 | t2 = w[i - 15]; 95 | t2 = 96 | ((t2 >>> 7) | (t2 << 25)) ^ 97 | ((t2 >>> 18) | (t2 << 14)) ^ 98 | (t2 >>> 3); 99 | // sum(t1, word 7 ago, t2, word 16 ago) modulo 2^32 100 | w[i] = (t1 + w[i - 7] + t2 + w[i - 16]) & 0xFFFFFFFF; 101 | } 102 | 103 | // initialize hash value for this chunk 104 | a = s.h0; 105 | b = s.h1; 106 | c = s.h2; 107 | d = s.h3; 108 | e = s.h4; 109 | f = s.h5; 110 | g = s.h6; 111 | h = s.h7; 112 | 113 | // round function 114 | for(i = 0; i < 64; ++i) { 115 | // Sum1(e) 116 | s1 = 117 | ((e >>> 6) | (e << 26)) ^ 118 | ((e >>> 11) | (e << 21)) ^ 119 | ((e >>> 25) | (e << 7)); 120 | // Ch(e, f, g) (optimized the same way as SHA-1) 121 | ch = g ^ (e & (f ^ g)); 122 | // Sum0(a) 123 | s0 = 124 | ((a >>> 2) | (a << 30)) ^ 125 | ((a >>> 13) | (a << 19)) ^ 126 | ((a >>> 22) | (a << 10)); 127 | // Maj(a, b, c) (optimized the same way as SHA-1) 128 | maj = (a & b) | (c & (a ^ b)); 129 | 130 | // main algorithm 131 | t1 = h + s1 + ch + _k[i] + w[i]; 132 | t2 = s0 + maj; 133 | h = g; 134 | g = f; 135 | f = e; 136 | e = (d + t1) & 0xFFFFFFFF; 137 | d = c; 138 | c = b; 139 | b = a; 140 | a = (t1 + t2) & 0xFFFFFFFF; 141 | } 142 | 143 | // update hash state 144 | s.h0 = (s.h0 + a) & 0xFFFFFFFF; 145 | s.h1 = (s.h1 + b) & 0xFFFFFFFF; 146 | s.h2 = (s.h2 + c) & 0xFFFFFFFF; 147 | s.h3 = (s.h3 + d) & 0xFFFFFFFF; 148 | s.h4 = (s.h4 + e) & 0xFFFFFFFF; 149 | s.h5 = (s.h5 + f) & 0xFFFFFFFF; 150 | s.h6 = (s.h6 + g) & 0xFFFFFFFF; 151 | s.h7 = (s.h7 + h) & 0xFFFFFFFF; 152 | len -= 64; 153 | } 154 | }; 155 | 156 | /** 157 | * Creates a SHA-256 message digest object. 158 | * 159 | * @return a message digest object. 160 | */ 161 | sha256.create = function() { 162 | // do initialization as necessary 163 | if(!_initialized) { 164 | _init(); 165 | } 166 | 167 | // SHA-256 state contains eight 32-bit integers 168 | var _state = null; 169 | 170 | // input buffer 171 | var _input = forge.util.createBuffer(); 172 | 173 | // used for word storage 174 | var _w = new Array(64); 175 | 176 | // message digest object 177 | var md = { 178 | algorithm: 'sha256', 179 | blockLength: 64, 180 | digestLength: 32, 181 | // length of message so far (does not including padding) 182 | messageLength: 0 183 | }; 184 | 185 | /** 186 | * Starts the digest. 187 | */ 188 | md.start = function() { 189 | md.messageLength = 0; 190 | _input = forge.util.createBuffer(); 191 | _state = { 192 | h0: 0x6A09E667, 193 | h1: 0xBB67AE85, 194 | h2: 0x3C6EF372, 195 | h3: 0xA54FF53A, 196 | h4: 0x510E527F, 197 | h5: 0x9B05688C, 198 | h6: 0x1F83D9AB, 199 | h7: 0x5BE0CD19 200 | }; 201 | }; 202 | // start digest automatically for first time 203 | md.start(); 204 | 205 | /** 206 | * Updates the digest with the given message input. The given input can 207 | * treated as raw input (no encoding will be applied) or an encoding of 208 | * 'utf8' maybe given to encode the input using UTF-8. 209 | * 210 | * @param msg the message input to update with. 211 | * @param encoding the encoding to use (default: 'raw', other: 'utf8'). 212 | */ 213 | md.update = function(msg, encoding) { 214 | if(encoding === 'utf8') { 215 | msg = forge.util.encodeUtf8(msg); 216 | } 217 | 218 | // update message length 219 | md.messageLength += msg.length; 220 | 221 | // add bytes to input buffer 222 | _input.putBytes(msg); 223 | 224 | // process bytes 225 | _update(_state, _w, _input); 226 | 227 | // compact input buffer every 2K or if empty 228 | if(_input.read > 2048 || _input.length() === 0) { 229 | _input.compact(); 230 | } 231 | }; 232 | 233 | /** 234 | * Produces the digest. 235 | * 236 | * @return a byte buffer containing the digest value. 237 | */ 238 | md.digest = function() { 239 | /* Note: Here we copy the remaining bytes in the input buffer and 240 | add the appropriate SHA-256 padding. Then we do the final update 241 | on a copy of the state so that if the user wants to get 242 | intermediate digests they can do so. */ 243 | 244 | /* Determine the number of bytes that must be added to the message 245 | to ensure its length is congruent to 448 mod 512. In other words, 246 | a 64-bit integer that gives the length of the message will be 247 | appended to the message and whatever the length of the message is 248 | plus 64 bits must be a multiple of 512. So the length of the 249 | message must be congruent to 448 mod 512 because 512 - 64 = 448. 250 | 251 | In order to fill up the message length it must be filled with 252 | padding that begins with 1 bit followed by all 0 bits. Padding 253 | must *always* be present, so if the message length is already 254 | congruent to 448 mod 512, then 512 padding bits must be added. */ 255 | 256 | // 512 bits == 64 bytes, 448 bits == 56 bytes, 64 bits = 8 bytes 257 | // _padding starts with 1 byte with first bit is set in it which 258 | // is byte value 128, then there may be up to 63 other pad bytes 259 | var len = md.messageLength; 260 | var padBytes = forge.util.createBuffer(); 261 | padBytes.putBytes(_input.bytes()); 262 | padBytes.putBytes(_padding.substr(0, 64 - ((len + 8) % 64))); 263 | 264 | /* Now append length of the message. The length is appended in bits 265 | as a 64-bit number in big-endian order. Since we store the length 266 | in bytes, we must multiply it by 8 (or left shift by 3). So here 267 | store the high 3 bits in the low end of the first 32-bits of the 268 | 64-bit number and the lower 5 bits in the high end of the second 269 | 32-bits. */ 270 | padBytes.putInt32((len >>> 29) & 0xFF); 271 | padBytes.putInt32((len << 3) & 0xFFFFFFFF); 272 | var s2 = { 273 | h0: _state.h0, 274 | h1: _state.h1, 275 | h2: _state.h2, 276 | h3: _state.h3, 277 | h4: _state.h4, 278 | h5: _state.h5, 279 | h6: _state.h6, 280 | h7: _state.h7 281 | }; 282 | _update(s2, _w, padBytes); 283 | var rval = forge.util.createBuffer(); 284 | rval.putInt32(s2.h0); 285 | rval.putInt32(s2.h1); 286 | rval.putInt32(s2.h2); 287 | rval.putInt32(s2.h3); 288 | rval.putInt32(s2.h4); 289 | rval.putInt32(s2.h5); 290 | rval.putInt32(s2.h6); 291 | rval.putInt32(s2.h7); 292 | return rval; 293 | }; 294 | 295 | return md; 296 | }; 297 | 298 | })(); 299 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/form2js.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Maxim Vasiliev 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | * 22 | * @author Maxim Vasiliev 23 | * Date: 09.09.2010 24 | * Time: 19:02:33 25 | */ 26 | 27 | 28 | var form2js = (function() 29 | { 30 | "use strict"; 31 | 32 | /** 33 | * Returns form values represented as Javascript object 34 | * "name" attribute defines structure of resulting object 35 | * 36 | * @param rootNode {Element|String} root form element (or it's id) or array of root elements 37 | * @param delimiter {String} structure parts delimiter defaults to '.' 38 | * @param skipEmpty {Boolean} should skip empty text values, defaults to true 39 | * @param nodeCallback {Function} custom function to get node value 40 | * @param useIdIfEmptyName {Boolean} if true value of id attribute of field will be used if name of field is empty 41 | */ 42 | function form2js(rootNode, delimiter, skipEmpty, nodeCallback, useIdIfEmptyName) 43 | { 44 | if (typeof skipEmpty == 'undefined' || skipEmpty == null) skipEmpty = true; 45 | if (typeof delimiter == 'undefined' || delimiter == null) delimiter = '.'; 46 | if (arguments.length < 5) useIdIfEmptyName = false; 47 | 48 | rootNode = typeof rootNode == 'string' ? document.getElementById(rootNode) : rootNode; 49 | 50 | var formValues = [], 51 | currNode, 52 | i = 0; 53 | 54 | /* If rootNode is array - combine values */ 55 | if (rootNode.constructor == Array || (typeof NodeList != "undefined" && rootNode.constructor == NodeList)) 56 | { 57 | while(currNode = rootNode[i++]) 58 | { 59 | formValues = formValues.concat(getFormValues(currNode, nodeCallback, useIdIfEmptyName)); 60 | } 61 | } 62 | else 63 | { 64 | formValues = getFormValues(rootNode, nodeCallback, useIdIfEmptyName); 65 | } 66 | 67 | return processNameValues(formValues, skipEmpty, delimiter); 68 | } 69 | 70 | /** 71 | * Processes collection of { name: 'name', value: 'value' } objects. 72 | * @param nameValues 73 | * @param skipEmpty if true skips elements with value == '' or value == null 74 | * @param delimiter 75 | */ 76 | function processNameValues(nameValues, skipEmpty, delimiter) 77 | { 78 | var result = {}, 79 | arrays = {}, 80 | i, j, k, l, 81 | value, 82 | nameParts, 83 | currResult, 84 | arrNameFull, 85 | arrName, 86 | arrIdx, 87 | namePart, 88 | name, 89 | _nameParts; 90 | 91 | for (i = 0; i < nameValues.length; i++) 92 | { 93 | value = nameValues[i].value; 94 | 95 | if (skipEmpty && (value === '' || value === null)) continue; 96 | 97 | name = nameValues[i].name; 98 | _nameParts = name.split(delimiter); 99 | nameParts = []; 100 | currResult = result; 101 | arrNameFull = ''; 102 | 103 | for(j = 0; j < _nameParts.length; j++) 104 | { 105 | namePart = _nameParts[j].split(']['); 106 | if (namePart.length > 1) 107 | { 108 | for(k = 0; k < namePart.length; k++) 109 | { 110 | if (k == 0) 111 | { 112 | namePart[k] = namePart[k] + ']'; 113 | } 114 | else if (k == namePart.length - 1) 115 | { 116 | namePart[k] = '[' + namePart[k]; 117 | } 118 | else 119 | { 120 | namePart[k] = '[' + namePart[k] + ']'; 121 | } 122 | 123 | arrIdx = namePart[k].match(/([a-z_]+)?\[([a-z_][a-z0-9_]+?)\]/i); 124 | if (arrIdx) 125 | { 126 | for(l = 1; l < arrIdx.length; l++) 127 | { 128 | if (arrIdx[l]) nameParts.push(arrIdx[l]); 129 | } 130 | } 131 | else{ 132 | nameParts.push(namePart[k]); 133 | } 134 | } 135 | } 136 | else 137 | nameParts = nameParts.concat(namePart); 138 | } 139 | 140 | for (j = 0; j < nameParts.length; j++) 141 | { 142 | namePart = nameParts[j]; 143 | 144 | if (namePart.indexOf('[]') > -1 && j == nameParts.length - 1) 145 | { 146 | arrName = namePart.substr(0, namePart.indexOf('[')); 147 | arrNameFull += arrName; 148 | 149 | if (!currResult[arrName]) currResult[arrName] = []; 150 | currResult[arrName].push(value); 151 | } 152 | else if (namePart.indexOf('[') > -1) 153 | { 154 | arrName = namePart.substr(0, namePart.indexOf('[')); 155 | arrIdx = namePart.replace(/(^([a-z_]+)?\[)|(\]$)/gi, ''); 156 | 157 | /* Unique array name */ 158 | arrNameFull += '_' + arrName + '_' + arrIdx; 159 | 160 | /* 161 | * Because arrIdx in field name can be not zero-based and step can be 162 | * other than 1, we can't use them in target array directly. 163 | * Instead we're making a hash where key is arrIdx and value is a reference to 164 | * added array element 165 | */ 166 | 167 | if (!arrays[arrNameFull]) arrays[arrNameFull] = {}; 168 | if (arrName != '' && !currResult[arrName]) currResult[arrName] = []; 169 | 170 | if (j == nameParts.length - 1) 171 | { 172 | if (arrName == '') 173 | { 174 | currResult.push(value); 175 | arrays[arrNameFull][arrIdx] = currResult[currResult.length - 1]; 176 | } 177 | else 178 | { 179 | currResult[arrName].push(value); 180 | arrays[arrNameFull][arrIdx] = currResult[arrName][currResult[arrName].length - 1]; 181 | } 182 | } 183 | else 184 | { 185 | if (!arrays[arrNameFull][arrIdx]) 186 | { 187 | if ((/^[a-z_]+\[?/i).test(nameParts[j+1])) currResult[arrName].push({}); 188 | else currResult[arrName].push([]); 189 | 190 | arrays[arrNameFull][arrIdx] = currResult[arrName][currResult[arrName].length - 1]; 191 | } 192 | } 193 | 194 | currResult = arrays[arrNameFull][arrIdx]; 195 | } 196 | else 197 | { 198 | arrNameFull += namePart; 199 | 200 | if (j < nameParts.length - 1) /* Not the last part of name - means object */ 201 | { 202 | if (!currResult[namePart]) currResult[namePart] = {}; 203 | currResult = currResult[namePart]; 204 | } 205 | else 206 | { 207 | currResult[namePart] = value; 208 | } 209 | } 210 | } 211 | } 212 | 213 | return result; 214 | } 215 | 216 | function getFormValues(rootNode, nodeCallback, useIdIfEmptyName) 217 | { 218 | var result = extractNodeValues(rootNode, nodeCallback, useIdIfEmptyName); 219 | return result.length > 0 ? result : getSubFormValues(rootNode, nodeCallback, useIdIfEmptyName); 220 | } 221 | 222 | function getSubFormValues(rootNode, nodeCallback, useIdIfEmptyName) 223 | { 224 | var result = [], 225 | currentNode = rootNode.firstChild; 226 | 227 | while (currentNode) 228 | { 229 | result = result.concat(extractNodeValues(currentNode, nodeCallback, useIdIfEmptyName)); 230 | currentNode = currentNode.nextSibling; 231 | } 232 | 233 | return result; 234 | } 235 | 236 | function extractNodeValues(node, nodeCallback, useIdIfEmptyName) { 237 | var callbackResult, fieldValue, result, fieldName = getFieldName(node, useIdIfEmptyName); 238 | 239 | callbackResult = nodeCallback && nodeCallback(node); 240 | 241 | if (callbackResult && callbackResult.name) { 242 | result = [callbackResult]; 243 | } 244 | else if (fieldName != '' && node.nodeName.match(/INPUT|TEXTAREA/i)) { 245 | fieldValue = getFieldValue(node); 246 | result = [ { name: fieldName, value: fieldValue} ]; 247 | } 248 | else if (fieldName != '' && node.nodeName.match(/SELECT/i)) { 249 | fieldValue = getFieldValue(node); 250 | result = [ { name: fieldName.replace(/\[\]$/, ''), value: fieldValue } ]; 251 | } 252 | else { 253 | result = getSubFormValues(node, nodeCallback, useIdIfEmptyName); 254 | } 255 | 256 | return result; 257 | } 258 | 259 | function getFieldName(node, useIdIfEmptyName) 260 | { 261 | if (node.name && node.name != '') return node.name; 262 | else if (useIdIfEmptyName && node.id && node.id != '') return node.id; 263 | else return ''; 264 | } 265 | 266 | 267 | function getFieldValue(fieldNode) 268 | { 269 | if (fieldNode.disabled) return null; 270 | 271 | switch (fieldNode.nodeName) { 272 | case 'INPUT': 273 | case 'TEXTAREA': 274 | switch (fieldNode.type.toLowerCase()) { 275 | case 'radio': 276 | case 'checkbox': 277 | if (fieldNode.checked && fieldNode.value === "true") return true; 278 | if (!fieldNode.checked && fieldNode.value === "true") return false; 279 | if (fieldNode.checked) return fieldNode.value; 280 | break; 281 | 282 | case 'button': 283 | case 'reset': 284 | case 'submit': 285 | case 'image': 286 | return ''; 287 | break; 288 | 289 | default: 290 | return fieldNode.value; 291 | break; 292 | } 293 | break; 294 | 295 | case 'SELECT': 296 | return getSelectedOptionValue(fieldNode); 297 | break; 298 | 299 | default: 300 | break; 301 | } 302 | 303 | return null; 304 | } 305 | 306 | function getSelectedOptionValue(selectNode) 307 | { 308 | var multiple = selectNode.multiple, 309 | result = [], 310 | options, 311 | i, l; 312 | 313 | if (!multiple) return selectNode.value; 314 | 315 | for (options = selectNode.getElementsByTagName("option"), i = 0, l = options.length; i < l; i++) 316 | { 317 | if (options[i].selected) result.push(options[i].value); 318 | } 319 | 320 | return result; 321 | } 322 | 323 | return form2js; 324 | 325 | })(); -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.toObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 Maxim Vasiliev 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | * 22 | * @author Maxim Vasiliev 23 | * Date: 29.06.11 24 | * Time: 20:09 25 | */ 26 | 27 | (function($){ 28 | 29 | /** 30 | * jQuery wrapper for form2object() 31 | * Extracts data from child inputs into javascript object 32 | */ 33 | $.fn.toObject = function(options) 34 | { 35 | var result = [], 36 | settings = { 37 | mode: 'first', // what to convert: 'all' or 'first' matched node 38 | delimiter: ".", 39 | skipEmpty: true, 40 | nodeCallback: null, 41 | useIdIfEmptyName: false 42 | }; 43 | 44 | if (options) 45 | { 46 | $.extend(settings, options); 47 | } 48 | 49 | switch(settings.mode) 50 | { 51 | case 'first': 52 | return form2js(this.get(0), settings.delimiter, settings.skipEmpty, settings.nodeCallback, settings.useIdIfEmptyName); 53 | break; 54 | case 'all': 55 | this.each(function(){ 56 | result.push(form2js(this, settings.delimiter, settings.skipEmpty, settings.nodeCallback, settings.useIdIfEmptyName)); 57 | }); 58 | return result; 59 | break; 60 | case 'combine': 61 | return form2js(Array.prototype.slice.call(this), settings.delimiter, settings.skipEmpty, settings.nodeCallback, settings.useIdIfEmptyName); 62 | break; 63 | } 64 | } 65 | 66 | })(jQuery); -------------------------------------------------------------------------------- /vendor/assets/swf/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/swordfish/1b38b02bf1c5e980881d10b27679eed9c1a9262f/vendor/assets/swf/ZeroClipboard.swf --------------------------------------------------------------------------------