darrrr 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHub
4
+ module DelegatedAccountRecovery
5
+ class RecoveryProvider
6
+ include Provider
7
+ include CryptoHelper
8
+
9
+ INTEGER_FIELDS = [:token_max_size]
10
+ BASE64_FIELDS = [:countersign_pubkeys_secp256r1]
11
+ URL_FIELDS = [
12
+ :issuer, :save_token,
13
+ :recover_account, :privacy_policy
14
+ ]
15
+ REQUIRED_FIELDS = URL_FIELDS + INTEGER_FIELDS + BASE64_FIELDS
16
+
17
+ attr_accessor(*REQUIRED_FIELDS)
18
+ attr_accessor :save_token_async_api_iframe # optional
19
+ attr_accessor :signing_private_key
20
+ alias :origin :issuer
21
+
22
+ # optional field
23
+ attr_accessor :icon_152px
24
+
25
+ # Used to serve content at /.well-known/delegated-account-recovery/configuration
26
+ def to_h
27
+ {
28
+ "issuer" => self.issuer,
29
+ "countersign-pubkeys-secp256r1" => self.countersign_pubkeys_secp256r1.dup,
30
+ "token-max-size" => self.token_max_size,
31
+ "save-token" => self.save_token,
32
+ "recover-account" => self.recover_account,
33
+ "save-token-async-api-iframe" => self.save_token_async_api_iframe,
34
+ "privacy-policy" => self.privacy_policy
35
+ }
36
+ end
37
+
38
+ # The CryptoHelper defines an `unseal` method that requires us to define
39
+ # a `unseal_keys` method that will return the set of keys that are valid
40
+ # when verifying the signature on a sealed key.
41
+ def unseal_keys
42
+ countersign_pubkeys_secp256r1
43
+ end
44
+
45
+ # The URL representing the location of the token. Used to initiate a recovery.
46
+ #
47
+ # token_id: the shared ID representing a token.
48
+ def recovery_url(token_id)
49
+ [self.recover_account, "?token_id=", URI.escape(token_id)].join
50
+ end
51
+
52
+ # Takes a binary representation of a token and signs if for a given
53
+ # account provider. Do not pass in a RecoveryToken object. The wrapping
54
+ # data structure is identical to the structure it's wrapping in format.
55
+ #
56
+ # token: the to_binary_s or binary representation of the recovery token
57
+ #
58
+ # returns a Base64 encoded representation of the countersigned token
59
+ # and the signature over the token.
60
+ def countersign_token(token)
61
+ begin
62
+ account_provider = RecoveryToken.account_provider_issuer(token)
63
+ rescue RecoveryTokenSerializationError, UnknownProviderError
64
+ raise TokenFormatError, "Could not determine provider"
65
+ end
66
+
67
+ counter_recovery_token = RecoveryToken.build(
68
+ issuer: self,
69
+ audience: account_provider,
70
+ type: COUNTERSIGNED_RECOVERY_TOKEN_TYPE
71
+ )
72
+
73
+ counter_recovery_token.data = token
74
+ seal(counter_recovery_token)
75
+ end
76
+
77
+ # Validate the token according to the processing instructions for the
78
+ # save-token endpoint.
79
+ #
80
+ # Returns a validated token
81
+ def validate_recovery_token!(token)
82
+ errors = []
83
+
84
+ # 1. Authenticate the User. The exact nature of how the Recovery Provider authenticates the User is beyond the scope of this specification.
85
+ # handled in before_filter
86
+
87
+ # 4. Retrieve the Account Provider configuration as described in Section 2 using the issuer field of the token as the subject.
88
+ begin
89
+ account_provider = RecoveryToken.account_provider_issuer(token)
90
+ rescue RecoveryTokenSerializationError, UnknownProviderError, TokenFormatError => e
91
+ raise RecoveryTokenError, "Could not determine provider: #{e.message}"
92
+ end
93
+
94
+ # 2. Parse the token.
95
+ # 3. Validate that the version value is 0.
96
+ # 5. Validate the signature over the token according to processing rules for the algorithm implied by the version.
97
+ begin
98
+ recovery_token = account_provider.unseal(token)
99
+ rescue CryptoError => e
100
+ raise RecoveryTokenError.new("Unable to verify signature of token")
101
+ rescue TokenFormatError => e
102
+ raise RecoveryTokenError.new(e.message)
103
+ end
104
+
105
+ # 6. Validate that the audience field of the token identifies an origin which the provider considers itself authoritative for. (Often the audience will be same-origin with the Recovery Provider, but other values may be acceptable, e.g. "https://mail.example.com" and "https://social.example.com" may be acceptable audiences for "https://recovery.example.com".)
106
+ unless self.origin == recovery_token.audience
107
+ raise RecoveryTokenError.new("Unnacceptable audience")
108
+ end
109
+
110
+ if DateTime.parse(recovery_token.issued_time).utc < (Time.now - CLOCK_SKEW).utc
111
+ raise RecoveryTokenError.new("Issued at time is too far in the past")
112
+ end
113
+
114
+ recovery_token
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Handles binary serialization/deserialization of recovery token data. It does
4
+ # not manage signing/verification of tokens.
5
+
6
+ module GitHub
7
+ module DelegatedAccountRecovery
8
+ class RecoveryToken
9
+ extend Forwardable
10
+
11
+ attr_reader :token_object
12
+
13
+ def_delegators :@token_object, :token_id, :issuer, :issued_time, :options,
14
+ :audience, :binding_data, :data, :version, :to_binary_s, :num_bytes, :data=, :token_type=, :token_type
15
+
16
+ BASE64_CHARACTERS = /\A[0-9a-zA-Z+\/=]+\z/
17
+
18
+ # Typically, you would not call `new` directly but instead use `build`
19
+ # and `parse`
20
+ #
21
+ # token_object: a RecoveryTokenWriter/RecoveryTokenReader instance
22
+ def initialize(token_object)
23
+ @token_object = token_object
24
+ end
25
+ private_class_method :new
26
+
27
+ def decode
28
+ Darrrr.encryptor.decrypt(self.data)
29
+ end
30
+
31
+ # A globally known location of the token, used to initiate a recovery
32
+ def state_url
33
+ [DelegatedAccountRecovery.recovery_provider(self.audience).recover_account, "id=#{CGI::escape(token_id.to_hex)}"].join("?")
34
+ end
35
+
36
+ class << self
37
+ # data: the value that will be encrypted by EncryptedData.
38
+ # recovery_provider: the provider for which we are building the token.
39
+ # binding_data: a value retrieved from the recovery provider for this
40
+ # token.
41
+ #
42
+ # returns a RecoveryToken.
43
+ def build(issuer:, audience:, type:)
44
+ token = RecoveryTokenWriter.new.tap do |token|
45
+ token.token_id = SecureRandom.random_bytes(16).bytes.to_a
46
+ token.issuer = issuer.origin
47
+ token.issued_time = Time.now.utc.iso8601
48
+ token.options = 0 # when the token-status endpoint is implemented, change this to 1
49
+ token.audience = audience.origin
50
+ token.version = DelegatedAccountRecovery::PROTOCOL_VERSION
51
+ token.token_type = type
52
+ end
53
+ new(token)
54
+ end
55
+
56
+ # serialized_data: a binary string representation of a RecoveryToken.
57
+ #
58
+ # returns a RecoveryToken.
59
+ def parse(serialized_data)
60
+ new RecoveryTokenReader.new.read(serialized_data)
61
+ rescue IOError => e
62
+ message = e.message
63
+ if serialized_data =~ BASE64_CHARACTERS
64
+ message = "#{message}: did you forget to Base64.strict_decode64 this value?"
65
+ end
66
+ raise RecoveryTokenSerializationError, message
67
+ end
68
+
69
+ # Extract a recovery provider from a token based on the token type.
70
+ #
71
+ # serialized_data: a binary string representation of a RecoveryToken.
72
+ #
73
+ # returns the recovery provider for the coutnersigned token or raises an
74
+ # error if the token is a recovery token
75
+ def recovery_provider_issuer(serialized_data)
76
+ issuer(serialized_data, DelegatedAccountRecovery::COUNTERSIGNED_RECOVERY_TOKEN_TYPE)
77
+ end
78
+
79
+ # Extract an account provider from a token based on the token type.
80
+ #
81
+ # serialized_data: a binary string representation of a RecoveryToken.
82
+ #
83
+ # returns the account provider for the recovery token or raises an error
84
+ # if the token is a countersigned token
85
+ def account_provider_issuer(serialized_data)
86
+ issuer(serialized_data, DelegatedAccountRecovery::RECOVERY_TOKEN_TYPE)
87
+ end
88
+
89
+ # Convenience method to find the issuer of the token
90
+ #
91
+ # serialized_data: a binary string representation of a RecoveryToken.
92
+ #
93
+ # raises an error if the token is the not the expected type
94
+ # returns the account provider or recovery provider instance based on the
95
+ # token type
96
+ private def issuer(serialized_data, token_type)
97
+ parsed_token = parse(serialized_data)
98
+ raise TokenFormatError, "Token type must be #{token_type}" unless parsed_token.token_type == token_type
99
+
100
+ issuer = parsed_token.issuer
101
+ case token_type
102
+ when DelegatedAccountRecovery::RECOVERY_TOKEN_TYPE
103
+ DelegatedAccountRecovery.account_provider(issuer)
104
+ when DelegatedAccountRecovery::COUNTERSIGNED_RECOVERY_TOKEN_TYPE
105
+ DelegatedAccountRecovery.recovery_provider(issuer)
106
+ else
107
+ raise RecoveryTokenError, "Could not determine provider"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHub
4
+ module DelegatedAccountRecovery
5
+ class RecoveryTokenReader < BinData::Record
6
+ uint8 :version
7
+ uint8 :token_type
8
+ array :token_id, :type => :uint8, :read_until => lambda { index + 1 == DelegatedAccountRecovery::TOKEN_ID_BYTE_LENGTH }
9
+ uint8 :options
10
+ uint16be :issuer_length
11
+ string :issuer, :read_length => :issuer_length
12
+ uint16be :audience_length
13
+ string :audience, :read_length => :audience_length
14
+ uint16be :issued_time_length
15
+ string :issued_time, :read_length => :issued_time_length
16
+ uint16be :data_length
17
+ string :data, :read_length => :data_length
18
+ uint16be :binding_data_length
19
+ string :binding_data, :read_length => :binding_data_length
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHub
4
+ module DelegatedAccountRecovery
5
+ class RecoveryTokenWriter < BinData::Record
6
+ uint8 :version
7
+ uint8 :token_type
8
+ array :token_id, :type => :uint8, :initial_length => DelegatedAccountRecovery::TOKEN_ID_BYTE_LENGTH
9
+ uint8 :options
10
+ uint16be :issuer_length, :value => lambda { issuer.length }
11
+ string :issuer
12
+ uint16be :audience_length, :value => lambda { audience.length }
13
+ string :audience
14
+ uint16be :issued_time_length, :value => lambda { issued_time.length }
15
+ string :issued_time
16
+ uint16be :data_length, :value => lambda { data.length }
17
+ string :data
18
+ uint16be :binding_data_length, :value => lambda { binding_data.length }
19
+ string :binding_data
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ module GitHub
2
+ module DelegatedAccountRecovery
3
+ VERSION = "0.0.2"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: darrrr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Neil Matatall
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-04-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bindata
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: addressable
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: multi_json
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: See https://www.facebook.com/notes/protect-the-graph/improving-account-security-with-delegated-recovery/1833022090271267/
84
+ email: opensource+darrrr@github.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - LICENSE
90
+ - README.md
91
+ - Rakefile
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: []
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.6.10
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Client library for the Delegated Recovery spec
129
+ test_files: []