Skip to content

Commit 357b404

Browse files
committed
Add zxcvbn for checking password entropy
1 parent c94153a commit 357b404

13 files changed

Lines changed: 227 additions & 3 deletions

Gemfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ gem 'uglifier', '>= 1.3.0'
4141
gem 'wicked_pdf'
4242
gem 'wkhtmltopdf-binary'
4343
gem 'rubyzip', '~> 1.2.2'
44+
gem 'rbnacl'
45+
gem 'zxcvbn-js', require: 'zxcvbn'
4446

45-
# security audits
47+
# dependency audits
4648
gem 'bundler-audit'
4749
gem 'ruby_audit'
4850

Gemfile.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ GEM
247247
rb-fsevent (0.10.3)
248248
rb-inotify (0.9.10)
249249
ffi (>= 0.5.0, < 2)
250+
rbnacl (6.0.1)
251+
ffi
250252
responders (2.4.0)
251253
actionpack (>= 4.2.0, < 5.3)
252254
railties (>= 4.2.0, < 5.3)
@@ -336,6 +338,8 @@ GEM
336338
wkhtmltopdf-binary (0.12.3.1)
337339
xpath (3.0.0)
338340
nokogiri (~> 1.8)
341+
zxcvbn-js (4.4.3)
342+
execjs
339343

340344
PLATFORMS
341345
ruby
@@ -380,6 +384,7 @@ DEPENDENCIES
380384
rails (~> 5.2.0)
381385
rails-controller-testing
382386
rails-erd
387+
rbnacl
383388
rollbar
384389
rotp
385390
rqrcode
@@ -398,6 +403,7 @@ DEPENDENCIES
398403
uglifier (>= 1.3.0)
399404
wicked_pdf
400405
wkhtmltopdf-binary
406+
zxcvbn-js
401407

402408
RUBY VERSION
403409
ruby 2.5.3p105

app/lib/env.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def self.set_for_development_and_test!
1212
default_env['DISTRICT_NAME'] = 'Localhost Public Schools'
1313
default_env['MULTIFACTOR_AUTHENTICATOR_ROTP_CONFIG_JSON'] = '{"issuer_base":"student-insights-multifactor-authenticator-educator"}'
1414
default_env['CONSISTENT_TIMING_FOR_MULTIFACTOR_CODE_IN_MILLISECONDS'] = '2000'
15+
default_env['PASSWORD_CHECKED_SECRET64'] = "IyIMFkLrcvHY/fDMomHt7yYB6EgjGj532cGNhymmCPg=\n"
1516

1617
# service config
1718
default_env['USE_MOCK_LDAP'] = 'true'

app/lib/ldap_authenticatable_tiny/strategy.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def authenticate_without_consistent_timing!
6060
ldap_login = PerDistrict.new.ldap_login_for_educator(educator)
6161
return fail!(:invalid) unless is_authorized_by_ldap?(ldap_login, password_text)
6262

63-
# Success
63+
# Success, run password checks and store results encrypted and noised,
64+
# ignoring any errors in the process.
65+
store_password_check(password_text)
66+
67+
# Return success
6468
return success!(educator)
6569
rescue => error
6670
Rollbar.error('LdapAuthenticatableTiny error caught', error)
@@ -90,6 +94,18 @@ def is_authorized_by_ldap?(ldap_login, password_text)
9094
LdapAuthenticator.new(logger: logger).is_authorized_by_ldap?(ldap_login, password_text)
9195
end
9296

97+
# Store password check, logging and ignoring any failures.
98+
def store_password_check(password_text)
99+
begin
100+
json_encrypted = PasswordChecker.new.json_stats_encrypted(password_text)
101+
PasswordCheck.create!(json_encrypted: json_encrypted)
102+
rescue => _ # don't log errors, in case they contain anything sensitive
103+
Rollbar.error('LdapAuthenticatableTiny, store_password_check failed, ignoring and continuing...')
104+
logger.error "LdapAuthenticatableTiny, store_password_check failed, ignoring and continuing..."
105+
end
106+
nil
107+
end
108+
93109
def logger
94110
Rails.logger
95111
end

app/lib/password_checker.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Perform checks on passwords, computed stats and then throwing some bits away
2+
# and encrypting the result.
3+
class PasswordChecker
4+
def initialize(options = {})
5+
@sodium_box = options.fetch(:sodium_box, SodiumBox.new(ENV['PASSWORD_CHECKED_SECRET64']))
6+
end
7+
8+
# This only stores some data, and does so without the password hash, login, or timestamp.
9+
# The intention is to gauge how much this is worth exploring further (eg, prompts
10+
# for users to change passwords).
11+
def json_stats_encrypted(password)
12+
result = Zxcvbn.test(password)
13+
@sodium_box.encrypt64({
14+
score: result.score,
15+
guesses_log10_floor: result.guesses_log10.floor,
16+
has_warning: result.feedback['warning'] != ''
17+
}.to_json)
18+
end
19+
end

app/lib/sodium_box.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'rbnacl'
2+
require 'base64'
3+
4+
# Uses rbnacl/libsodium to encrypt strings using a shared secret.
5+
# Expects secret to be base64 encoded, and wraps payloads in base64 too.
6+
class SodiumBox
7+
def self.new_shared_secret64
8+
Base64.encode64(RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes))
9+
end
10+
11+
def initialize(shared_secret64)
12+
@shared_secret64 = shared_secret64
13+
end
14+
15+
def encrypt64(string)
16+
box = RbNaCl::SimpleBox.from_secret_key(Base64.decode64(@shared_secret64))
17+
Base64.encode64(box.encrypt(string))
18+
end
19+
20+
def decrypt64(payload64)
21+
box = RbNaCl::SimpleBox.from_secret_key(Base64.decode64(@shared_secret64))
22+
box.decrypt(Base64.decode64(payload64))
23+
end
24+
end

app/models/password_check.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class PasswordCheck < ApplicationRecord
2+
default_scope { order(id: :asc) }
3+
validates :json_encrypted, presence: true
4+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class EnablePgcryptoExtension < ActiveRecord::Migration[5.2]
2+
def change
3+
enable_extension 'pgcrypto'
4+
end
5+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class CreatePasswordChecks < ActiveRecord::Migration[5.2]
2+
def change
3+
create_table :password_checks, id: :uuid do |t|
4+
t.text :json_encrypted
5+
end
6+
end
7+
end

db/schema.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema.define(version: 2019_01_28_131614) do
13+
ActiveRecord::Schema.define(version: 2019_03_06_155619) do
1414

1515
# These are extensions that must be enabled in order to support this database
16+
enable_extension "pgcrypto"
1617
enable_extension "plpgsql"
1718

1819
create_table "absences", id: :serial, force: :cascade do |t|
@@ -406,6 +407,10 @@
406407
t.datetime "updated_at", null: false
407408
end
408409

410+
create_table "password_checks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
411+
t.text "json_encrypted"
412+
end
413+
409414
create_table "precomputed_query_docs", id: :serial, force: :cascade do |t|
410415
t.text "key"
411416
t.text "json"

0 commit comments

Comments
 (0)