clarion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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