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.
- checksums.yaml +4 -4
- data/README.md +12 -5
- data/lib/darrrr.rb +144 -2
- data/lib/darrrr/account_provider.rb +150 -0
- data/lib/darrrr/constants.rb +17 -0
- data/lib/darrrr/crypto_helper.rb +55 -0
- data/lib/darrrr/cryptors/default/default_encryptor.rb +68 -0
- data/lib/darrrr/cryptors/default/encrypted_data.rb +90 -0
- data/lib/darrrr/cryptors/default/encrypted_data_io.rb +10 -0
- data/lib/darrrr/provider.rb +157 -0
- data/lib/darrrr/recovery_provider.rb +129 -0
- data/lib/darrrr/recovery_token.rb +117 -0
- data/lib/darrrr/serialization/recovery_token_reader.rb +20 -0
- data/lib/darrrr/serialization/recovery_token_writer.rb +20 -0
- metadata +17 -18
- data/lib/github/delegated_account_recovery.rb +0 -178
- data/lib/github/delegated_account_recovery/account_provider.rb +0 -140
- data/lib/github/delegated_account_recovery/constants.rb +0 -19
- data/lib/github/delegated_account_recovery/crypto_helper.rb +0 -57
- data/lib/github/delegated_account_recovery/cryptors/default/default_encryptor.rb +0 -66
- data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data.rb +0 -92
- data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data_io.rb +0 -12
- data/lib/github/delegated_account_recovery/provider.rb +0 -125
- data/lib/github/delegated_account_recovery/recovery_provider.rb +0 -118
- data/lib/github/delegated_account_recovery/recovery_token.rb +0 -113
- data/lib/github/delegated_account_recovery/serialization/recovery_token_reader.rb +0 -22
- data/lib/github/delegated_account_recovery/serialization/recovery_token_writer.rb +0 -22
- data/lib/github/delegated_account_recovery/version.rb +0 -5
@@ -1,66 +0,0 @@
|
|
1
|
-
module GitHub
|
2
|
-
module DelegatedAccountRecovery
|
3
|
-
module DefaultEncryptor
|
4
|
-
class << self
|
5
|
-
include Constants
|
6
|
-
|
7
|
-
# Encrypts the data in an opaque way
|
8
|
-
#
|
9
|
-
# data: the secret to be encrypted
|
10
|
-
#
|
11
|
-
# returns a byte array representation of the data
|
12
|
-
def encrypt(data)
|
13
|
-
EncryptedData.build(data).to_binary_s
|
14
|
-
end
|
15
|
-
|
16
|
-
# Decrypts the data
|
17
|
-
#
|
18
|
-
# ciphertext: the byte array to be decrypted
|
19
|
-
#
|
20
|
-
# returns a string
|
21
|
-
def decrypt(ciphertext)
|
22
|
-
EncryptedData.parse(ciphertext).decrypt
|
23
|
-
end
|
24
|
-
|
25
|
-
|
26
|
-
# payload: binary serialized recovery token (to_binary_s).
|
27
|
-
#
|
28
|
-
# key: the private EC key used to sign the token
|
29
|
-
#
|
30
|
-
# returns signature in ASN.1 DER r + s sequence
|
31
|
-
def sign(payload, key)
|
32
|
-
digest = DIGEST.new.digest(payload)
|
33
|
-
ec = OpenSSL::PKey::EC.new(Base64.strict_decode64(key))
|
34
|
-
ec.dsa_sign_asn1(digest)
|
35
|
-
end
|
36
|
-
|
37
|
-
# payload: token in binary form
|
38
|
-
# signature: signature of the binary token
|
39
|
-
# key: the EC public key used to verify the signature
|
40
|
-
#
|
41
|
-
# returns true if signature validates the payload
|
42
|
-
def verify(payload, signature, key)
|
43
|
-
public_key_hex = format_key(key)
|
44
|
-
pkey = OpenSSL::PKey::EC.new(GROUP)
|
45
|
-
public_key_bn = OpenSSL::BN.new(public_key_hex, 16)
|
46
|
-
public_key = OpenSSL::PKey::EC::Point.new(GROUP, public_key_bn)
|
47
|
-
pkey.public_key = public_key
|
48
|
-
|
49
|
-
pkey.verify(DIGEST.new, signature, payload)
|
50
|
-
rescue OpenSSL::PKey::ECError, OpenSSL::PKey::PKeyError => e
|
51
|
-
raise CryptoError, "Unable verify recovery token"
|
52
|
-
end
|
53
|
-
|
54
|
-
private def format_key(key)
|
55
|
-
sequence, bit_string = OpenSSL::ASN1.decode(Base64.decode64(key)).value
|
56
|
-
unless bit_string.try(:tag) == OpenSSL::ASN1::BIT_STRING
|
57
|
-
raise CryptoError, "DER-encoded key did not contain a bit string"
|
58
|
-
end
|
59
|
-
bit_string.value.unpack("H*").first
|
60
|
-
rescue OpenSSL::ASN1::ASN1Error => e
|
61
|
-
raise CryptoError, "Invalid public key format. The key must be in ASN.1 format. #{e.message}"
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
@@ -1,92 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GitHub
|
4
|
-
module DelegatedAccountRecovery
|
5
|
-
class EncryptedData
|
6
|
-
extend Forwardable
|
7
|
-
CIPHER_OPTIONS = [:encrypt, :decrypt].freeze
|
8
|
-
CIPHER = "aes-256-gcm".freeze
|
9
|
-
CIPHER_VERSION = 0
|
10
|
-
# This is the NIST recommended minimum: http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
|
11
|
-
IV_LENGTH = 12
|
12
|
-
AUTH_TAG_LENGTH = 16
|
13
|
-
PROTOCOL_VERSION = 0
|
14
|
-
|
15
|
-
attr_reader :token_object
|
16
|
-
|
17
|
-
def_delegators :@token_object, :version, :iv, :auth_tag, :ciphertext,
|
18
|
-
:to_binary_s, :num_bytes
|
19
|
-
|
20
|
-
# token_object: an EncryptedDataIO instance
|
21
|
-
# instance.
|
22
|
-
def initialize(token_object)
|
23
|
-
raise ArgumentError, "Version must be #{PROTOCOL_VERSION}. Supplied: #{token_object.version}" unless token_object.version == CIPHER_VERSION
|
24
|
-
raise ArgumentError, "Auth Tag must be 16 bytes" unless token_object.auth_tag.length == AUTH_TAG_LENGTH
|
25
|
-
raise ArgumentError, "IV must be 12 bytes" unless token_object.iv.length == IV_LENGTH
|
26
|
-
@token_object = token_object
|
27
|
-
end
|
28
|
-
private_class_method :new
|
29
|
-
|
30
|
-
def decrypt
|
31
|
-
cipher = self.class.cipher(:decrypt)
|
32
|
-
cipher.iv = self.iv.to_binary_s
|
33
|
-
cipher.auth_tag = self.auth_tag.to_binary_s
|
34
|
-
cipher.auth_data = ""
|
35
|
-
cipher.update(self.ciphertext.to_binary_s) + cipher.final
|
36
|
-
rescue OpenSSL::Cipher::CipherError => e
|
37
|
-
raise CryptoError, "Unable to decrypt data: #{e}"
|
38
|
-
end
|
39
|
-
|
40
|
-
class << self
|
41
|
-
# data: the value to encrypt.
|
42
|
-
#
|
43
|
-
# returns an EncryptedData instance.
|
44
|
-
def build(data)
|
45
|
-
cipher = cipher(:encrypt)
|
46
|
-
iv = SecureRandom.random_bytes(EncryptedData::IV_LENGTH)
|
47
|
-
cipher.iv = iv
|
48
|
-
cipher.auth_data = ""
|
49
|
-
|
50
|
-
ciphertext = cipher.update(data.to_s) + cipher.final
|
51
|
-
|
52
|
-
token = EncryptedDataIO.new.tap do |edata|
|
53
|
-
edata.version = CIPHER_VERSION
|
54
|
-
edata.auth_tag = cipher.auth_tag.bytes
|
55
|
-
edata.iv = iv.bytes
|
56
|
-
edata.ciphertext = ciphertext.bytes
|
57
|
-
end
|
58
|
-
|
59
|
-
new(token)
|
60
|
-
end
|
61
|
-
|
62
|
-
# serialized_data: the binary representation of a token.
|
63
|
-
#
|
64
|
-
# returns an EncryptedData instance.
|
65
|
-
def parse(serialized_data)
|
66
|
-
data = new(EncryptedDataIO.new.read(serialized_data))
|
67
|
-
|
68
|
-
# be extra paranoid, oracles and stuff
|
69
|
-
if data.num_bytes != serialized_data.bytesize
|
70
|
-
raise CryptoError, "Encypted data field includes unexpected extra bytes"
|
71
|
-
end
|
72
|
-
|
73
|
-
data
|
74
|
-
rescue IOError => e
|
75
|
-
raise RecoveryTokenSerializationError, e.message
|
76
|
-
end
|
77
|
-
|
78
|
-
# DRY helper for generating cipher objects
|
79
|
-
def cipher(mode)
|
80
|
-
unless CIPHER_OPTIONS.include?(mode)
|
81
|
-
raise ArgumentError, "mode must be `encrypt` or `decrypt`"
|
82
|
-
end
|
83
|
-
|
84
|
-
OpenSSL::Cipher.new(EncryptedData::CIPHER).tap do |cipher|
|
85
|
-
cipher.send(mode)
|
86
|
-
cipher.key = [Darrrr.this_account_provider.symmetric_key].pack("H*")
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GitHub
|
4
|
-
module DelegatedAccountRecovery
|
5
|
-
class EncryptedDataIO < BinData::Record
|
6
|
-
uint8 :version
|
7
|
-
array :auth_tag, :type => :uint8, :initial_length => EncryptedData::AUTH_TAG_LENGTH
|
8
|
-
array :iv, :type => :uint8, :initial_length => EncryptedData::IV_LENGTH
|
9
|
-
array :ciphertext, :type => :uint8, :read_until => :eof
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
@@ -1,125 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GitHub
|
4
|
-
module DelegatedAccountRecovery
|
5
|
-
module Provider
|
6
|
-
RECOVERY_PROVIDER_CACHE_LENGTH = 60.seconds
|
7
|
-
MAX_RECOVERY_PROVIDER_CACHE_LENGTH = 5.minutes
|
8
|
-
include Constants
|
9
|
-
|
10
|
-
def self.included(base)
|
11
|
-
base.instance_eval do
|
12
|
-
# this represents the account/recovery provider on this web app
|
13
|
-
class << self
|
14
|
-
attr_accessor :this
|
15
|
-
|
16
|
-
def configure(&block)
|
17
|
-
raise ArgumentError, "Block required to configure #{self.name}" unless block_given?
|
18
|
-
raise ProviderConfigError, "#{self.name} already configured" if self.this
|
19
|
-
self.this = self.new.tap { |provider| provider.instance_eval(&block).freeze }
|
20
|
-
self.this.privacy_policy = DelegatedAccountRecovery.privacy_policy
|
21
|
-
self.this.icon_152px = DelegatedAccountRecovery.icon_152px
|
22
|
-
self.this.issuer = DelegatedAccountRecovery.authority
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def initialize(provider_origin = nil, attrs: nil)
|
29
|
-
self.issuer = provider_origin
|
30
|
-
load(attrs) if attrs
|
31
|
-
end
|
32
|
-
|
33
|
-
# Lazily loads attributes if attrs is nil. It makes an http call to the
|
34
|
-
# recovery provider's well-known config location and caches the response
|
35
|
-
# if it's valid json.
|
36
|
-
#
|
37
|
-
# attrs: optional way of building the provider without making an http call.
|
38
|
-
def load(attrs = nil)
|
39
|
-
body = attrs || fetch_config!
|
40
|
-
set_attrs!(body)
|
41
|
-
self
|
42
|
-
end
|
43
|
-
|
44
|
-
private def faraday
|
45
|
-
Faraday.new do |f|
|
46
|
-
f.adapter(Faraday.default_adapter)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
private def cache_config(response)
|
51
|
-
match = /max-age=(\d+)/.match(response.headers["cache-control"])
|
52
|
-
cache_age = if match
|
53
|
-
[match[1].to_i, MAX_RECOVERY_PROVIDER_CACHE_LENGTH].min
|
54
|
-
else
|
55
|
-
RECOVERY_PROVIDER_CACHE_LENGTH
|
56
|
-
end
|
57
|
-
DelegatedAccountRecovery.cache.try(:set, cache_key, response.body, cache_age)
|
58
|
-
end
|
59
|
-
|
60
|
-
private def cache_key
|
61
|
-
"recovery_provider_config:#{self.origin}:configuration"
|
62
|
-
end
|
63
|
-
|
64
|
-
private def fetch_config!
|
65
|
-
unless body = DelegatedAccountRecovery.cache.try(:get, cache_key)
|
66
|
-
response = faraday.get([self.origin, DelegatedAccountRecovery::WELL_KNOWN_CONFIG_PATH].join("/"))
|
67
|
-
if response.success?
|
68
|
-
cache_config(response)
|
69
|
-
else
|
70
|
-
raise ProviderConfigError.new("Unable to retrieve recovery provider config for #{self.origin}: #{response.status}: #{response.body[0..100]}")
|
71
|
-
end
|
72
|
-
|
73
|
-
body = response.body
|
74
|
-
end
|
75
|
-
|
76
|
-
JSON.parse(body)
|
77
|
-
rescue ::JSON::ParserError
|
78
|
-
raise ProviderConfigError.new("Unable to parse recovery provider config for #{self.origin}:#{body[0..100]}")
|
79
|
-
end
|
80
|
-
|
81
|
-
private def set_attrs!(context)
|
82
|
-
self.class::REQUIRED_FIELDS.each do |attr|
|
83
|
-
value = context[attr.to_s.tr("_", "-")]
|
84
|
-
self.instance_variable_set("@#{attr}", value)
|
85
|
-
end
|
86
|
-
|
87
|
-
if errors.any?
|
88
|
-
raise ProviderConfigError.new("Unable to parse recovery provider config for #{self.origin}: #{errors.join(", ")}")
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
private def errors
|
93
|
-
errors = []
|
94
|
-
self.class::REQUIRED_FIELDS.each do |field|
|
95
|
-
unless self.instance_variable_get("@#{field}")
|
96
|
-
errors << "#{field} not set"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
self.class::URL_FIELDS.each do |field|
|
101
|
-
begin
|
102
|
-
uri = Addressable::URI.parse(self.instance_variable_get("@#{field}"))
|
103
|
-
if !DelegatedAccountRecovery.allow_unsafe_urls && uri.try(:scheme) != "https"
|
104
|
-
errors << "#{field} must be an https URL"
|
105
|
-
end
|
106
|
-
rescue Addressable::URI::InvalidURIError
|
107
|
-
errors << "#{field} must be a valid URL"
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
if self.is_a? RecoveryProvider
|
112
|
-
unless self.token_max_size.to_i > 0
|
113
|
-
errors << "token max size must be an integer"
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
unless self.unseal_keys.try(:any?)
|
118
|
-
errors << "No public key provided"
|
119
|
-
end
|
120
|
-
|
121
|
-
errors
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
@@ -1,118 +0,0 @@
|
|
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
|
@@ -1,113 +0,0 @@
|
|
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
|