Skip to content
This repository has been archived by the owner on Aug 13, 2019. It is now read-only.

Allow users to store OAuth keys encrypted #25

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
FROM phusion/passenger-ruby21
FROM ruby:2.1.5
RUN useradd app && mkdir /home/app
MAINTAINER Greg Brockman <[email protected]>
ADD . /gaps
# If you're running a version of docker before .dockerignore
RUN rm -f /gaps/site.yaml*
RUN chown -R app: /gaps
# Need app to write to /usr/local so the bundle command works
RUN chown -R app: /gaps /usr/local
USER app
ENV HOME /home/app
RUN cd /gaps && bundle install --path vendor/bundle
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ gem 'rack_csrf'
gem 'erubis'
gem 'sinatra'
gem 'mongo_mapper'
# Needed for MongoLab which requires SCRAM-SHA-1
gem 'mongo', '~> 1.12'
gem 'bson_ext'
gem 'einhorn'
gem 'chalk-log'
gem 'chalk-config'
gem 'symmetric-encryption'

gem 'rake'

Expand Down
21 changes: 16 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ GEM
addressable (>= 2.3.1)
extlib (>= 0.9.15)
multi_json (>= 1.0.0)
bson (1.11.1)
bson_ext (1.11.1)
bson (~> 1.11.1)
bson (1.12.5)
bson_ext (1.12.5)
bson (~> 1.12.5)
builder (3.2.2)
chalk-config (0.2.1)
configatron (~> 4.4)
Expand All @@ -34,7 +34,11 @@ GEM
logging
lspace
coderay (1.1.0)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
configatron (4.4.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
einhorn (0.6.3)
erubis (2.7.0)
extlib (0.9.16)
Expand Down Expand Up @@ -70,8 +74,8 @@ GEM
minitest (5.4.3)
mocha (1.1.0)
metaclass (~> 0.0.1)
mongo (1.11.1)
bson (= 1.11.1)
mongo (1.12.5)
bson (= 1.12.5)
mongo_mapper (0.13.1)
activemodel (>= 3.0.0)
activesupport (>= 3.0)
Expand Down Expand Up @@ -110,6 +114,8 @@ GEM
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
slop (3.6.0)
symmetric-encryption (3.8.2)
coercible (~> 1.0)
thread (0.1.4)
thread_safe (0.3.4)
tilt (1.4.1)
Expand All @@ -131,6 +137,7 @@ DEPENDENCIES
google-api-client
mail
mocha
mongo (~> 1.12)
mongo_mapper
pry
puma
Expand All @@ -139,4 +146,8 @@ DEPENDENCIES
rake
rest-client
sinatra
symmetric-encryption
thread

BUNDLED WITH
1.11.2
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,19 @@ configuration file. Clone this repository and execute
`bin/docker-runner` to run the Docker image we've published with your
`site.yaml` bind-mounted inside.

# Permissions
# Security

The Gaps application runs with the minimal privileges it can. However,
it needs to perform powerful actions such as viewing all lists and
subscribing a user to any list. You should restrict access to Gaps
appropriately.

Please be aware that Gaps stores its OAuth credentials in its backing
database. You can enable encryption of those credentials via the
`encrypt_oauth_credentials` configuration option (and also providing a
value for `db.encryption_key`).

## Permissions

Gaps uses your domain admin's credentials to perform most actions
(listing all groups, joining a group). So permissions are entrusted to
Expand Down
7 changes: 6 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
"required": true,
"description": "A secret key for sessions.",
"generator": "secret"
},
"DB_ENCRYPTION_KEY": {
"required": false,
"description": "Secret key to encrypt your OAuth credentials at rest",
"generator": "secret"
}
}
}
}
10 changes: 5 additions & 5 deletions bin/gaps_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,14 +267,14 @@ def die(msg)
## Suggested Sets

get '/sets', auth: true do
@sets = Gaps::DB::Set.find_each
@sets = Gaps::DB::GroupSet.find_each
erb :sets
end

post '/sets', auth: true do
set_id = params[:set]
log.info('Adding user to subscription set', set: set_id, user: @user.email)
Gaps::DB::Set.find(set_id).groups_.each do |group|
Gaps::DB::GroupSet.find(set_id).groups_.each do |group|
membership = @user.group_member?(group)
through_list = @user.group_member_through_list(group)
direct_membership = membership && !through_list
Expand All @@ -295,7 +295,7 @@ def die(msg)

get '/sets/:id', auth: true do
if params[:id] != 'new'
@set = Gaps::DB::Set.find(params[:id])
@set = Gaps::DB::GroupSet.find(params[:id])
return not_found unless @set
end
@groups = Gaps::DB::Group.categorized(@user)
Expand All @@ -310,9 +310,9 @@ def die(msg)
}

if params[:id] == 'new'
Gaps::DB::Set.new(args).save
Gaps::DB::GroupSet.new(args).save
else
set = Gaps::DB::Set.find(params[:id])
set = Gaps::DB::GroupSet.find(params[:id])
return not_found unless set

old_groups = set.groups
Expand Down
3 changes: 3 additions & 0 deletions bin/hk-runner
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ arr=(${MONGODB_URL//\// })
MONGODB_DATABASE=${arr[2]}

cat <<EOF > site.yaml
encrypt_oauth_credentials: true

db:
database: $MONGODB_DATABASE
mongodb_url: $MONGODB_URL
encryption_key: $DB_ENCRYPTION_KEY

favicon: $FAVICON_URL

Expand Down
7 changes: 6 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
default: &default
# We disable this by default for now. People need to generate their
# own encryption key (under `db.encryption_key`) for it to be
# useful.
encrypt_oauth_credentials: false

cache:
# How large of a threadpool to maintain for populating the cache
# of what each list is subscribed to. (Note for implementation
Expand Down Expand Up @@ -41,7 +46,7 @@ default: &default
gaps_scheme: true
# Whether to use the Google Groups Settings API to decide if a
# list is private. (Can be composed with the Gaps scheme.)
api_scheme: false
api_scheme: true

# Whether to fetch the Google Groups settings upon group
# refresh. Needed to make the api_scheme work. In the future will
Expand Down
15 changes: 14 additions & 1 deletion lib/gaps/db.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@ class DBError < StandardError; end
class UniqueKeyViolation < DBError; end

def self.init
# Initialize even if we're not encrypting, since we may need to
# unencrypt some records.
if configatron.db.key?(:encryption_key)
SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(
cipher_name: 'aes-128-cbc',
key: configatron.db.encryption_key,
encoding: :base64strict
)
end

MongoMapper.database = configatron.db.database
MongoMapper.connection = Mongo::MongoClient.from_uri(configatron.db.mongodb_url, pool_size: 5)
MongoMapper.connection = Mongo::MongoClient.from_uri(
configatron.db.mongodb_url,
pool_size: [configatron.cache.pool_size, 5].max
)

Cache.build_index
Group.build_index
Expand Down
2 changes: 1 addition & 1 deletion lib/gaps/db/set.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Gaps::DB
class Set < Base
class GroupSet < Base
set_collection_name 'set'

key :name, String
Expand Down
74 changes: 72 additions & 2 deletions lib/gaps/db/user.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'thread'
require 'symmetric-encryption'

module Gaps::DB
class User < Base
Expand All @@ -13,8 +14,10 @@ class InvalidUser < StandardError; end
key :admin, Boolean, default: false
key :image_url, String

key :refresh_token, String
key :access_token, String
key :refresh_token, String # plaintext, thus deprecated
key :access_token, String # plaintext, thus deprecated
key :encrypted_refresh_token, String
key :encrypted_access_token, String
key :expires_in, Integer
key :issued_at, Integer

Expand All @@ -26,6 +29,73 @@ class InvalidUser < StandardError; end

key :sets, Array, :default => []

### Secrets

def initialize_from_database(*args)
ret = super

# Migrate back and forth between encrypted credentials at load time
if configatron.encrypt_oauth_credentials && (@access_token || @refresh_token)
self.encrypted_access_token = encrypt(@access_token)
self.encrypted_refresh_token = encrypt(@refresh_token)
@access_token = nil
@refresh_token = nil
save!
elsif !configatron.encrypt_oauth_credentials && (@encrypted_access_token || @encrypted_refresh_token)
self.access_token = decrypt(@encrypted_access_token)
self.refresh_token = decrypt(@encrypted_refresh_token)
@encrypted_access_token = nil
@encrypted_refresh_token = nil
save!
end

ret
end

def encrypt(str)
# Use random iv and compress
SymmetricEncryption.encrypt(str, true, true)
end

def decrypt(str)
SymmetricEncryption.decrypt(str)
end

def access_token
if configatron.encrypt_oauth_credentials
decrypt(self.encrypted_access_token)
else
super
end
end

def access_token=(tok)
if configatron.encrypt_oauth_credentials
self.encrypted_access_token = encrypt(tok)
else
super
end
end

def refresh_token
if configatron.encrypt_oauth_credentials
decrypt(self.encrypted_refresh_token)
else
super
end
end

def refresh_token=(tok)
if configatron.encrypt_oauth_credentials
self.encrypted_refresh_token = encrypt(tok)
else
super
end
end


### End secrets

def self.build_index
self.ensure_index([[:google_id, 1]], unique: true, sparse: true)
end
Expand Down
2 changes: 2 additions & 0 deletions site.yaml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ db:
# persisted). Note that Gaps stores some interesting state, such as
# people's filter configuration and group categorization.
mongodb_url: mongodb://localhost:27017
# Set this to a random string, such as `openssl rand -base64 129`
encryption_key: "FN25NUJ6pk1Ys53aVHJp7usl1Dnz18QHy7+7FQH3R/QXd5XC9Syh5Dx+tsvD2o4OWylHqw9sgPQUU472B5RZXkx27Wo2rw0nlhXsYFWAVRpRz8jZ7Egzl4K0t4c0m/Gzy8QuMRbDCEktJq41S78J5KJYopTJ1oNKPlxjAKj7mFCl"

# Your favicon
favicon:
Expand Down