From c9f0de418b34f52cb72032282643d0afabc39abd Mon Sep 17 00:00:00 2001 From: Andy Geers Date: Tue, 6 Jun 2017 10:53:28 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + .ruby-version | 1 + Gemfile | 7 ++ Gemfile.lock | 69 ++++++++++++++++++++ LICENSE | 19 ++++++ README.md | 29 +++++++++ app.rb | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++ app.yaml | 6 ++ openapi.yaml | 98 ++++++++++++++++++++++++++++ 9 files changed, 407 insertions(+) create mode 100644 .gitignore create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.rb create mode 100644 app.yaml create mode 100644 openapi.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..180bf07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.bundle +vendor diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..0bee604 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.3.3 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..861ddbf --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# A sample Gemfile +source "https://rubygems.org" + +# gem "rails" +gem "sinatra" +gem "google-api-client" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..437a5f7 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,69 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + declarative (0.0.9) + declarative-option (0.1.0) + faraday (0.12.1) + multipart-post (>= 1.2, < 3) + google-api-client (0.11.2) + addressable (>= 2.5.1) + googleauth (~> 0.5) + httpclient (>= 2.8.1, < 3.0) + mime-types (>= 3.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + googleauth (0.5.1) + faraday (~> 0.9) + jwt (~> 1.4) + logging (~> 2.0) + memoist (~> 0.12) + multi_json (~> 1.11) + os (~> 0.9) + signet (~> 0.7) + httpclient (2.8.3) + jwt (1.5.6) + little-plugger (1.1.4) + logging (2.2.2) + little-plugger (~> 1.1) + multi_json (~> 1.10) + memoist (0.15.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + multi_json (1.12.1) + multipart-post (2.0.0) + mustermann (1.0.0) + os (0.9.6) + public_suffix (2.0.5) + rack (2.0.3) + rack-protection (2.0.0) + rack + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.0.2) + signet (0.7.3) + addressable (~> 2.3) + faraday (~> 0.9) + jwt (~> 1.5) + multi_json (~> 1.10) + sinatra (2.0.0) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.0) + tilt (~> 2.0) + tilt (2.0.7) + uber (0.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + google-api-client + sinatra + +BUNDLED WITH + 1.14.6 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..683d371 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Andy Geers. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..db3c8d2 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +## How do you encrypt a Firebase database? + +I've written a blog post [here](http://www.geero.net/2017/05/how-to-encrypt-a-google-firebase-realtime-database/) and this is the backend auth service described there. + +Firebase Keysafe is a super-simple back end service designed to be deployed to Google App Engine Flexible Environment and Cloud Endpoints, to protect the encryption keys of mobile users authenticated using Google Firebase +by means of Google Cloud KMS. + +To configure: + + 1. Make sure you edit openapi.yaml and replace with the name of your App Engine app, and with the Project ID that houses your Firebase Auth. + 2. Do the same in app.yaml + 3. Plug in values for , and in app.rb + 3. Deploy the configuration to Google Cloud Endpoints: + + gcloud service-management deploy openapi.yaml + + 4. You will then be given a config_id that you can plug in to app.yaml. + 5. Deploy to App Engine: + + bundle install + gcloud app deploy + +To run in development mode: + + bundle install + bundle exec ruby app.rb -p 8080 + +(caveat: I couldn't actually get my local development machine to work properly with Cloud KMS - I always got permission denied errors.) + diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..4223867 --- /dev/null +++ b/app.rb @@ -0,0 +1,176 @@ +# Copyright 2017 Andy Geers. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# [START app] +require "sinatra" +require "google/apis/cloudkms_v1" +require "openssl" + +# Your Google Cloud Platform project ID +project_id = "prayermate-auth-service" + +# Lists keys in the "global" location. +location = "global" + +# Instantiate the client +Cloudkms = Google::Apis::CloudkmsV1 # Alias the module +kms_client = Cloudkms::CloudKMSService.new + +# Set the required scopes to access the Key Management Service API +# @see https://developers.google.com/identity/protocols/application-default-credentials#callingruby +kms_client.authorization = Google::Auth.get_application_default( + "https://www.googleapis.com/auth/cloud-platform" +) + +parent = "projects/#{project_id}/locations/#{location}" + +keyring_name = "" +key_id = "" + +# The resource name of the location associated with the Key rings +key_name = "#{parent}/keyRings/#{keyring_name}/cryptoKeys/#{key_id}" + +class AuthorizationException < Exception; end + +def auth_info + # This will only be defined when in App Engine production and if Cloud Endpoints is configured correctly + encoded_info = request.env["HTTP_X_ENDPOINT_API_USERINFO"] + + if encoded_info + info_json = Base64.decode64 encoded_info + user_info = JSON.parse info_json + + raise AuthorizationException if user_info['id'].nil? || user_info['id'].length == 0 + + return user_info + else + raise AuthorizationException + end +end + +post "/key" do + content_type 'application/json' + + begin + # Generate a random key + pass = "" + salt = OpenSSL::Random.random_bytes(16) + iter = 20000 + key_len = 16 + + auth = auth_info + user_id = auth['id'] + + raw_key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, iter, key_len) + + # Request list of key rings + encrypt_request_object = Google::Apis::CloudkmsV1::EncryptRequest.from_json({ + "plaintext": Base64.urlsafe_encode64(user_id + "|" + Base64.urlsafe_encode64(raw_key).sub(/==\n?$/, "")) + }.to_json) + response = kms_client.encrypt_crypto_key(key_name, encrypt_request_object) + + { + "key": Base64.urlsafe_encode64(raw_key).sub(/==\n?$/, ""), + "encrypted": Base64.urlsafe_encode64(response.ciphertext).sub(/==\n?$/, "") + }.to_json + + rescue AuthorizationException => e + status 401 + return { + "success": false, + "error": "Not authorised" + }.to_json + + rescue => e + + error = nil + + begin + error = { + "body": e.body, + "header": e.header, + "status_code": e.status_code + } + rescue => e2 + error = e + end + + return { + "success": false, + "error": error + }.to_json + end + + #Google::Apis::CloudkmsV1::EncryptResponse + + +end + +get "/decrypt" do + content_type 'application/json' + + input = params[:value] + + begin + auth = auth_info + user_id = auth['id'] + + # Request list of key rings + decrypt_request_object = Google::Apis::CloudkmsV1::DecryptRequest.from_json({ + "ciphertext": input + }.to_json) + response = kms_client.decrypt_crypto_key(key_name, decrypt_request_object) + + components = response.plaintext.split("|", 2) + + unless components.first == user_id + $stderr.puts "Auth failure comparing user IDs #{components.first} with #{user_id}" + raise AuthorizationException + end + + { + "key": components.last.sub(/==\n?$/, "") + }.to_json + + rescue AuthorizationException => e + status 401 + return { + "success": false, + "error": "Not authorised" + }.to_json + + rescue => e + return { + "success": false, + "input": input, + "error": { + "body": e.body, + "header": e.header, + "status_code": e.status_code + } + }.to_json + end +end + +get "/_ah/health" do + { "success": true }.to_json +end +# [END app] + diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..e7687e5 --- /dev/null +++ b/app.yaml @@ -0,0 +1,6 @@ +entrypoint: bundle exec ruby app.rb +env: flex +runtime: ruby +endpoints_api_service: + name: .appspot.com + config_id: 2017-01-01r0 \ No newline at end of file diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..ba65924 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,98 @@ +swagger: "2.0" +info: + title: Firebase Keysafe + version: v1 + contact: + name: PrayerMate Team + email: info@prayermate.net + url: http://app.prayermate.net +host: .appspot.com +schemes: + - https +consumes: +- application/json +produces: +- application/json +x-google-allow: configured +securityDefinitions: + firebase: + authorizationUrl: "" + flow: "implicit" + type: "oauth2" + x-google-issuer: "https://securetoken.google.com/" + x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com" + x-google-audiences: "" +security: + - firebase: [] +paths: + /key: + post: + operationId: createKey + summary: Creates a new data encryption key + responses: + "200": + description: |- + 200 response + examples: + application/json: |- + { + "key": "abcdefg", + "encrypted": "bcdefga" + } + default: + description: unexpected error + examples: + application/json: |- + { + "success": false, + "error": { + "body": "e.body", + "header": "e.header", + "status_code": 403 + } + } + /decrypt: + get: + operationId: decryptKey + summary: Decrypts a data encryption key + parameters: + - name: value + in: query + description: data encryption key to decrypt + required: true + type: string + responses: + "200": + description: |- + 200 response + examples: + application/json: |- + { + "key": "abcdefg" + } + default: + description: unexpected error + examples: + application/json: |- + { + "success": false, + "input": "abcdefg", + "error": { + "body": "e.body", + "header": "e.header", + "status_code": 403 + } + } + /_ah/health: + get: + operationId: healthCheck + security: [] + responses: + "200": + description: |- + 200 response + examples: + application/json: |- + { + "success": true + } \ No newline at end of file