darrrr 0.0.2

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b73c12978f19a57c566f6cdc1e3caf2409307b89
4
+ data.tar.gz: 84ea2347b3d30a53489cfafad46b7aa4d5d4dc13
5
+ SHA512:
6
+ metadata.gz: 5c224e64fc58c14ee6dc13272274d74f8439d20d73781770b007d13db567dfed10760a739737bf07922e0ac8f04496cac578a4facb58409167329dd3626031f6
7
+ data.tar.gz: f2800490548b13ae9d5da99bcce76552f508b51975f2cf864427802ed895288c734eae0f30044c8ade4351fe41eacd5af8bf6f1b2b1a1f12e69426d38d3f1fb8
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 GitHub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,203 @@
1
+ [![Build Status](https://travis-ci.org/github/darrrr.svg?branch=master)](https://travis-ci.org/github/darrrr) [![Code Climate](https://codeclimate.com/github/github/darrrr/badges/gpa.svg)](https://codeclimate.com/github/github/darrrr)
2
+
3
+ The Delegated Account Recovery Rigid Reusable Ruby (aka D.a.r.r.r.r. or "Darrrr") library is meant to be used as the fully-complete plumbing in your Rack application when implementing the [Delegated Account Recovery specification](https://github.com/facebook/DelegatedRecoverySpecification). This library is currently used for the implementation at [GitHub](https://githubengineering.com/recover-accounts-elsewhere/).
4
+
5
+ Along with a fully featured library, a proof of concept application is provided in this repo.
6
+
7
+ ## Configuration
8
+
9
+ 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
+
11
+ In `config/initializers` or any location that is run during application setup, add a file:
12
+
13
+ ```ruby
14
+ Darrrr.authority = "http://localhost:9292"
15
+ Darrrr.privacy_policy = "#{Darrrr.authority}/articles/github-privacy-statement/"
16
+ Darrrr.icon_152px = "#{Darrrr.authority}/icon.png"
17
+
18
+ # See script/setup for instructions on how to generate keys
19
+ Darrrr::AccountProvider.configure do |config|
20
+ config.signing_private_key = ENV["ACCOUNT_PROVIDER_PRIVATE_KEY"]
21
+ config.symmetric_key = ENV["TOKEN_DATA_AES_KEY"]
22
+ config.tokensign_pubkeys_secp256r1 = [ENV["ACCOUNT_PROVIDER_PUBLIC_KEY"]]
23
+ config.save_token_return = "#{Darrrr.authority}/account-provider/save-token-return"
24
+ config.recover_account_return = "#{Darrrr.authority}/account-provider/recover-account-return"
25
+ end
26
+
27
+ Darrrr::RecoveryProvider.configure do |config|
28
+ config.signing_private_key = ENV["RECOVERY_PROVIDER_PRIVATE_KEY"]
29
+ config.countersign_pubkeys_secp256r1 = [ENV["RECOVERY_PROVIDER_PUBLIC_KEY"]]
30
+ config.token_max_size = 8192
31
+ config.save_token = "#{Darrrr.authority}/recovery-provider/save-token"
32
+ config.recover_account = "#{Darrrr.authority}/recovery-provider/recover-account"
33
+ end
34
+ ```
35
+
36
+ The delegated recovery spec depends on publicly available endpoints serving standard configs. These responses can be cached but are not by default. To configure your cache store, provide the reference:
37
+
38
+ ```ruby
39
+ Darrrr.cache = Dalli::Client.new('localhost:11211', options)
40
+ ```
41
+
42
+ The spec disallows `http` URIs for basic security, but sometimes we don't have this setup locally.
43
+
44
+ ```ruby
45
+ Darrrr.allow_unsafe_urls = true
46
+ ```
47
+
48
+ ## Provider registration
49
+
50
+ In order to allow a site to act as a provider, it must be "registered" on boot to prevent unauthorized providers from managing tokens.
51
+
52
+ ```ruby
53
+ # Only configure this if you are acting as a recovery provider
54
+ Darrrr.register_account_provider("https://github.com")
55
+
56
+ # Only configure this if you are acting as an account provider
57
+ Darrrr.register_recovery_provider("https://www.facebook.com")
58
+ ```
59
+
60
+ ## Custom crypto
61
+
62
+ Create a module that responds to `Module.sign`, `Module.verify`, `Module.decrypt`, and `Module.encrypt`. You can use the template below. I recommend leaving the `#verify` method as is unless you have a compelling reason to override it.
63
+
64
+ ### Global config
65
+
66
+ Set `Darrrr.custom_encryptor = MyCustomEncryptor`
67
+
68
+ ### On-demand
69
+
70
+ ```ruby
71
+ Darrrr.with_encryptor(MyCustomEncryptor) do
72
+ # perform DAR actions using MyCustomEncryptor as the crypto provider
73
+ token = Darrrr.this_account_provider.generate_recovery_token(data: "foo", audience: recovery_provider)
74
+ end
75
+ ```
76
+
77
+ ```ruby
78
+ module MyCustomEncryptor
79
+ class << self
80
+ # Encrypts the data in an opaque way
81
+ #
82
+ # data: the secret to be encrypted
83
+ #
84
+ # returns a byte array representation of the data
85
+ def encrypt(data)
86
+
87
+ end
88
+
89
+ # Decrypts the data
90
+ #
91
+ # ciphertext: the byte array to be decrypted
92
+ #
93
+ # returns a string
94
+ def decrypt(ciphertext)
95
+
96
+ end
97
+
98
+ # payload: binary serialized recovery token (to_binary_s).
99
+ #
100
+ # key: the private EC key used to sign the token
101
+ #
102
+ # returns signature in ASN.1 DER r + s sequence
103
+ def sign(payload, key)
104
+
105
+ end
106
+
107
+ # payload: token in binary form
108
+ # signature: signature of the binary token
109
+ # key: the EC public key used to verify the signature
110
+ #
111
+ # returns true if signature validates the payload
112
+ def verify(payload, signature, key)
113
+ # typically, the default verify function should be used to ensure compatibility
114
+ Darrrr::DefaultEncryptor.verify(payload, signature, key)
115
+ end
116
+ end
117
+ end
118
+ ```
119
+
120
+ ## Example implementation
121
+
122
+ I strongly suggest you read the specification, specifically section 3.1 (save-token) and 3.5 (recover account) as they contain the most dangerous operations.
123
+
124
+ **NOTE:** this is NOT meant to be a complete implementation, it is just the starting point. Crucial aspects such as authentication, audit logging, out of band notifications, and account provider persistence are not implemented.
125
+
126
+ * [Account Provider](controllers/account_provider_controller.rb) (save-token-return, recover-account-return)
127
+ * [Recovery Provider](controllers/recovery_provider_controller.rb) (save-token, recover-account)
128
+ * [Configuration endpoint](controllers/well_known_config_controller.rb) (`/.well-known/delegated-account-recovery/configuration`)
129
+
130
+ Specifically, the gem exposes the following APIs for manipulating tokens.
131
+ * Account Provider
132
+ * [Generating](https://github.com/github/darrrr/blob/faafda5b1773e077c9c10b55b46216f97d13cd3b/lib/github/delegated_account_recovery/account_provider.rb#L49) a token
133
+ * Signing ([`#seal`](https://github.com/github/darrrr/blob/faafda5b1773e077c9c10b55b46216f97d13cd3b/lib/github/delegated_account_recovery/crypto_helper.rb#L13)) a token
134
+ * Verifying ([`#unseal`](https://github.com/github/darrrr/blob/faafda5b1773e077c9c10b55b46216f97d13cd3b/lib/github/delegated_account_recovery/crypto_helper.rb#L30)) a countersigned token
135
+ * Recovery Provider
136
+ * Verifying ([`#unseal`](https://github.com/github/darrrr/blob/faafda5b1773e077c9c10b55b46216f97d13cd3b/lib/github/delegated_account_recovery/crypto_helper.rb#L30)) a token
137
+ * [Countersigning](https://github.com/github/darrrr/blob/faafda5b1773e077c9c10b55b46216f97d13cd3b/lib/github/delegated_account_recovery/recovery_provider.rb#L60) a token
138
+
139
+ ### Development
140
+
141
+ Local development assumes a Mac OS environment with [homebrew](https://brew.sh/) available. Postgres and phantom JS will be installed.
142
+
143
+ Run `./script/bootstrap` then run `./script/server`
144
+
145
+ * Visit `http://localhost:9292/account-provider`
146
+ * (Optionally) Record the random number for verification
147
+ * Click "connect to http://localhost:9292"
148
+ * You'll see some debug information on the page.
149
+ * Click "setup recovery".
150
+ * If recovery setup was successful, click "Recovery Setup Successful"
151
+ * Click the "recover now?" link
152
+ * You'll see an intermediate page, where more debug information is presented. Click "recover token"
153
+ * You should be sent back to your host
154
+ * And see something like `Recovered data: <the secret from step 1>`
155
+
156
+ ### Tests
157
+
158
+ Run `./script/test` to run all tests.
159
+
160
+ ## Deploying to heroku
161
+
162
+ Use `heroku config:set` to set the environment variables listed in [script/setup](/script/setup). Additionally, run:
163
+
164
+ ```
165
+ heroku config:set HOST_URL=$(heroku info -s | grep web_url | cut -d= -f2)
166
+ ```
167
+
168
+ Push your app to heroku:
169
+
170
+ ```
171
+ git push heroku <branch-name>:master
172
+ ```
173
+
174
+ Migrate the database:
175
+
176
+ ```
177
+ heroku run rake db:migrate
178
+ ```
179
+
180
+ Use the app!
181
+
182
+ ```
183
+ heroku restart
184
+ heroku open
185
+ ```
186
+
187
+ ## Roadmap
188
+
189
+ * Add support for `token-status` endpoints as defined by the spec
190
+ * Add async API as defined by the spec
191
+ * Implement token binding as part of the async API
192
+
193
+ ## Don't want to run `./script` entries?
194
+
195
+ See `script/setup` for the environment variables that need to be set.
196
+
197
+ ## Contributions
198
+
199
+ See [CONTRIBUTING.md](CONTRIBUTING.md)
200
+
201
+ ## License
202
+
203
+ `darrrr` is licensed under the [MIT license](LICENSE.md).
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'date'
6
+
7
+ require_relative "app"
8
+ require_relative "lib/darrrr"
9
+ require "sinatra/activerecord/rake"
10
+
11
+ namespace :db do
12
+ task :load_config do
13
+ require "./app"
14
+ end
15
+ end
16
+
17
+
18
+ unless ENV["RACK_ENV"] == "production"
19
+ require 'rspec/core/rake_task'
20
+ desc "Run RSpec"
21
+ RSpec::Core::RakeTask.new do |t|
22
+ t.verbose = false
23
+ t.rspec_opts = "--format progress"
24
+ end
25
+
26
+ task default: :spec
27
+ end
28
+
29
+ begin
30
+ require 'rdoc/task'
31
+ rescue LoadError
32
+ require 'rdoc/rdoc'
33
+ require 'rake/rdoctask'
34
+ RDoc::Task = Rake::RDocTask
35
+ end
36
+
37
+ RDoc::Task.new(:rdoc) do |rdoc|
38
+ rdoc.rdoc_dir = 'rdoc'
39
+ rdoc.title = 'SecureHeaders'
40
+ rdoc.options << '--line-numbers'
41
+ rdoc.rdoc_files.include('lib/**/*.rb')
42
+ end
@@ -0,0 +1,9 @@
1
+ require "bindata"
2
+ require "openssl"
3
+ require "addressable"
4
+ require "forwardable"
5
+ require "faraday"
6
+
7
+ require_relative "github/delegated_account_recovery"
8
+
9
+ Darrrr = GitHub::DelegatedAccountRecovery
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "openssl"
5
+
6
+ require_relative "delegated_account_recovery/constants"
7
+ require_relative "delegated_account_recovery/crypto_helper"
8
+ require_relative "delegated_account_recovery/recovery_token"
9
+ require_relative "delegated_account_recovery/provider"
10
+ require_relative "delegated_account_recovery/account_provider"
11
+ require_relative "delegated_account_recovery/recovery_provider"
12
+ require_relative "delegated_account_recovery/serialization/recovery_token_writer"
13
+ require_relative "delegated_account_recovery/serialization/recovery_token_reader"
14
+ require_relative "delegated_account_recovery/cryptors/default/default_encryptor"
15
+ require_relative "delegated_account_recovery/cryptors/default/encrypted_data"
16
+ require_relative "delegated_account_recovery/cryptors/default/encrypted_data_io"
17
+
18
+ module GitHub
19
+ module DelegatedAccountRecovery
20
+ # Represents a binary serialization error
21
+ class RecoveryTokenSerializationError < StandardError; end
22
+
23
+ # Represents invalid data within a valid token
24
+ # (e.g. wrong `version` number, invalid token `type`)
25
+ class TokenFormatError < StandardError; end
26
+
27
+ # Represents all crypto errors
28
+ # (e.g. invalid keys, invalid signature, decrypt failures)
29
+ class CryptoError < StandardError; end
30
+
31
+ # Represents providers supplying invalid configurations
32
+ # (e.g. non-https URLs, missing required fields, http errors)
33
+ class ProviderConfigError < StandardError; end
34
+
35
+ # Represents an invalid countersigned recovery token.
36
+ # (e.g. invalid signature, invalid nested token, unregistered provider, stale tokens)
37
+ class CountersignedTokenError < StandardError
38
+ attr_reader :key
39
+ def initialize(message, key)
40
+ super(message)
41
+ @key = key
42
+ end
43
+ end
44
+
45
+ # Represents an invalid recovery token.
46
+ # (e.g. invalid signature, unregistered provider, stale tokens)
47
+ class RecoveryTokenError < StandardError; end
48
+
49
+ # Represents a call to to `recovery_provider` or `account_provider` that
50
+ # has not been registered.
51
+ class UnknownProviderError < ArgumentError; end
52
+
53
+ include Constants
54
+
55
+ class << self
56
+ REQUIRED_CRYPTO_OPS = [:sign, :verify, :encrypt, :decrypt].freeze
57
+ # recovery provider data is only loaded (and cached) upon use.
58
+ attr_accessor :recovery_providers, :account_providers, :cache, :allow_unsafe_urls,
59
+ :privacy_policy, :icon_152px, :authority
60
+
61
+ # Find and load remote recovery provider configuration data.
62
+ #
63
+ # provider_origin: the origin that contains the config data in a well-known
64
+ # location.
65
+ def recovery_provider(provider_origin)
66
+ unless self.recovery_providers
67
+ raise "No recovery providers configured"
68
+ end
69
+ if self.recovery_providers.include?(provider_origin)
70
+ RecoveryProvider.new(provider_origin).load
71
+ else
72
+ raise UnknownProviderError, "Unknown recovery provider: #{provider_origin}"
73
+ end
74
+ end
75
+
76
+ # Permit an origin to act as a recovery provider.
77
+ #
78
+ # provider_origin: the origin to permit
79
+ def register_recovery_provider(provider_origin)
80
+ self.recovery_providers ||= []
81
+ self.recovery_providers << provider_origin
82
+ end
83
+
84
+ # Find and load remote account provider configuration data.
85
+ #
86
+ # provider_origin: the origin that contains the config data in a well-known
87
+ # location.
88
+ def account_provider(provider_origin)
89
+ unless self.account_providers
90
+ raise "No account providers configured"
91
+ end
92
+ if self.account_providers.include?(provider_origin)
93
+ AccountProvider.new(provider_origin).load
94
+ else
95
+ raise UnknownProviderError, "Unknown account provider: #{provider_origin}"
96
+ end
97
+ end
98
+
99
+ # Permit an origin to act as an account provider.
100
+ #
101
+ # account_origin: the origin to permit
102
+ def register_account_provider(account_origin)
103
+ self.account_providers ||= []
104
+ self.account_providers << account_origin
105
+ end
106
+
107
+ # Provide a reference to the account provider configuration for this web app
108
+ def this_account_provider
109
+ AccountProvider.this
110
+ end
111
+
112
+ # Provide a reference to the recovery provider configuration for this web app
113
+ def this_recovery_provider
114
+ RecoveryProvider.this
115
+ end
116
+
117
+ # Returns a hash of all configuration values, recovery and account provider.
118
+ def account_and_recovery_provider_config
119
+ provider_data = Darrrr.this_account_provider.try(:to_h) || {}
120
+
121
+ if Darrrr.this_recovery_provider
122
+ provider_data.merge!(recovery_provider_config) do |key, lhs, rhs|
123
+ unless lhs == rhs
124
+ raise ArgumentError, "inconsistent config value detected #{key}: #{lhs} != #{rhs}"
125
+ end
126
+
127
+ lhs
128
+ end
129
+ end
130
+
131
+ provider_data
132
+ end
133
+
134
+ # returns the account provider information in hash form
135
+ def account_provider_config
136
+ this_account_provider.to_h
137
+ end
138
+
139
+ # returns the account provider information in hash form
140
+ def recovery_provider_config
141
+ this_recovery_provider.to_h
142
+ end
143
+
144
+ def with_encryptor(encryptor)
145
+ raise ArgumentError, "A block must be supplied" unless block_given?
146
+ unless valid_encryptor?(encryptor)
147
+ raise ArgumentError, "custom encryption class must respond to all of #{REQUIRED_CRYPTO_OPS}"
148
+ end
149
+
150
+ Thread.current[:darrrr_encryptor] = encryptor
151
+ yield
152
+ ensure
153
+ Thread.current[:darrrr_encryptor] = nil
154
+ end
155
+
156
+ # Returns the crypto API to be used. A thread local instance overrides the
157
+ # globally configured value which overrides the default encryptor.
158
+ def encryptor
159
+ Thread.current[:darrrr_encryptor] || @encryptor || DefaultEncryptor
160
+ end
161
+
162
+ # Overrides the global `encryptor` API to use
163
+ #
164
+ # encryptor: a class/module that responds to all +REQUIRED_CRYPTO_OPS+.
165
+ def custom_encryptor=(encryptor)
166
+ if valid_encryptor?(encryptor)
167
+ @encryptor = encryptor
168
+ else
169
+ raise ArgumentError, "custom encryption class must respond to all of #{REQUIRED_CRYPTO_OPS}"
170
+ end
171
+ end
172
+
173
+ private def valid_encryptor?(encryptor)
174
+ REQUIRED_CRYPTO_OPS.all? {|m| encryptor.respond_to?(m)}
175
+ end
176
+ end
177
+ end
178
+ end