clarion 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ require 'clarion/const_finder'
2
+
3
+ module Clarion
4
+ module Counters
5
+ def self.find(name)
6
+ ConstFinder.find(self, 'clarion/counters', name)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module Clarion
2
+ module Counters
3
+ class Base
4
+ def initialize(options={})
5
+ @options = options
6
+ end
7
+
8
+ def get(key)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def store(key)
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ 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
@@ -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,9 @@
1
+ require 'clarion/const_finder'
2
+
3
+ module Clarion
4
+ module Stores
5
+ def self.find(name)
6
+ ConstFinder.find(self, 'clarion/stores', name)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module Clarion
2
+ module Stores
3
+ class Base
4
+ def initialize(options={})
5
+ @options = options
6
+ end
7
+
8
+ def store_authn(authn)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def find_authn(id)
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ 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