darrrr 0.0.3 → 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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -5
  3. data/lib/darrrr.rb +144 -2
  4. data/lib/darrrr/account_provider.rb +150 -0
  5. data/lib/darrrr/constants.rb +17 -0
  6. data/lib/darrrr/crypto_helper.rb +55 -0
  7. data/lib/darrrr/cryptors/default/default_encryptor.rb +68 -0
  8. data/lib/darrrr/cryptors/default/encrypted_data.rb +90 -0
  9. data/lib/darrrr/cryptors/default/encrypted_data_io.rb +10 -0
  10. data/lib/darrrr/provider.rb +157 -0
  11. data/lib/darrrr/recovery_provider.rb +129 -0
  12. data/lib/darrrr/recovery_token.rb +117 -0
  13. data/lib/darrrr/serialization/recovery_token_reader.rb +20 -0
  14. data/lib/darrrr/serialization/recovery_token_writer.rb +20 -0
  15. metadata +17 -18
  16. data/lib/github/delegated_account_recovery.rb +0 -178
  17. data/lib/github/delegated_account_recovery/account_provider.rb +0 -140
  18. data/lib/github/delegated_account_recovery/constants.rb +0 -19
  19. data/lib/github/delegated_account_recovery/crypto_helper.rb +0 -57
  20. data/lib/github/delegated_account_recovery/cryptors/default/default_encryptor.rb +0 -66
  21. data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data.rb +0 -92
  22. data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data_io.rb +0 -12
  23. data/lib/github/delegated_account_recovery/provider.rb +0 -125
  24. data/lib/github/delegated_account_recovery/recovery_provider.rb +0 -118
  25. data/lib/github/delegated_account_recovery/recovery_token.rb +0 -113
  26. data/lib/github/delegated_account_recovery/serialization/recovery_token_reader.rb +0 -22
  27. data/lib/github/delegated_account_recovery/serialization/recovery_token_writer.rb +0 -22
  28. data/lib/github/delegated_account_recovery/version.rb +0 -5
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darrrr
4
+ class RecoveryTokenReader < BinData::Record
5
+ uint8 :version
6
+ uint8 :token_type
7
+ array :token_id, :type => :uint8, :read_until => lambda { index + 1 == Darrrr::TOKEN_ID_BYTE_LENGTH }
8
+ uint8 :options
9
+ uint16be :issuer_length
10
+ string :issuer, :read_length => :issuer_length
11
+ uint16be :audience_length
12
+ string :audience, :read_length => :audience_length
13
+ uint16be :issued_time_length
14
+ string :issued_time, :read_length => :issued_time_length
15
+ uint16be :data_length
16
+ string :data, :read_length => :data_length
17
+ uint16be :binding_data_length
18
+ string :binding_data, :read_length => :binding_data_length
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Darrrr
4
+ class RecoveryTokenWriter < BinData::Record
5
+ uint8 :version
6
+ uint8 :token_type
7
+ array :token_id, :type => :uint8, :initial_length => Darrrr::TOKEN_ID_BYTE_LENGTH
8
+ uint8 :options
9
+ uint16be :issuer_length, :value => lambda { issuer.length }
10
+ string :issuer
11
+ uint16be :audience_length, :value => lambda { audience.length }
12
+ string :audience
13
+ uint16be :issued_time_length, :value => lambda { issued_time.length }
14
+ string :issued_time
15
+ uint16be :data_length, :value => lambda { data.length }
16
+ string :data
17
+ uint16be :binding_data_length, :value => lambda { binding_data.length }
18
+ string :binding_data
19
+ end
20
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: darrrr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Matatall
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-25 00:00:00.000000000 Z
11
+ date: 2017-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -90,21 +90,20 @@ files:
90
90
  - README.md
91
91
  - Rakefile
92
92
  - lib/darrrr.rb
93
- - lib/github/delegated_account_recovery.rb
94
- - lib/github/delegated_account_recovery/account_provider.rb
95
- - lib/github/delegated_account_recovery/constants.rb
96
- - lib/github/delegated_account_recovery/crypto_helper.rb
97
- - lib/github/delegated_account_recovery/cryptors/default/default_encryptor.rb
98
- - lib/github/delegated_account_recovery/cryptors/default/encrypted_data.rb
99
- - lib/github/delegated_account_recovery/cryptors/default/encrypted_data_io.rb
100
- - lib/github/delegated_account_recovery/provider.rb
101
- - lib/github/delegated_account_recovery/recovery_provider.rb
102
- - lib/github/delegated_account_recovery/recovery_token.rb
103
- - lib/github/delegated_account_recovery/serialization/recovery_token_reader.rb
104
- - lib/github/delegated_account_recovery/serialization/recovery_token_writer.rb
105
- - lib/github/delegated_account_recovery/version.rb
106
- homepage: http://github.com/oreoshake/darrrr
107
- licenses: []
93
+ - lib/darrrr/account_provider.rb
94
+ - lib/darrrr/constants.rb
95
+ - lib/darrrr/crypto_helper.rb
96
+ - lib/darrrr/cryptors/default/default_encryptor.rb
97
+ - lib/darrrr/cryptors/default/encrypted_data.rb
98
+ - lib/darrrr/cryptors/default/encrypted_data_io.rb
99
+ - lib/darrrr/provider.rb
100
+ - lib/darrrr/recovery_provider.rb
101
+ - lib/darrrr/recovery_token.rb
102
+ - lib/darrrr/serialization/recovery_token_reader.rb
103
+ - lib/darrrr/serialization/recovery_token_writer.rb
104
+ homepage: http://github.com/github/darrrr
105
+ licenses:
106
+ - MIT
108
107
  metadata: {}
109
108
  post_install_message:
110
109
  rdoc_options: []
@@ -122,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
121
  version: '0'
123
122
  requirements: []
124
123
  rubyforge_project:
125
- rubygems_version: 2.5.1
124
+ rubygems_version: 2.6.10
126
125
  signing_key:
127
126
  specification_version: 4
128
127
  summary: Client library for the Delegated Recovery spec
@@ -1,178 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bindata"
4
- require "openssl"
5
-
6
- require_relative "delegated_account_recovery/constants"
7
- require_relative "delegated_account_recovery/crypto_helper"
8
- require_relative "delegated_account_recovery/recovery_token"
9
- require_relative "delegated_account_recovery/provider"
10
- require_relative "delegated_account_recovery/account_provider"
11
- require_relative "delegated_account_recovery/recovery_provider"
12
- require_relative "delegated_account_recovery/serialization/recovery_token_writer"
13
- require_relative "delegated_account_recovery/serialization/recovery_token_reader"
14
- require_relative "delegated_account_recovery/cryptors/default/default_encryptor"
15
- require_relative "delegated_account_recovery/cryptors/default/encrypted_data"
16
- require_relative "delegated_account_recovery/cryptors/default/encrypted_data_io"
17
-
18
- module GitHub
19
- module DelegatedAccountRecovery
20
- # Represents a binary serialization error
21
- class RecoveryTokenSerializationError < StandardError; end
22
-
23
- # Represents invalid data within a valid token
24
- # (e.g. wrong `version` number, invalid token `type`)
25
- class TokenFormatError < StandardError; end
26
-
27
- # Represents all crypto errors
28
- # (e.g. invalid keys, invalid signature, decrypt failures)
29
- class CryptoError < StandardError; end
30
-
31
- # Represents providers supplying invalid configurations
32
- # (e.g. non-https URLs, missing required fields, http errors)
33
- class ProviderConfigError < StandardError; end
34
-
35
- # Represents an invalid countersigned recovery token.
36
- # (e.g. invalid signature, invalid nested token, unregistered provider, stale tokens)
37
- class CountersignedTokenError < StandardError
38
- attr_reader :key
39
- def initialize(message, key)
40
- super(message)
41
- @key = key
42
- end
43
- end
44
-
45
- # Represents an invalid recovery token.
46
- # (e.g. invalid signature, unregistered provider, stale tokens)
47
- class RecoveryTokenError < StandardError; end
48
-
49
- # Represents a call to to `recovery_provider` or `account_provider` that
50
- # has not been registered.
51
- class UnknownProviderError < ArgumentError; end
52
-
53
- include Constants
54
-
55
- class << self
56
- REQUIRED_CRYPTO_OPS = [:sign, :verify, :encrypt, :decrypt].freeze
57
- # recovery provider data is only loaded (and cached) upon use.
58
- attr_accessor :recovery_providers, :account_providers, :cache, :allow_unsafe_urls,
59
- :privacy_policy, :icon_152px, :authority
60
-
61
- # Find and load remote recovery provider configuration data.
62
- #
63
- # provider_origin: the origin that contains the config data in a well-known
64
- # location.
65
- def recovery_provider(provider_origin)
66
- unless self.recovery_providers
67
- raise "No recovery providers configured"
68
- end
69
- if self.recovery_providers.include?(provider_origin)
70
- RecoveryProvider.new(provider_origin).load
71
- else
72
- raise UnknownProviderError, "Unknown recovery provider: #{provider_origin}"
73
- end
74
- end
75
-
76
- # Permit an origin to act as a recovery provider.
77
- #
78
- # provider_origin: the origin to permit
79
- def register_recovery_provider(provider_origin)
80
- self.recovery_providers ||= []
81
- self.recovery_providers << provider_origin
82
- end
83
-
84
- # Find and load remote account provider configuration data.
85
- #
86
- # provider_origin: the origin that contains the config data in a well-known
87
- # location.
88
- def account_provider(provider_origin)
89
- unless self.account_providers
90
- raise "No account providers configured"
91
- end
92
- if self.account_providers.include?(provider_origin)
93
- AccountProvider.new(provider_origin).load
94
- else
95
- raise UnknownProviderError, "Unknown account provider: #{provider_origin}"
96
- end
97
- end
98
-
99
- # Permit an origin to act as an account provider.
100
- #
101
- # account_origin: the origin to permit
102
- def register_account_provider(account_origin)
103
- self.account_providers ||= []
104
- self.account_providers << account_origin
105
- end
106
-
107
- # Provide a reference to the account provider configuration for this web app
108
- def this_account_provider
109
- AccountProvider.this
110
- end
111
-
112
- # Provide a reference to the recovery provider configuration for this web app
113
- def this_recovery_provider
114
- RecoveryProvider.this
115
- end
116
-
117
- # Returns a hash of all configuration values, recovery and account provider.
118
- def account_and_recovery_provider_config
119
- provider_data = Darrrr.this_account_provider.try(:to_h) || {}
120
-
121
- if Darrrr.this_recovery_provider
122
- provider_data.merge!(recovery_provider_config) do |key, lhs, rhs|
123
- unless lhs == rhs
124
- raise ArgumentError, "inconsistent config value detected #{key}: #{lhs} != #{rhs}"
125
- end
126
-
127
- lhs
128
- end
129
- end
130
-
131
- provider_data
132
- end
133
-
134
- # returns the account provider information in hash form
135
- def account_provider_config
136
- this_account_provider.to_h
137
- end
138
-
139
- # returns the account provider information in hash form
140
- def recovery_provider_config
141
- this_recovery_provider.to_h
142
- end
143
-
144
- def with_encryptor(encryptor)
145
- raise ArgumentError, "A block must be supplied" unless block_given?
146
- unless valid_encryptor?(encryptor)
147
- raise ArgumentError, "custom encryption class must respond to all of #{REQUIRED_CRYPTO_OPS}"
148
- end
149
-
150
- Thread.current[:darrrr_encryptor] = encryptor
151
- yield
152
- ensure
153
- Thread.current[:darrrr_encryptor] = nil
154
- end
155
-
156
- # Returns the crypto API to be used. A thread local instance overrides the
157
- # globally configured value which overrides the default encryptor.
158
- def encryptor
159
- Thread.current[:darrrr_encryptor] || @encryptor || DefaultEncryptor
160
- end
161
-
162
- # Overrides the global `encryptor` API to use
163
- #
164
- # encryptor: a class/module that responds to all +REQUIRED_CRYPTO_OPS+.
165
- def custom_encryptor=(encryptor)
166
- if valid_encryptor?(encryptor)
167
- @encryptor = encryptor
168
- else
169
- raise ArgumentError, "custom encryption class must respond to all of #{REQUIRED_CRYPTO_OPS}"
170
- end
171
- end
172
-
173
- private def valid_encryptor?(encryptor)
174
- REQUIRED_CRYPTO_OPS.all? {|m| encryptor.respond_to?(m)}
175
- end
176
- end
177
- end
178
- end
@@ -1,140 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GitHub
4
- module DelegatedAccountRecovery
5
- class AccountProvider
6
- include CryptoHelper
7
- include Provider
8
-
9
- # Only applicable when acting as a recovery provider
10
- PRIVATE_FIELDS = [:symmetric_key, :signing_private_key]
11
-
12
- FIELDS = [:tokensign_pubkeys_secp256r1].freeze
13
- URL_FIELDS = [:issuer, :save_token_return, :recover_account_return,
14
- :privacy_policy, :icon_152px].freeze
15
-
16
- # These are the fields required by the spec
17
- REQUIRED_FIELDS = FIELDS + URL_FIELDS
18
-
19
- attr_accessor(*REQUIRED_FIELDS)
20
- attr_accessor(*PRIVATE_FIELDS)
21
-
22
- alias :origin :issuer
23
-
24
- # The CryptoHelper defines an `unseal` method that requires us to
25
- # define a `unseal_keys` method that will return the set of keys that
26
- # are valid when verifying the signature on a sealed key.
27
- def unseal_keys
28
- tokensign_pubkeys_secp256r1
29
- end
30
-
31
- # Used to serve content at /.well-known/delegated-account-recovery/configuration
32
- def to_h
33
- {
34
- "issuer" => self.issuer,
35
- "tokensign-pubkeys-secp256r1" => self.tokensign_pubkeys_secp256r1.dup,
36
- "save-token-return" => self.save_token_return,
37
- "recover-account-return" => self.recover_account_return,
38
- "privacy-policy" => self.privacy_policy,
39
- "icon-152px" => self.icon_152px
40
- }
41
- end
42
-
43
- # Generates a binary token with an encrypted arbitrary data payload.
44
- #
45
- # data: value to encrypt in the token
46
- # provider: the recovery provider/audience of the token
47
- # binding data: binding data value retrieved from recovery provider to
48
- # provide some assurance the same browser was used.
49
- def generate_recovery_token(data:, audience:)
50
- RecoveryToken.build(issuer: self, audience: audience, type: RECOVERY_TOKEN_TYPE).tap do |token|
51
- token.data = Darrrr.encryptor.encrypt(data)
52
- end
53
- end
54
-
55
- # Parses a countersigned_token and returns the nested recovery token
56
- # WITHOUT verifying any signatures. This should only be used if no user
57
- # context can be identified or if we're extracting issuer information.
58
- def dangerous_unverified_recovery_token(countersigned_token)
59
- parsed_countersigned_token = RecoveryToken.parse(Base64.strict_decode64(countersigned_token))
60
- RecoveryToken.parse(parsed_countersigned_token.data)
61
- end
62
-
63
- # Validates the countersigned recovery token by verifying the signature
64
- # of the countersigned token, parsing out the origin recovery token,
65
- # verifying the signature on the recovery token, and finally decrypting
66
- # the data in the origin recovery token.
67
- #
68
- # countersigned_token: our original recovery token wrapped in recovery
69
- # token instance that is signed by the recovery provider.
70
- #
71
- # returns a verified recovery token or raises
72
- # an error if the token fails validation.
73
- def validate_countersigned_recovery_token!(countersigned_token)
74
- # 5. Validate the the issuer field is present in the token,
75
- # and that it matches the audience field in the original countersigned token.
76
- begin
77
- recovery_provider = RecoveryToken.recovery_provider_issuer(Base64.strict_decode64(countersigned_token))
78
- rescue RecoveryTokenSerializationError => e
79
- raise CountersignedTokenError.new("Countersigned token is invalid: " + e.message, :countersigned_token_parse_error)
80
- rescue UnknownProviderError => e
81
- raise CountersignedTokenError.new(e.message, :recovery_token_invalid_issuer)
82
- end
83
-
84
- # 1. Parse the countersigned-token.
85
- # 2. Validate that the version field is 0.
86
- # 7. Retrieve the current Recovery Provider configuration as described in Section 2.
87
- # 8. Validate that the counter-signed token signature validates with a current element of the countersign-pubkeys-secp256r1 array.
88
- begin
89
- parsed_countersigned_token = recovery_provider.unseal(Base64.strict_decode64(countersigned_token))
90
- rescue TokenFormatError => e
91
- raise CountersignedTokenError.new(e.message, :countersigned_invalid_token_version)
92
- rescue CryptoError
93
- raise CountersignedTokenError.new("Countersigned token has an invalid signature", :countersigned_invalid_signature)
94
- end
95
-
96
- # 3. De-serialize the original recovery token from the data field.
97
- # 4. Validate the signature on the original recovery token.
98
- begin
99
- recovery_token = self.unseal(parsed_countersigned_token.data)
100
- rescue RecoveryTokenSerializationError => e
101
- raise CountersignedTokenError.new("Nested recovery token is invalid: " + e.message, :recovery_token_token_parse_error)
102
- rescue TokenFormatError => e
103
- raise CountersignedTokenError.new("Nested recovery token format error: #{e.message}", :recovery_token_invalid_token_type)
104
- rescue CryptoError
105
- raise CountersignedTokenError.new("Nested recovery token has an invalid signature", :recovery_token_invalid_signature)
106
- end
107
-
108
- # 5. Validate the the issuer field is present in the countersigned-token,
109
- # and that it matches the audience field in the original token.
110
-
111
- countersigned_token_issuer = parsed_countersigned_token.issuer
112
- if countersigned_token_issuer.blank? || countersigned_token_issuer != recovery_token.audience || recovery_provider.origin != countersigned_token_issuer
113
- raise CountersignedTokenError.new("Validate the the issuer field is present in the countersigned-token, and that it matches the audience field in the original token", :recovery_token_invalid_issuer)
114
- end
115
-
116
- # 6. Validate the token binding for the countersigned token, if present.
117
- # (the token binding for the inner token is not relevant)
118
- # TODO not required, to be implemented later
119
-
120
- # 9. Decrypt the data field from the original recovery token and parse its information, if present.
121
- begin
122
- recovery_token.decode
123
- rescue CryptoError
124
- raise CountersignedTokenError.new("Recovery token data could not be decrypted", :indecipherable_opaque_data)
125
- end
126
-
127
- # 10. Apply any additional processing which provider-specific data in the opaque data portion may indicate is necessary.
128
- begin
129
- if DateTime.parse(parsed_countersigned_token.issued_time).utc < (Time.now - CLOCK_SKEW).utc
130
- raise CountersignedTokenError.new("Countersigned recovery token issued at time is too far in the past", :stale_token)
131
- end
132
- rescue ArgumentError
133
- raise CountersignedTokenError.new("Invalid countersigned token issued time", :invalid_issued_time)
134
- end
135
-
136
- recovery_token
137
- end
138
- end
139
- end
140
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GitHub
4
- module DelegatedAccountRecovery
5
- module Constants
6
- PROTOCOL_VERSION = 0
7
- PRIME_256_V1 = "prime256v1" # AKA secp256r1
8
- GROUP = OpenSSL::PKey::EC::Group.new(PRIME_256_V1)
9
- DIGEST = OpenSSL::Digest::SHA256
10
- TOKEN_ID_BYTE_LENGTH = 16
11
- RECOVERY_TOKEN_TYPE = 0
12
- COUNTERSIGNED_RECOVERY_TOKEN_TYPE = 1
13
- WELL_KNOWN_CONFIG_PATH = ".well-known/delegated-account-recovery/configuration"
14
- CLOCK_SKEW = 5 * 60
15
- end
16
-
17
- include Constants
18
- end
19
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GitHub
4
- module DelegatedAccountRecovery
5
- module CryptoHelper
6
- include Constants
7
- # Signs the provided token and joins the data with the signature.
8
- #
9
- # token: a RecoveryToken instance
10
- #
11
- # returns a base64 value for the binary token string and the signature
12
- # of the token.
13
- def seal(token)
14
- raise RuntimeError, "signing private key must be set" unless self.signing_private_key
15
- binary_token = token.to_binary_s
16
- signature = Darrrr.encryptor.sign(binary_token, self.signing_private_key)
17
- Base64.strict_encode64([binary_token, signature].join)
18
- end
19
-
20
- # Splits the payload by the token size, treats the remaining portion as
21
- # the signature of the payload, and verifies the signature is valid for
22
- # the given payload.
23
- #
24
- # token_and_signature: binary string consisting of [token_binary_str, signature].join
25
- # keys - An array of public keys to use for signature verification.
26
- #
27
- # returns a RecoveryToken if the payload has been verified and
28
- # deserializes correctly. Raises exceptions if any crypto fails.
29
- # Raises an error if the token's version field is not valid.
30
- def unseal(token_and_signature)
31
- token = RecoveryToken.parse(token_and_signature)
32
-
33
- unless token.version.to_i == PROTOCOL_VERSION
34
- raise TokenFormatError, "Version field must be #{PROTOCOL_VERSION}"
35
- end
36
-
37
- token_data, signature = partition_signed_token(token_and_signature, token)
38
- self.unseal_keys.each do |key|
39
- return token if Darrrr.encryptor.verify(token_data, signature, key)
40
- end
41
- raise CryptoError, "Recovery token signature was invalid"
42
- end
43
-
44
- # Split the binary token into the token data and the signature over the
45
- # data.
46
- #
47
- # token_and_signature: binary serialization of the token and signature for the token
48
- # recovery_token: a RecoveryToken object parsed from token_and_signature
49
- #
50
- # returns a two element array of [token, signature]
51
- private def partition_signed_token(token_and_signature, recovery_token)
52
- token_length = recovery_token.num_bytes
53
- [token_and_signature[0...token_length], token_and_signature[token_length..-1]]
54
- end
55
- end
56
- end
57
- end