-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add zxcvbn for checking password entropy
- Loading branch information
1 parent
c94153a
commit 357b404
Showing
13 changed files
with
227 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Perform checks on passwords, computed stats and then throwing some bits away | ||
# and encrypting the result. | ||
class PasswordChecker | ||
def initialize(options = {}) | ||
@sodium_box = options.fetch(:sodium_box, SodiumBox.new(ENV['PASSWORD_CHECKED_SECRET64'])) | ||
end | ||
|
||
# This only stores some data, and does so without the password hash, login, or timestamp. | ||
# The intention is to gauge how much this is worth exploring further (eg, prompts | ||
# for users to change passwords). | ||
def json_stats_encrypted(password) | ||
result = Zxcvbn.test(password) | ||
@sodium_box.encrypt64({ | ||
score: result.score, | ||
guesses_log10_floor: result.guesses_log10.floor, | ||
has_warning: result.feedback['warning'] != '' | ||
}.to_json) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
require 'rbnacl' | ||
require 'base64' | ||
|
||
# Uses rbnacl/libsodium to encrypt strings using a shared secret. | ||
# Expects secret to be base64 encoded, and wraps payloads in base64 too. | ||
class SodiumBox | ||
def self.new_shared_secret64 | ||
Base64.encode64(RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes)) | ||
end | ||
|
||
def initialize(shared_secret64) | ||
@shared_secret64 = shared_secret64 | ||
end | ||
|
||
def encrypt64(string) | ||
box = RbNaCl::SimpleBox.from_secret_key(Base64.decode64(@shared_secret64)) | ||
Base64.encode64(box.encrypt(string)) | ||
end | ||
|
||
def decrypt64(payload64) | ||
box = RbNaCl::SimpleBox.from_secret_key(Base64.decode64(@shared_secret64)) | ||
box.decrypt(Base64.decode64(payload64)) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class PasswordCheck < ApplicationRecord | ||
default_scope { order(id: :asc) } | ||
validates :json_encrypted, presence: true | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class EnablePgcryptoExtension < ActiveRecord::Migration[5.2] | ||
def change | ||
enable_extension 'pgcrypto' | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
class CreatePasswordChecks < ActiveRecord::Migration[5.2] | ||
def change | ||
create_table :password_checks, id: :uuid do |t| | ||
t.text :json_encrypted | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
require 'spec_helper' | ||
|
||
|
||
RSpec.describe PasswordChecker do | ||
it '#json_stats_encrypted does not raise and encrypts the value' do | ||
sodium_box = SodiumBox.new(SodiumBox.new_shared_secret64) | ||
checker = PasswordChecker.new(sodium_box: sodium_box) | ||
encrypted = checker.json_stats_encrypted('dangerous') | ||
expect(encrypted).not_to include('dangerous') | ||
end | ||
|
||
it '#json_stats_encrypted returns different values on subsequent runs' do | ||
sodium_box = SodiumBox.new(SodiumBox.new_shared_secret64) | ||
checker = PasswordChecker.new(sodium_box: sodium_box) | ||
encrypteds = 3.times.map { checker.json_stats_encrypted('dangerous') } | ||
expect(encrypteds.size).to eq(encrypteds.uniq.size) | ||
end | ||
|
||
describe 'env variable nil' do | ||
before do | ||
@PASSWORD_CHECKED_SECRET64 = ENV['PASSWORD_CHECKED_SECRET64'] | ||
ENV['PASSWORD_CHECKED_SECRET64'] = nil | ||
end | ||
|
||
after do | ||
ENV['PASSWORD_CHECKED_SECRET64'] = @PASSWORD_CHECKED_SECRET64 | ||
end | ||
|
||
it 'raises' do | ||
expect { PasswordChecker.new.json_stats_encrypted('dangerous') }.to raise_error NoMethodError | ||
end | ||
end | ||
|
||
describe 'env variable invalid' do | ||
before do | ||
@PASSWORD_CHECKED_SECRET64 = ENV['PASSWORD_CHECKED_SECRET64'] | ||
ENV['PASSWORD_CHECKED_SECRET64'] = 'invalid' | ||
end | ||
|
||
after do | ||
ENV['PASSWORD_CHECKED_SECRET64'] = @PASSWORD_CHECKED_SECRET64 | ||
end | ||
|
||
it 'raises' do | ||
expect { PasswordChecker.new.json_stats_encrypted('dangerous') }.to raise_error RbNaCl::LengthError | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
require 'spec_helper' | ||
|
||
# These are just smoke tests | ||
RSpec.describe SodiumBox do | ||
TEST_ITERATIONS = 1000 | ||
|
||
it '.new_shared_secret64 generates unique values' do | ||
secrets = TEST_ITERATIONS.times.map { SodiumBox.new_shared_secret64 } | ||
expect(secrets.size).to eq(secrets.uniq.size) | ||
end | ||
|
||
it 'works round-trip' do | ||
box = SodiumBox.new(SodiumBox.new_shared_secret64) | ||
expect(box.decrypt64(box.encrypt64('foo'))).to eq 'foo' | ||
TEST_ITERATIONS.times do | ||
test_string = SecureRandom.hex | ||
expect(box.decrypt64(box.encrypt64(test_string))).to eq test_string | ||
end | ||
end | ||
|
||
it 'generates different values for each encryption' do | ||
box = SodiumBox.new(SodiumBox.new_shared_secret64) | ||
payloads = TEST_ITERATIONS.times.map { box.encrypt64('foo') } | ||
expect(payloads.size).to eq(payloads.uniq.size) | ||
end | ||
|
||
it 'cannot decrypt with wrong secret' do | ||
box1 = SodiumBox.new(SodiumBox.new_shared_secret64) | ||
box2 = SodiumBox.new(SodiumBox.new_shared_secret64) | ||
TEST_ITERATIONS.times do | ||
expect { box2.decrypt64(box1.encrypt64(SecureRandom.hex)) }.to raise_error RbNaCl::CryptoError | ||
expect { box1.decrypt64(box2.encrypt64(SecureRandom.hex)) }.to raise_error RbNaCl::CryptoError | ||
end | ||
end | ||
end |