Class: Darrrr::AccountProvider

Inherits:
Object
  • Object
show all
Includes:
CryptoHelper, Provider
Defined in:
lib/darrrr/account_provider.rb

Constant Summary collapse

PRIVATE_FIELDS =

Only applicable when acting as a recovery provider

[:symmetric_key, :signing_private_key]
FIELDS =
[:tokensign_pubkeys_secp256r1].freeze
URL_FIELDS =
[:issuer, :save_token_return, :recover_account_return,
:privacy_policy, :icon_152px].freeze
REQUIRED_FIELDS =

These are the fields required by the spec

FIELDS + URL_FIELDS

Constants included from Provider

Provider::MAX_RECOVERY_PROVIDER_CACHE_LENGTH, Provider::RECOVERY_PROVIDER_CACHE_LENGTH, Provider::REQUIRED_CRYPTO_OPS

Constants included from Constants

Constants::CLOCK_SKEW, Constants::COUNTERSIGNED_RECOVERY_TOKEN_TYPE, Constants::DIGEST, Constants::GROUP, Constants::PRIME_256_V1, Constants::PROTOCOL_VERSION, Constants::RECOVERY_TOKEN_TYPE, Constants::TOKEN_ID_BYTE_LENGTH, Constants::WELL_KNOWN_CONFIG_PATH

Instance Method Summary collapse

Methods included from Provider

#custom_encryptor=, #encryptor, included, #initialize, #load, #with_encryptor

Methods included from CryptoHelper

#seal, #unseal

Instance Method Details

#dangerous_unverified_recovery_token(countersigned_token) ⇒ Object

Parses a countersigned_token and returns the nested recovery token WITHOUT verifying any signatures. This should only be used if no user context can be identified or if we're extracting issuer information.


69
70
71
72
# File 'lib/darrrr/account_provider.rb', line 69

def dangerous_unverified_recovery_token(countersigned_token)
  parsed_countersigned_token = RecoveryToken.parse(Base64.strict_decode64(countersigned_token))
  RecoveryToken.parse(parsed_countersigned_token.data)
end

#encryptor_keyObject


74
75
76
# File 'lib/darrrr/account_provider.rb', line 74

def encryptor_key
  :darrrr_account_provider_encryptor
end

#generate_recovery_token(data:, audience:, context: nil, options: 0x00) ⇒ Object

Generates a binary token with an encrypted arbitrary data payload.

data: value to encrypt in the token provider: the recovery provider/audience of the token context: arbitrary data passed on to underlying crypto operations options: the value to set for the options byte

returns a [RecoveryToken, b64 encoded sealed_token] tuple


59
60
61
62
63
64
# File 'lib/darrrr/account_provider.rb', line 59

def generate_recovery_token(data:, audience:, context: nil, options: 0x00)
  token = RecoveryToken.build(issuer: self, audience: audience, type: RECOVERY_TOKEN_TYPE, options: options)
  token.data = self.encryptor.encrypt(data, self, context)

  [token, seal(token, context)]
end

#to_hObject

Used to serve content at /.well-known/delegated-account-recovery/configuration


40
41
42
43
44
45
46
47
48
49
# File 'lib/darrrr/account_provider.rb', line 40

def to_h
  {
    "issuer" => self.issuer,
    "tokensign-pubkeys-secp256r1" => self.unseal_keys.dup,
    "save-token-return" => self.save_token_return,
    "recover-account-return" => self.,
    "privacy-policy" => self.privacy_policy,
    "icon-152px" => self.icon_152px
  }
end

#unseal_keys(context = nil) ⇒ Object

The CryptoHelper defines an `unseal` method that requires us to define a `unseal_keys` method that will return the set of keys that are valid when verifying the signature on a sealed key.

returns the value of `tokensign_pubkeys_secp256r1` or executes a proc passing `self` as the first argument.


31
32
33
34
35
36
37
# File 'lib/darrrr/account_provider.rb', line 31

def unseal_keys(context = nil)
  if @tokensign_pubkeys_secp256r1.respond_to?(:call)
    @tokensign_pubkeys_secp256r1.call(context)
  else
    @tokensign_pubkeys_secp256r1
  end
end

#validate_countersigned_recovery_token!(countersigned_token, context = {}) ⇒ Object

Validates the countersigned recovery token by verifying the signature of the countersigned token, parsing out the origin recovery token, verifying the signature on the recovery token, and finally decrypting the data in the origin recovery token.

countersigned_token: our original recovery token wrapped in recovery token instance that is signed by the recovery provider. context: arbitrary data to be passed to Provider#unseal.

returns a verified recovery token or raises an error if the token fails validation.


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/darrrr/account_provider.rb', line 89

def validate_countersigned_recovery_token!(countersigned_token, context = {})
  # 5. Validate the the issuer field is present in the token,
  # and that it matches the audience field in the original countersigned token.
  begin
    recovery_provider = RecoveryToken.recovery_provider_issuer(Base64.strict_decode64(countersigned_token))
  rescue RecoveryTokenSerializationError => e
    raise CountersignedTokenError.new("Countersigned token is invalid: " + e.message, :countersigned_token_parse_error)
  rescue UnknownProviderError => e
    raise CountersignedTokenError.new(e.message, :recovery_token_invalid_issuer)
  end

  # 1. Parse the countersigned-token.
  # 2. Validate that the version field is 0.
  # 7. Retrieve the current Recovery Provider configuration as described in Section 2.
  # 8. Validate that the counter-signed token signature validates with a current element of the countersign-pubkeys-secp256r1 array.
  begin
    parsed_countersigned_token = recovery_provider.unseal(Base64.strict_decode64(countersigned_token), context)
  rescue TokenFormatError => e
    raise CountersignedTokenError.new(e.message, :countersigned_invalid_token_version)
  rescue CryptoError
    raise CountersignedTokenError.new("Countersigned token has an invalid signature", :countersigned_invalid_signature)
  end

  # 3. De-serialize the original recovery token from the data field.
  # 4. Validate the signature on the original recovery token.
  begin
    recovery_token = self.unseal(parsed_countersigned_token.data, context)
  rescue RecoveryTokenSerializationError => e
    raise CountersignedTokenError.new("Nested recovery token is invalid: " + e.message, :recovery_token_token_parse_error)
  rescue TokenFormatError => e
    raise CountersignedTokenError.new("Nested recovery token format error: #{e.message}", :recovery_token_invalid_token_type)
  rescue CryptoError
    raise CountersignedTokenError.new("Nested recovery token has an invalid signature", :recovery_token_invalid_signature)
  end

  # 5. Validate the the issuer field is present in the countersigned-token,
  # and that it matches the audience field in the original token.

  countersigned_token_issuer = parsed_countersigned_token.issuer
  if countersigned_token_issuer.blank? || countersigned_token_issuer != recovery_token.audience || recovery_provider.origin != countersigned_token_issuer
    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)
  end

  # 6. Validate the token binding for the countersigned token, if present.
  # (the token binding for the inner token is not relevant)
  # TODO not required, to be implemented later

  # 9. Decrypt the data field from the original recovery token and parse its information, if present.
  # no decryption here is attempted. Attempts to call `decode` will just fail.

  # 10. Apply any additional processing which provider-specific data in the opaque data portion may indicate is necessary.
  begin
    if DateTime.parse(parsed_countersigned_token.issued_time).utc < (Time.now - CLOCK_SKEW).utc
      raise CountersignedTokenError.new("Countersigned recovery token issued at time is too far in the past", :stale_token)
    end
  rescue ArgumentError
    raise CountersignedTokenError.new("Invalid countersigned token issued time", :invalid_issued_time)
  end

  recovery_token
end