clarion 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +6 -0
- data/app/public/register.js +82 -0
- data/app/public/sign.js +67 -0
- data/app/public/test.js +81 -0
- data/app/public/u2f-api.js +748 -0
- data/app/views/authn.erb +51 -0
- data/app/views/layout.erb +128 -0
- data/app/views/register.erb +55 -0
- data/app/views/test.erb +35 -0
- data/app/views/test_callback.erb +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/test-authn +11 -0
- data/clarion.gemspec +32 -0
- data/config.ru +63 -0
- data/dev.rb +30 -0
- data/docs/api.md +113 -0
- data/docs/counters.md +14 -0
- data/docs/stores.md +13 -0
- data/lib/clarion.rb +3 -0
- data/lib/clarion/app.rb +271 -0
- data/lib/clarion/authenticator.rb +51 -0
- data/lib/clarion/authn.rb +106 -0
- data/lib/clarion/config.rb +58 -0
- data/lib/clarion/const_finder.rb +24 -0
- data/lib/clarion/counters.rb +9 -0
- data/lib/clarion/counters/base.rb +17 -0
- data/lib/clarion/counters/dynamodb.rb +45 -0
- data/lib/clarion/counters/memory.rb +29 -0
- data/lib/clarion/key.rb +76 -0
- data/lib/clarion/registrator.rb +26 -0
- data/lib/clarion/stores.rb +9 -0
- data/lib/clarion/stores/base.rb +17 -0
- data/lib/clarion/stores/memory.rb +30 -0
- data/lib/clarion/stores/s3.rb +54 -0
- data/lib/clarion/version.rb +3 -0
- metadata +199 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'clarion/key'
|
4
|
+
|
5
|
+
module Clarion
|
6
|
+
class Authn
|
7
|
+
STATUSES = %i(open verified)
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def make(**kwargs)
|
11
|
+
kwargs.delete(:id)
|
12
|
+
new(
|
13
|
+
id: random_id,
|
14
|
+
created_at: Time.now,
|
15
|
+
**kwargs,
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def random_id
|
20
|
+
SecureRandom.urlsafe_base64(64)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(id:, name: nil, comment: nil, keys: [], created_at:, expires_at:, status:, verified_at: nil, verified_key: nil)
|
25
|
+
@id = id
|
26
|
+
@name = name
|
27
|
+
@comment = comment
|
28
|
+
@keys = keys.map{ |_| _.is_a?(Hash) ? Key.new(**_) : _}
|
29
|
+
@created_at = created_at
|
30
|
+
@expires_at = expires_at
|
31
|
+
@status = status.to_sym
|
32
|
+
@verified_at = verified_at
|
33
|
+
@verified_key = verified_key.is_a?(Hash) ? Key.new(**verified_key) : verified_key
|
34
|
+
|
35
|
+
@created_at = Time.xmlschema(@created_at) if @created_at && @created_at.is_a?(String)
|
36
|
+
@expires_at = Time.xmlschema(@expires_at) if @expires_at && @expires_at.is_a?(String)
|
37
|
+
@verified_at = Time.xmlschema(@verified_at) if @verified_at && @verified_at.is_a?(String)
|
38
|
+
|
39
|
+
raise ArgumentError, ":status not valid" unless STATUSES.include?(@status)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :id, :name, :comment
|
43
|
+
attr_reader :keys
|
44
|
+
attr_reader :created_at
|
45
|
+
attr_reader :expires_at
|
46
|
+
attr_reader :status
|
47
|
+
attr_reader :verified_at, :verified_key
|
48
|
+
|
49
|
+
def expired?
|
50
|
+
Time.now > expires_at
|
51
|
+
end
|
52
|
+
|
53
|
+
def open?
|
54
|
+
status == :open
|
55
|
+
end
|
56
|
+
|
57
|
+
def verified?
|
58
|
+
status == :verified
|
59
|
+
end
|
60
|
+
|
61
|
+
def key_for_handle(handle)
|
62
|
+
keys.find { |_| _.handle == handle }
|
63
|
+
end
|
64
|
+
|
65
|
+
def verify(key, verified_at: Time.now)
|
66
|
+
unless key_for_handle(key.handle)
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
@verified_at = verified_at
|
70
|
+
@verified_key = key
|
71
|
+
@status = :verified
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_h(all=false)
|
76
|
+
{
|
77
|
+
id: id,
|
78
|
+
status: status,
|
79
|
+
name: name,
|
80
|
+
comment: comment,
|
81
|
+
created_at: created_at,
|
82
|
+
expires_at: expires_at,
|
83
|
+
}.tap do |h|
|
84
|
+
if verified_key
|
85
|
+
h[:verified_at] = verified_at
|
86
|
+
h[:verified_key] = verified_key.to_h(all)
|
87
|
+
end
|
88
|
+
if all
|
89
|
+
h[:keys] = keys.map{ |_| _.to_h(all) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def as_json(*args)
|
95
|
+
to_h(*args).tap { |_|
|
96
|
+
_[:created_at] = _[:created_at].xmlschema if _[:created_at]
|
97
|
+
_[:verified_at] = _[:verified_at].xmlschema if _[:verified_at]
|
98
|
+
_[:expires_at] = _[:expires_at].xmlschema if _[:expires_at]
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_json(*args)
|
103
|
+
as_json(*args).to_json
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'clarion/stores'
|
2
|
+
require 'clarion/counters'
|
3
|
+
|
4
|
+
module Clarion
|
5
|
+
class Config
|
6
|
+
class << self
|
7
|
+
def option(meth)
|
8
|
+
options << meth
|
9
|
+
end
|
10
|
+
|
11
|
+
def options
|
12
|
+
@options ||= []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(options={})
|
17
|
+
@options = options
|
18
|
+
|
19
|
+
# Validation
|
20
|
+
self.class.options.each do |m|
|
21
|
+
send(m)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
option def registration_allowed_url
|
28
|
+
@options.fetch(:registration_allowed_url)
|
29
|
+
end
|
30
|
+
|
31
|
+
option def authn_default_expires_in
|
32
|
+
@options.fetch(:authn_default_expires_in, 300).to_i
|
33
|
+
end
|
34
|
+
|
35
|
+
option def app_id
|
36
|
+
@options[:app_id]
|
37
|
+
end
|
38
|
+
|
39
|
+
option def store
|
40
|
+
@store ||= Clarion::Stores.find(@options.fetch(:store).fetch(:kind)).new(store_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def store_options
|
44
|
+
@store_options ||= @options.fetch(:store).dup.tap { |_| _.delete(:kind) }
|
45
|
+
end
|
46
|
+
|
47
|
+
option def counter
|
48
|
+
if @options[:counter]
|
49
|
+
@counter ||= Clarion::Counters.find(@options.fetch(:counter).fetch(:kind)).new(counter_options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def counter_options
|
54
|
+
@counter_options ||= @options.fetch(:counter).dup.tap { |_| _.delete(:kind) }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Clarion
|
2
|
+
module ConstFinder
|
3
|
+
def self.find(const, prefix, name)
|
4
|
+
retried = false
|
5
|
+
constant_name = name.to_s.gsub(/\A.|_./) { |s| s[-1].upcase }
|
6
|
+
|
7
|
+
begin
|
8
|
+
const.const_get constant_name, false
|
9
|
+
rescue NameError
|
10
|
+
unless retried
|
11
|
+
begin
|
12
|
+
require "#{prefix}/#{name}"
|
13
|
+
rescue LoadError
|
14
|
+
end
|
15
|
+
|
16
|
+
retried = true
|
17
|
+
retry
|
18
|
+
end
|
19
|
+
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'aws-sdk-dynamodb'
|
2
|
+
require 'clarion/counters/base'
|
3
|
+
|
4
|
+
module Clarion
|
5
|
+
module Counters
|
6
|
+
class Dynamodb < Base
|
7
|
+
def initialize(table_name:, region:)
|
8
|
+
@table_name = table_name
|
9
|
+
@region = region
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(key)
|
13
|
+
item = table.query(
|
14
|
+
limit: 1,
|
15
|
+
select: 'ALL_ATTRIBUTES',
|
16
|
+
key_condition_expression: 'handle = :handle',
|
17
|
+
expression_attribute_values: {":handle" => key.handle},
|
18
|
+
).items.first
|
19
|
+
|
20
|
+
item && item['key_counter']
|
21
|
+
end
|
22
|
+
|
23
|
+
def store(key)
|
24
|
+
table.update_item(
|
25
|
+
key: {
|
26
|
+
'handle' => key.handle,
|
27
|
+
},
|
28
|
+
update_expression: 'SET key_counter = :new',
|
29
|
+
condition_expression: 'attribute_not_exists(key_counter) OR key_counter < :new',
|
30
|
+
expression_attribute_values: {':new' => key.counter},
|
31
|
+
)
|
32
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
33
|
+
end
|
34
|
+
|
35
|
+
def table
|
36
|
+
@table ||= dynamodb.table(@table_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def dynamodb
|
40
|
+
@dynamodb ||= Aws::DynamoDB::Resource.new(region: @region)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'clarion/counters/base'
|
3
|
+
|
4
|
+
module Clarion
|
5
|
+
module Counters
|
6
|
+
class Memory < Base
|
7
|
+
def initialize(*)
|
8
|
+
super
|
9
|
+
@lock = Mutex.new
|
10
|
+
@counters = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(key)
|
14
|
+
@lock.synchronize do
|
15
|
+
@counters[key.handle]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def store(key)
|
20
|
+
@lock.synchronize do
|
21
|
+
counter = @counters[key.handle]
|
22
|
+
if !counter || key.counter > counter
|
23
|
+
@counters[key.handle] = key.counter
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/clarion/key.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module Clarion
|
2
|
+
class Key
|
3
|
+
CIPHER_ALGO = 'aes-256-gcm'
|
4
|
+
def self.from_encrypted_json(private_key, json)
|
5
|
+
payload = JSON.parse(json, symbolize_names: true)
|
6
|
+
encrypted_data = payload.fetch(:data).unpack('m*')[0]
|
7
|
+
encrypted_shared_key = payload.fetch(:key).unpack('m*')[0]
|
8
|
+
|
9
|
+
shared_key_json = private_key.private_decrypt(encrypted_shared_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
10
|
+
shared_key_info = JSON.parse(shared_key_json, symbolize_names: true)
|
11
|
+
iv = shared_key_info.fetch(:iv).unpack('m*')[0]
|
12
|
+
shared_key = shared_key_info.fetch(:key).unpack('m*')[0]
|
13
|
+
tag = shared_key_info.fetch(:tag).unpack('m*')[0]
|
14
|
+
|
15
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGO).tap do |c|
|
16
|
+
c.decrypt
|
17
|
+
c.key = shared_key
|
18
|
+
c.iv = iv
|
19
|
+
c.auth_data = ''
|
20
|
+
c.auth_tag = tag
|
21
|
+
end
|
22
|
+
|
23
|
+
key_json = cipher.update(encrypted_data)
|
24
|
+
key_json << cipher.final
|
25
|
+
key = JSON.parse(key_json, symbolize_names: true)
|
26
|
+
new(**key)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(handle:, name: nil, public_key: nil, counter: nil)
|
30
|
+
@handle = handle
|
31
|
+
@name = name
|
32
|
+
@public_key = public_key
|
33
|
+
@counter = counter
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :handle, :name, :public_key
|
37
|
+
attr_accessor :counter
|
38
|
+
|
39
|
+
def to_h(all=false)
|
40
|
+
{
|
41
|
+
handle: handle,
|
42
|
+
}.tap do |h|
|
43
|
+
h[:name] = name if name
|
44
|
+
h[:counter] = counter if counter
|
45
|
+
if all
|
46
|
+
h[:public_key] = public_key if public_key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_json(*args)
|
52
|
+
to_h(*args).to_json
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_encrypted_json(public_key, *args)
|
56
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGO)
|
57
|
+
shared_key = OpenSSL::Random.random_bytes(cipher.key_len)
|
58
|
+
cipher.encrypt
|
59
|
+
cipher.key = shared_key
|
60
|
+
cipher.iv = iv = cipher.random_iv
|
61
|
+
cipher.auth_data = ''
|
62
|
+
|
63
|
+
json = to_json(*args)
|
64
|
+
|
65
|
+
ciphertext = cipher.update(json)
|
66
|
+
ciphertext << cipher.final
|
67
|
+
|
68
|
+
encrypted_key = public_key.public_encrypt({
|
69
|
+
iv: [iv].pack('m*').gsub(/\r?\n/,''),
|
70
|
+
tag: [cipher.auth_tag].pack('m*').gsub(/\r?\n/,''),
|
71
|
+
key: [shared_key].pack('m*').gsub(/\r?\n/,''),
|
72
|
+
}.to_json, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
73
|
+
{data: [ciphertext].pack('m*'), key: [encrypted_key].pack('m*')}.to_json
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'clarion/key'
|
2
|
+
|
3
|
+
module Clarion
|
4
|
+
class Registrator
|
5
|
+
def initialize(u2f, counter)
|
6
|
+
@u2f = u2f
|
7
|
+
@counter = counter
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :u2f, :counter
|
11
|
+
|
12
|
+
def request
|
13
|
+
[u2f.app_id, u2f.registration_requests]
|
14
|
+
end
|
15
|
+
|
16
|
+
def register!(challenges, response_json)
|
17
|
+
response = U2F::RegisterResponse.load_from_json(response_json)
|
18
|
+
reg = u2f.register!(challenges, response)
|
19
|
+
key = Key.new(handle: reg.key_handle, public_key: reg.public_key, counter: reg.counter)
|
20
|
+
if counter
|
21
|
+
counter.store(key)
|
22
|
+
end
|
23
|
+
key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'clarion/stores/base'
|
3
|
+
require 'clarion/authn'
|
4
|
+
|
5
|
+
module Clarion
|
6
|
+
module Stores
|
7
|
+
class Memory < Base
|
8
|
+
def initialize(*)
|
9
|
+
super
|
10
|
+
@lock = Mutex.new
|
11
|
+
@store = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def store_authn(authn)
|
15
|
+
@lock.synchronize do
|
16
|
+
@store[authn.id] = authn.to_h(:all)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_authn(id)
|
21
|
+
@lock.synchronize do
|
22
|
+
unless @store.key?(id)
|
23
|
+
return nil
|
24
|
+
end
|
25
|
+
Authn.new(**@store[id])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|