darrrr 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -5
  3. data/lib/darrrr.rb +144 -2
  4. data/lib/darrrr/account_provider.rb +150 -0
  5. data/lib/darrrr/constants.rb +17 -0
  6. data/lib/darrrr/crypto_helper.rb +55 -0
  7. data/lib/darrrr/cryptors/default/default_encryptor.rb +68 -0
  8. data/lib/darrrr/cryptors/default/encrypted_data.rb +90 -0
  9. data/lib/darrrr/cryptors/default/encrypted_data_io.rb +10 -0
  10. data/lib/darrrr/provider.rb +157 -0
  11. data/lib/darrrr/recovery_provider.rb +129 -0
  12. data/lib/darrrr/recovery_token.rb +117 -0
  13. data/lib/darrrr/serialization/recovery_token_reader.rb +20 -0
  14. data/lib/darrrr/serialization/recovery_token_writer.rb +20 -0
  15. metadata +17 -18
  16. data/lib/github/delegated_account_recovery.rb +0 -178
  17. data/lib/github/delegated_account_recovery/account_provider.rb +0 -140
  18. data/lib/github/delegated_account_recovery/constants.rb +0 -19
  19. data/lib/github/delegated_account_recovery/crypto_helper.rb +0 -57
  20. data/lib/github/delegated_account_recovery/cryptors/default/default_encryptor.rb +0 -66
  21. data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data.rb +0 -92
  22. data/lib/github/delegated_account_recovery/cryptors/default/encrypted_data_io.rb +0 -12
  23. data/lib/github/delegated_account_recovery/provider.rb +0 -125
  24. data/lib/github/delegated_account_recovery/recovery_provider.rb +0 -118
  25. data/lib/github/delegated_account_recovery/recovery_token.rb +0 -113
  26. data/lib/github/delegated_account_recovery/serialization/recovery_token_reader.rb +0 -22
  27. data/lib/github/delegated_account_recovery/serialization/recovery_token_writer.rb +0 -22
  28. 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: 30652adfea61c9ea52722a617bebec55645af566
4
- data.tar.gz: cefe7abf7ddee57fc2783440323439dfbed6c04b
3
+ metadata.gz: 29975ab5312f2fef2ceceac4fe40abe1f011ca64
4
+ data.tar.gz: 30001607c84a89388bf5b19690cd092638e895a7
5
5
  SHA512:
6
- metadata.gz: 0b2c5d4f6cff3e2c2380371e2a56a4f9da04e4bbf48a8d0272ebb121cd229d4a57c8921da9fa4327626cdaefc2a8f7e956c92ce48dde08febd8883591b900089
7
- data.tar.gz: 915e5856945aae98bab5ebf7e00dba4d2f3aced075cebaedbb7fc9ba7becd84382cda1f3217c3babe4a431c8b32c098d865e388a2994c908c01734c4092d269e
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
- token = Darrrr.this_account_provider.generate_recovery_token(data: "foo", audience: recovery_provider)
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).
@@ -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 "github/delegated_account_recovery"
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
- Darrrr = GitHub::DelegatedAccountRecovery
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