darrrr 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29975ab5312f2fef2ceceac4fe40abe1f011ca64
|
4
|
+
data.tar.gz: 30001607c84a89388bf5b19690cd092638e895a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84d5112ca262d1575d46faf457227e0e5aa1532799f746af80be60721aee888ae31d52b5dd19618a3ac525129211145901444491f2a73b76b71e27387aaf4878
|
7
|
+
data.tar.gz: 5acf83a5071edcf20085dcda3c87732ae72e5caffea20c82e577577895a3186ee7d9ed4ab5636c324c42477b893424f030a5db119cc6a71bdf1486a5368b9747
|
data/README.md
CHANGED
@@ -4,11 +4,13 @@ The Delegated Account Recovery Rigid Reusable Ruby (aka D.a.r.r.r.r. or "Darrrr"
|
|
4
4
|
|
5
5
|
Along with a fully featured library, a proof of concept application is provided in this repo.
|
6
6
|
|
7
|
+
![](/logos/dar-logo-transparent-small.png)
|
8
|
+
|
7
9
|
## Configuration
|
8
10
|
|
9
11
|
An account provider (e.g. GitHub) is someone who stores a token with someone else (a recovery provider e.g. Facebook) in order to grant access to an account.
|
10
12
|
|
11
|
-
In `config/initializers` or any location that is run during application setup, add a file
|
13
|
+
In `config/initializers` or any location that is run during application setup, add a file. **NOTE:** `proc`s are valid values for `countersign_pubkeys_secp256r1` and `tokensign_pubkeys_secp256r1`
|
12
14
|
|
13
15
|
```ruby
|
14
16
|
Darrrr.authority = "http://localhost:9292"
|
@@ -19,14 +21,14 @@ Darrrr.icon_152px = "#{Darrrr.authority}/icon.png"
|
|
19
21
|
Darrrr::AccountProvider.configure do |config|
|
20
22
|
config.signing_private_key = ENV["ACCOUNT_PROVIDER_PRIVATE_KEY"]
|
21
23
|
config.symmetric_key = ENV["TOKEN_DATA_AES_KEY"]
|
22
|
-
config.tokensign_pubkeys_secp256r1 = [ENV["ACCOUNT_PROVIDER_PUBLIC_KEY"]]
|
24
|
+
config.tokensign_pubkeys_secp256r1 = [ENV["ACCOUNT_PROVIDER_PUBLIC_KEY"]] || lambda { |provider, context| "you wouldn't do this in real life but procs are supported for this value" }
|
23
25
|
config.save_token_return = "#{Darrrr.authority}/account-provider/save-token-return"
|
24
26
|
config.recover_account_return = "#{Darrrr.authority}/account-provider/recover-account-return"
|
25
27
|
end
|
26
28
|
|
27
29
|
Darrrr::RecoveryProvider.configure do |config|
|
28
30
|
config.signing_private_key = ENV["RECOVERY_PROVIDER_PRIVATE_KEY"]
|
29
|
-
config.countersign_pubkeys_secp256r1 = [ENV["RECOVERY_PROVIDER_PUBLIC_KEY"]]
|
31
|
+
config.countersign_pubkeys_secp256r1 = [ENV["RECOVERY_PROVIDER_PUBLIC_KEY"]] || lambda { |provider, context| "you wouldn't do this in real life but procs are supported for this value" }
|
30
32
|
config.token_max_size = 8192
|
31
33
|
config.save_token = "#{Darrrr.authority}/recovery-provider/save-token"
|
32
34
|
config.recover_account = "#{Darrrr.authority}/recovery-provider/recover-account"
|
@@ -63,14 +65,15 @@ Create a module that responds to `Module.sign`, `Module.verify`, `Module.decrypt
|
|
63
65
|
|
64
66
|
### Global config
|
65
67
|
|
66
|
-
Set `Darrrr.custom_encryptor = MyCustomEncryptor`
|
68
|
+
Set `Darrrr.this_account_provider.custom_encryptor = MyCustomEncryptor`
|
69
|
+
Set `Darrrr.this_recovery_provider.custom_encryptor = MyCustomEncryptor`
|
67
70
|
|
68
71
|
### On-demand
|
69
72
|
|
70
73
|
```ruby
|
71
74
|
Darrrr.with_encryptor(MyCustomEncryptor) do
|
72
75
|
# perform DAR actions using MyCustomEncryptor as the crypto provider
|
73
|
-
|
76
|
+
recovery_token, sealed_token = Darrrr.this_account_provider.generate_recovery_token(data: "foo", audience: recovery_provider, context: { user: current_user })
|
74
77
|
end
|
75
78
|
```
|
76
79
|
|
@@ -201,3 +204,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
201
204
|
## License
|
202
205
|
|
203
206
|
`darrrr` is licensed under the [MIT license](LICENSE.md).
|
207
|
+
|
208
|
+
The MIT license grant is not for GitHub's trademarks, which include the logo designs. GitHub reserves all trademark and copyright rights in and to all GitHub trademarks. GitHub's logos include, for instance, the stylized designs that include "logo" in the file title in the following folder: [logos](/logos).
|
209
|
+
|
210
|
+
GitHub® and its stylized versions and the Invertocat mark are GitHub's Trademarks or registered Trademarks. When using GitHub's logos, be sure to follow the GitHub [logo guidelines](https://github.com/logos).
|
data/lib/darrrr.rb
CHANGED
@@ -1,9 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "bindata"
|
2
4
|
require "openssl"
|
3
5
|
require "addressable"
|
4
6
|
require "forwardable"
|
5
7
|
require "faraday"
|
6
8
|
|
7
|
-
require_relative "
|
9
|
+
require_relative "darrrr/constants"
|
10
|
+
require_relative "darrrr/crypto_helper"
|
11
|
+
require_relative "darrrr/recovery_token"
|
12
|
+
require_relative "darrrr/provider"
|
13
|
+
require_relative "darrrr/account_provider"
|
14
|
+
require_relative "darrrr/recovery_provider"
|
15
|
+
require_relative "darrrr/serialization/recovery_token_writer"
|
16
|
+
require_relative "darrrr/serialization/recovery_token_reader"
|
17
|
+
require_relative "darrrr/cryptors/default/default_encryptor"
|
18
|
+
require_relative "darrrr/cryptors/default/encrypted_data"
|
19
|
+
require_relative "darrrr/cryptors/default/encrypted_data_io"
|
20
|
+
|
21
|
+
module Darrrr
|
22
|
+
class DelegatedRecoveryError < StandardError; end
|
23
|
+
# Represents a binary serialization error
|
24
|
+
class RecoveryTokenSerializationError < DelegatedRecoveryError; end
|
25
|
+
|
26
|
+
# Represents invalid data within a valid token
|
27
|
+
# (e.g. wrong `version` number, invalid token `type`)
|
28
|
+
class TokenFormatError < DelegatedRecoveryError; end
|
29
|
+
|
30
|
+
# Represents all crypto errors
|
31
|
+
# (e.g. invalid keys, invalid signature, decrypt failures)
|
32
|
+
class CryptoError < DelegatedRecoveryError; end
|
33
|
+
|
34
|
+
# Represents providers supplying invalid configurations
|
35
|
+
# (e.g. non-https URLs, missing required fields, http errors)
|
36
|
+
class ProviderConfigError < DelegatedRecoveryError; end
|
37
|
+
|
38
|
+
# Represents an invalid countersigned recovery token.
|
39
|
+
# (e.g. invalid signature, invalid nested token, unregistered provider, stale tokens)
|
40
|
+
class CountersignedTokenError < DelegatedRecoveryError
|
41
|
+
attr_reader :key
|
42
|
+
def initialize(message, key)
|
43
|
+
super(message)
|
44
|
+
@key = key
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Represents an invalid recovery token.
|
49
|
+
# (e.g. invalid signature, unregistered provider, stale tokens)
|
50
|
+
class RecoveryTokenError < DelegatedRecoveryError; end
|
51
|
+
|
52
|
+
# Represents a call to to `recovery_provider` or `account_provider` that
|
53
|
+
# has not been registered.
|
54
|
+
class UnknownProviderError < DelegatedRecoveryError; end
|
55
|
+
|
56
|
+
include Constants
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# recovery provider data is only loaded (and cached) upon use.
|
60
|
+
attr_accessor :recovery_providers, :account_providers, :cache, :allow_unsafe_urls,
|
61
|
+
:privacy_policy, :icon_152px, :authority
|
62
|
+
|
63
|
+
# Find and load remote recovery provider configuration data.
|
64
|
+
#
|
65
|
+
# provider_origin: the origin that contains the config data in a well-known
|
66
|
+
# location.
|
67
|
+
def recovery_provider(provider_origin)
|
68
|
+
unless self.recovery_providers
|
69
|
+
raise "No recovery providers configured"
|
70
|
+
end
|
71
|
+
|
72
|
+
if provider_origin == this_recovery_provider.origin
|
73
|
+
this_recovery_provider
|
74
|
+
elsif self.recovery_providers.include?(provider_origin)
|
75
|
+
RecoveryProvider.new(provider_origin).load
|
76
|
+
else
|
77
|
+
raise UnknownProviderError, "Unknown recovery provider: #{provider_origin}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Permit an origin to act as a recovery provider.
|
82
|
+
#
|
83
|
+
# provider_origin: the origin to permit
|
84
|
+
def register_recovery_provider(provider_origin)
|
85
|
+
self.recovery_providers ||= []
|
86
|
+
self.recovery_providers << provider_origin
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find and load remote account provider configuration data.
|
90
|
+
#
|
91
|
+
# provider_origin: the origin that contains the config data in a well-known
|
92
|
+
# location.
|
93
|
+
def account_provider(provider_origin)
|
94
|
+
unless self.account_providers
|
95
|
+
raise "No account providers configured"
|
96
|
+
end
|
97
|
+
if provider_origin == this_account_provider.origin
|
98
|
+
this_account_provider
|
99
|
+
elsif self.account_providers.include?(provider_origin)
|
100
|
+
AccountProvider.new(provider_origin).load
|
101
|
+
else
|
102
|
+
raise UnknownProviderError, "Unknown account provider: #{provider_origin}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Permit an origin to act as an account provider.
|
107
|
+
#
|
108
|
+
# account_origin: the origin to permit
|
109
|
+
def register_account_provider(account_origin)
|
110
|
+
self.account_providers ||= []
|
111
|
+
self.account_providers << account_origin
|
112
|
+
end
|
113
|
+
|
114
|
+
# Provide a reference to the account provider configuration for this web app
|
115
|
+
def this_account_provider
|
116
|
+
AccountProvider.this
|
117
|
+
end
|
118
|
+
|
119
|
+
# Provide a reference to the recovery provider configuration for this web app
|
120
|
+
def this_recovery_provider
|
121
|
+
RecoveryProvider.this
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns a hash of all configuration values, recovery and account provider.
|
125
|
+
def account_and_recovery_provider_config
|
126
|
+
provider_data = Darrrr.this_account_provider.try(:to_h) || {}
|
127
|
+
|
128
|
+
if Darrrr.this_recovery_provider
|
129
|
+
provider_data.merge!(recovery_provider_config) do |key, lhs, rhs|
|
130
|
+
unless lhs == rhs
|
131
|
+
raise ArgumentError, "inconsistent config value detected #{key}: #{lhs} != #{rhs}"
|
132
|
+
end
|
133
|
+
|
134
|
+
lhs
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
provider_data
|
139
|
+
end
|
140
|
+
|
141
|
+
# returns the account provider information in hash form
|
142
|
+
def account_provider_config
|
143
|
+
this_account_provider.to_h
|
144
|
+
end
|
8
145
|
|
9
|
-
|
146
|
+
# returns the account provider information in hash form
|
147
|
+
def recovery_provider_config
|
148
|
+
this_recovery_provider.to_h
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Darrrr
|
4
|
+
class AccountProvider
|
5
|
+
include CryptoHelper
|
6
|
+
include Provider
|
7
|
+
private :seal
|
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_writer *REQUIRED_FIELDS
|
20
|
+
attr_writer *PRIVATE_FIELDS
|
21
|
+
attr_reader *(REQUIRED_FIELDS - [:tokensign_pubkeys_secp256r1])
|
22
|
+
|
23
|
+
alias :origin :issuer
|
24
|
+
|
25
|
+
# The CryptoHelper defines an `unseal` method that requires us to
|
26
|
+
# define a `unseal_keys` method that will return the set of keys that
|
27
|
+
# are valid when verifying the signature on a sealed key.
|
28
|
+
#
|
29
|
+
# returns the value of `tokensign_pubkeys_secp256r1` or executes a proc
|
30
|
+
# passing `self` as the first argument.
|
31
|
+
def unseal_keys(context = nil)
|
32
|
+
if @tokensign_pubkeys_secp256r1.respond_to?(:call)
|
33
|
+
@tokensign_pubkeys_secp256r1.call(context)
|
34
|
+
else
|
35
|
+
@tokensign_pubkeys_secp256r1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Used to serve content at /.well-known/delegated-account-recovery/configuration
|
40
|
+
def to_h
|
41
|
+
{
|
42
|
+
"issuer" => self.issuer,
|
43
|
+
"tokensign-pubkeys-secp256r1" => self.unseal_keys.dup,
|
44
|
+
"save-token-return" => self.save_token_return,
|
45
|
+
"recover-account-return" => self.recover_account_return,
|
46
|
+
"privacy-policy" => self.privacy_policy,
|
47
|
+
"icon-152px" => self.icon_152px
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generates a binary token with an encrypted arbitrary data payload.
|
52
|
+
#
|
53
|
+
# data: value to encrypt in the token
|
54
|
+
# provider: the recovery provider/audience of the token
|
55
|
+
# context: arbitrary data passed on to underlying crypto operations
|
56
|
+
#
|
57
|
+
# returns a [RecoveryToken, b64 encoded sealed_token] tuple
|
58
|
+
def generate_recovery_token(data:, audience:, context: nil)
|
59
|
+
token = RecoveryToken.build(issuer: self, audience: audience, type: RECOVERY_TOKEN_TYPE)
|
60
|
+
token.data = self.encryptor.encrypt(data, self, context)
|
61
|
+
|
62
|
+
[token, seal(token, context)]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parses a countersigned_token and returns the nested recovery token
|
66
|
+
# WITHOUT verifying any signatures. This should only be used if no user
|
67
|
+
# context can be identified or if we're extracting issuer information.
|
68
|
+
def dangerous_unverified_recovery_token(countersigned_token)
|
69
|
+
parsed_countersigned_token = RecoveryToken.parse(Base64.strict_decode64(countersigned_token))
|
70
|
+
RecoveryToken.parse(parsed_countersigned_token.data)
|
71
|
+
end
|
72
|
+
|
73
|
+
def encryptor_key
|
74
|
+
:darrrr_account_provider_encryptor
|
75
|
+
end
|
76
|
+
|
77
|
+
# Validates the countersigned recovery token by verifying the signature
|
78
|
+
# of the countersigned token, parsing out the origin recovery token,
|
79
|
+
# verifying the signature on the recovery token, and finally decrypting
|
80
|
+
# the data in the origin recovery token.
|
81
|
+
#
|
82
|
+
# countersigned_token: our original recovery token wrapped in recovery
|
83
|
+
# token instance that is signed by the recovery provider.
|
84
|
+
# context: arbitrary data to be passed to Provider#unseal.
|
85
|
+
#
|
86
|
+
# returns a verified recovery token or raises
|
87
|
+
# an error if the token fails validation.
|
88
|
+
def validate_countersigned_recovery_token!(countersigned_token, context = {})
|
89
|
+
# 5. Validate the the issuer field is present in the token,
|
90
|
+
# and that it matches the audience field in the original countersigned token.
|
91
|
+
begin
|
92
|
+
recovery_provider = RecoveryToken.recovery_provider_issuer(Base64.strict_decode64(countersigned_token))
|
93
|
+
rescue RecoveryTokenSerializationError => e
|
94
|
+
raise CountersignedTokenError.new("Countersigned token is invalid: " + e.message, :countersigned_token_parse_error)
|
95
|
+
rescue UnknownProviderError => e
|
96
|
+
raise CountersignedTokenError.new(e.message, :recovery_token_invalid_issuer)
|
97
|
+
end
|
98
|
+
|
99
|
+
# 1. Parse the countersigned-token.
|
100
|
+
# 2. Validate that the version field is 0.
|
101
|
+
# 7. Retrieve the current Recovery Provider configuration as described in Section 2.
|
102
|
+
# 8. Validate that the counter-signed token signature validates with a current element of the countersign-pubkeys-secp256r1 array.
|
103
|
+
begin
|
104
|
+
parsed_countersigned_token = recovery_provider.unseal(Base64.strict_decode64(countersigned_token), context)
|
105
|
+
rescue TokenFormatError => e
|
106
|
+
raise CountersignedTokenError.new(e.message, :countersigned_invalid_token_version)
|
107
|
+
rescue CryptoError
|
108
|
+
raise CountersignedTokenError.new("Countersigned token has an invalid signature", :countersigned_invalid_signature)
|
109
|
+
end
|
110
|
+
|
111
|
+
# 3. De-serialize the original recovery token from the data field.
|
112
|
+
# 4. Validate the signature on the original recovery token.
|
113
|
+
begin
|
114
|
+
recovery_token = self.unseal(parsed_countersigned_token.data, context)
|
115
|
+
rescue RecoveryTokenSerializationError => e
|
116
|
+
raise CountersignedTokenError.new("Nested recovery token is invalid: " + e.message, :recovery_token_token_parse_error)
|
117
|
+
rescue TokenFormatError => e
|
118
|
+
raise CountersignedTokenError.new("Nested recovery token format error: #{e.message}", :recovery_token_invalid_token_type)
|
119
|
+
rescue CryptoError
|
120
|
+
raise CountersignedTokenError.new("Nested recovery token has an invalid signature", :recovery_token_invalid_signature)
|
121
|
+
end
|
122
|
+
|
123
|
+
# 5. Validate the the issuer field is present in the countersigned-token,
|
124
|
+
# and that it matches the audience field in the original token.
|
125
|
+
|
126
|
+
countersigned_token_issuer = parsed_countersigned_token.issuer
|
127
|
+
if countersigned_token_issuer.blank? || countersigned_token_issuer != recovery_token.audience || recovery_provider.origin != countersigned_token_issuer
|
128
|
+
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)
|
129
|
+
end
|
130
|
+
|
131
|
+
# 6. Validate the token binding for the countersigned token, if present.
|
132
|
+
# (the token binding for the inner token is not relevant)
|
133
|
+
# TODO not required, to be implemented later
|
134
|
+
|
135
|
+
# 9. Decrypt the data field from the original recovery token and parse its information, if present.
|
136
|
+
# no decryption here is attempted. Attempts to call `decode` will just fail.
|
137
|
+
|
138
|
+
# 10. Apply any additional processing which provider-specific data in the opaque data portion may indicate is necessary.
|
139
|
+
begin
|
140
|
+
if DateTime.parse(parsed_countersigned_token.issued_time).utc < (Time.now - CLOCK_SKEW).utc
|
141
|
+
raise CountersignedTokenError.new("Countersigned recovery token issued at time is too far in the past", :stale_token)
|
142
|
+
end
|
143
|
+
rescue ArgumentError
|
144
|
+
raise CountersignedTokenError.new("Invalid countersigned token issued time", :invalid_issued_time)
|
145
|
+
end
|
146
|
+
|
147
|
+
recovery_token
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Darrrr
|
4
|
+
module Constants
|
5
|
+
PROTOCOL_VERSION = 0
|
6
|
+
PRIME_256_V1 = "prime256v1" # AKA secp256r1
|
7
|
+
GROUP = OpenSSL::PKey::EC::Group.new(PRIME_256_V1)
|
8
|
+
DIGEST = OpenSSL::Digest::SHA256
|
9
|
+
TOKEN_ID_BYTE_LENGTH = 16
|
10
|
+
RECOVERY_TOKEN_TYPE = 0
|
11
|
+
COUNTERSIGNED_RECOVERY_TOKEN_TYPE = 1
|
12
|
+
WELL_KNOWN_CONFIG_PATH = ".well-known/delegated-account-recovery/configuration"
|
13
|
+
CLOCK_SKEW = 5 * 60
|
14
|
+
end
|
15
|
+
|
16
|
+
include Constants
|
17
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Darrrr
|
4
|
+
module CryptoHelper
|
5
|
+
include Constants
|
6
|
+
# Signs the provided token and joins the data with the signature.
|
7
|
+
#
|
8
|
+
# token: a RecoveryToken instance
|
9
|
+
#
|
10
|
+
# returns a base64 value for the binary token string and the signature
|
11
|
+
# of the token.
|
12
|
+
def seal(token, context = nil)
|
13
|
+
raise RuntimeError, "signing private key must be set" unless self.instance_variable_get(:@signing_private_key)
|
14
|
+
binary_token = token.to_binary_s
|
15
|
+
signature = self.encryptor.sign(binary_token, self.instance_variable_get(:@signing_private_key), self, context)
|
16
|
+
Base64.strict_encode64([binary_token, signature].join)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Splits the payload by the token size, treats the remaining portion as
|
20
|
+
# the signature of the payload, and verifies the signature is valid for
|
21
|
+
# the given payload.
|
22
|
+
#
|
23
|
+
# token_and_signature: binary string consisting of [token_binary_str, signature].join
|
24
|
+
# keys - An array of public keys to use for signature verification.
|
25
|
+
#
|
26
|
+
# returns a RecoveryToken if the payload has been verified and
|
27
|
+
# deserializes correctly. Raises exceptions if any crypto fails.
|
28
|
+
# Raises an error if the token's version field is not valid.
|
29
|
+
def unseal(token_and_signature, context = nil)
|
30
|
+
token = RecoveryToken.parse(token_and_signature)
|
31
|
+
|
32
|
+
unless token.version.to_i == PROTOCOL_VERSION
|
33
|
+
raise TokenFormatError, "Version field must be #{PROTOCOL_VERSION}"
|
34
|
+
end
|
35
|
+
|
36
|
+
token_data, signature = partition_signed_token(token_and_signature, token)
|
37
|
+
self.unseal_keys(context).each do |key|
|
38
|
+
return token if self.encryptor.verify(token_data, signature, key, self, context)
|
39
|
+
end
|
40
|
+
raise CryptoError, "Recovery token signature was invalid"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Split the binary token into the token data and the signature over the
|
44
|
+
# data.
|
45
|
+
#
|
46
|
+
# token_and_signature: binary serialization of the token and signature for the token
|
47
|
+
# recovery_token: a RecoveryToken object parsed from token_and_signature
|
48
|
+
#
|
49
|
+
# returns a two element array of [token, signature]
|
50
|
+
private def partition_signed_token(token_and_signature, recovery_token)
|
51
|
+
token_length = recovery_token.num_bytes
|
52
|
+
[token_and_signature[0...token_length], token_and_signature[token_length..-1]]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Darrrr
|
2
|
+
module DefaultEncryptor
|
3
|
+
class << self
|
4
|
+
include Constants
|
5
|
+
|
6
|
+
# Encrypts the data in an opaque way
|
7
|
+
#
|
8
|
+
# data: the secret to be encrypted
|
9
|
+
# context: arbitrary data originally passed in via Provider#seal
|
10
|
+
#
|
11
|
+
# returns a byte array representation of the data
|
12
|
+
def encrypt(data, _provider, _context = nil)
|
13
|
+
EncryptedData.build(data).to_binary_s
|
14
|
+
end
|
15
|
+
|
16
|
+
# Decrypts the data
|
17
|
+
#
|
18
|
+
# ciphertext: the byte array to be decrypted
|
19
|
+
# context: arbitrary data originally passed in via RecoveryToken#decode
|
20
|
+
#
|
21
|
+
# returns a string
|
22
|
+
def decrypt(ciphertext, _provider, _context = nil)
|
23
|
+
EncryptedData.parse(ciphertext).decrypt
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# payload: binary serialized recovery token (to_binary_s).
|
28
|
+
#
|
29
|
+
# key: the private EC key used to sign the token
|
30
|
+
# context: arbitrary data originally passed in via Provider#seal
|
31
|
+
#
|
32
|
+
# returns signature in ASN.1 DER r + s sequence
|
33
|
+
def sign(payload, key, _provider, context = nil)
|
34
|
+
digest = DIGEST.new.digest(payload)
|
35
|
+
ec = OpenSSL::PKey::EC.new(Base64.strict_decode64(key))
|
36
|
+
ec.dsa_sign_asn1(digest)
|
37
|
+
end
|
38
|
+
|
39
|
+
# payload: token in binary form
|
40
|
+
# signature: signature of the binary token
|
41
|
+
# key: the EC public key used to verify the signature
|
42
|
+
# context: arbitrary data originally passed in via #unseal
|
43
|
+
#
|
44
|
+
# returns true if signature validates the payload
|
45
|
+
def verify(payload, signature, key, _provider, _context = nil)
|
46
|
+
public_key_hex = format_key(key)
|
47
|
+
pkey = OpenSSL::PKey::EC.new(GROUP)
|
48
|
+
public_key_bn = OpenSSL::BN.new(public_key_hex, 16)
|
49
|
+
public_key = OpenSSL::PKey::EC::Point.new(GROUP, public_key_bn)
|
50
|
+
pkey.public_key = public_key
|
51
|
+
|
52
|
+
pkey.verify(DIGEST.new, signature, payload)
|
53
|
+
rescue OpenSSL::PKey::ECError, OpenSSL::PKey::PKeyError => e
|
54
|
+
raise CryptoError, "Unable verify recovery token"
|
55
|
+
end
|
56
|
+
|
57
|
+
private def format_key(key)
|
58
|
+
sequence, bit_string = OpenSSL::ASN1.decode(Base64.decode64(key)).value
|
59
|
+
unless bit_string.try(:tag) == OpenSSL::ASN1::BIT_STRING
|
60
|
+
raise CryptoError, "DER-encoded key did not contain a bit string"
|
61
|
+
end
|
62
|
+
bit_string.value.unpack("H*").first
|
63
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
64
|
+
raise CryptoError, "Invalid public key format. The key must be in ASN.1 format. #{e.message}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|