darrrr 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +203 -0
- data/Rakefile +42 -0
- data/lib/darrrr.rb +9 -0
- data/lib/github/delegated_account_recovery.rb +178 -0
- data/lib/github/delegated_account_recovery/account_provider.rb +140 -0
- data/lib/github/delegated_account_recovery/constants.rb +19 -0
- data/lib/github/delegated_account_recovery/crypto_helper.rb +57 -0
- data/lib/github/delegated_account_recovery/cryptors/default/default_encryptor.rb +66 -0
- data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data.rb +92 -0
- data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data_io.rb +12 -0
- data/lib/github/delegated_account_recovery/provider.rb +125 -0
- data/lib/github/delegated_account_recovery/recovery_provider.rb +118 -0
- data/lib/github/delegated_account_recovery/recovery_token.rb +113 -0
- data/lib/github/delegated_account_recovery/serialization/recovery_token_reader.rb +22 -0
- data/lib/github/delegated_account_recovery/serialization/recovery_token_writer.rb +22 -0
- data/lib/github/delegated_account_recovery/version.rb +5 -0
- metadata +129 -0
@@ -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
|
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: []
|