darrrr 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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