Skip to content

Commit

Permalink
🛢 Encrypt tokens at rest
Browse files Browse the repository at this point in the history
  • Loading branch information
David Glick committed Feb 12, 2019
1 parent e41ac55 commit 60b136e
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 22 deletions.
5 changes: 3 additions & 2 deletions CONTRIBUTING.rst
Expand Up @@ -33,8 +33,9 @@ Copy the ``.env`` file somewhere that will be sourced when you need it::
cp env.example $VIRTUAL_ENV/bin/postactivate

Edit this file to change ``DJANGO_SECRET_KEY`` and ``DJANGO_HASHID_SALT`` to any
two different arbitrary string values, and to edit the following environment
variables (if you're an OddBird, you can find these values in the shared Keybase
two different arbitrary string values. Also set ``DB_ENCRYPTION_KEY``.
Edit the following environment variables
(if you're an OddBird, you can find these values in the shared Keybase
team folder -- ``metadeploy/env``)::

export BUCKETEER_AWS_ACCESS_KEY_ID=...
Expand Down
23 changes: 18 additions & 5 deletions app.json
Expand Up @@ -2,7 +2,13 @@
"name": "MetaDeploy",
"description": "The future, today.",
"repository": "https://github.com/SFDO-Tooling/metadeploy",
"keywords": ["ci", "python", "django", "salesforce", "github"],
"keywords": [
"ci",
"python",
"django",
"salesforce",
"github"
],
"env": {
"DJANGO_ALLOWED_HOSTS": {
"description": "Heroku proxies web requests and Django needs to be configured to allow the forwards",
Expand Down Expand Up @@ -41,7 +47,10 @@
},
"GITHUB_TOKEN": {
"description": "A valid github personal access token that has read access to all repositories."
}
},
"DB_ENCRYPTION_KEY": {
"description": "A key for encrypting using cryptography.fernet. Generate using cryptography.fernet.Fernet.generate_key"
},
},
"formation": {
"web": {
Expand All @@ -60,8 +69,12 @@
"heroku-redis"
],
"buildpacks": [
{"url": "heroku/node"},
{"url": "heroku/python"}
{
"url": "heroku/node"
},
{
"url": "heroku/python"
}
],
"environments": {
"test": {
Expand All @@ -78,4 +91,4 @@
}
}
}
}
}
1 change: 1 addition & 0 deletions config/settings/base.py
Expand Up @@ -85,6 +85,7 @@ def env(name, default=NoDefaultValue, type_=str):
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("DJANGO_SECRET_KEY")
HASHID_FIELD_SALT = env("DJANGO_HASHID_SALT")
DB_ENCRYPTION_KEY = env("DB_ENCRYPTION_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DJANGO_DEBUG", default=False, type_=boolish)
Expand Down
4 changes: 4 additions & 0 deletions env.example
Expand Up @@ -26,3 +26,7 @@ export CONNECTED_APP_CLIENT_SECRET=...
export CONNECTED_APP_CALLBACK_URL=...
export CONNECTED_APP_CLIENT_ID=...
export GITHUB_TOKEN=...

# Change this to a new key,
# generated using cryptography.fernet.Fernet.generate_key()
export DB_ENCRYPTION_KEY="Ul-OySkEawSxUc7Ck13Twu2109IzIFh54C1WXO9KAFE="
33 changes: 33 additions & 0 deletions metadeploy/api/migrations/0050_encrypt_tokens.py
@@ -0,0 +1,33 @@
# Generated by Django 2.1.5 on 2019-02-12 20:39

from django.db import migrations

from metadeploy.multisalesforce.views import fernet_decrypt, fernet_encrypt


def forwards(apps, schema_editor):
SocialToken = apps.get_model("socialaccount", "SocialToken")

for token in SocialToken.objects.all():
token.token = fernet_encrypt(token.token)
token.token_secret = fernet_encrypt(token.token_secret)
token.save()


def backwards(apps, schema_editor):
SocialToken = apps.get_model("socialaccount", "SocialToken")

for token in SocialToken.objects.all():
token.token = fernet_decrypt(token.token)
token.token_secret = fernet_decrypt(token.token_secret)
token.save()


class Migration(migrations.Migration):

dependencies = [
("api", "0049_add_all_other_translations"),
("socialaccount", "0003_extra_data_default_dict"),
]

operations = [migrations.RunPython(forwards, backwards)]
4 changes: 3 additions & 1 deletion metadeploy/api/models.py
Expand Up @@ -25,6 +25,8 @@
from parler.models import TranslatableModel, TranslatedFields
from sfdo_template_helpers.fields import MarkdownField

from metadeploy.multisalesforce.views import fernet_decrypt

from .belvedere_utils import convert_to_18
from .constants import ERROR, OPTIONAL, ORGANIZATION_DETAILS
from .push import (
Expand Down Expand Up @@ -146,7 +148,7 @@ def token(self):
account = self.social_account
if account and account.socialtoken_set.exists():
token = self.social_account.socialtoken_set.first()
return (token.token, token.token_secret)
return (fernet_decrypt(token.token), fernet_decrypt(token.token_secret))
return (None, None)

@property
Expand Down
5 changes: 3 additions & 2 deletions metadeploy/conftest.py
Expand Up @@ -19,6 +19,7 @@
Step,
Version,
)
from metadeploy.multisalesforce.views import fernet_encrypt

User = get_user_model()

Expand All @@ -39,8 +40,8 @@ class SocialTokenFactory(factory.django.DjangoModelFactory):
class Meta:
model = SocialToken

token = "0123456789abcdef"
token_secret = "secret.0123456789abcdef"
token = fernet_encrypt("0123456789abcdef")
token_secret = fernet_encrypt("secret.0123456789abcdef")
app = factory.SubFactory(SocialAppFactory)


Expand Down
25 changes: 19 additions & 6 deletions metadeploy/multisalesforce/tests/views.py
Expand Up @@ -6,7 +6,9 @@
LoggingOAuth2CallbackView,
LoggingOAuth2LoginView,
SalesforceOAuth2CustomAdapter,
SaveInstanceUrlMixin,
SalesforceOAuth2Mixin,
fernet_decrypt,
fernet_encrypt,
)


Expand All @@ -17,13 +19,13 @@ def test_SalesforceOAuth2CustomAdapter_base_url(rf):
assert adapter.base_url == "https://foo.my.salesforce.com"


class TestSaveInstanceUrlMixin:
class TestSalesforceOAuth2Mixin:
def test_complete_login(self, mocker, rf):
# This is a mess of terrible mocking and I do not like it.
# This is really just to exercise the mixin, and confirm that it
# assigns instance_url
mocker.patch("requests.get")
adapter = SaveInstanceUrlMixin()
adapter = SalesforceOAuth2Mixin()
adapter.userinfo_url = None
adapter.get_provider = mock.MagicMock()
slfr = mock.MagicMock()
Expand All @@ -33,9 +35,11 @@ def test_complete_login(self, mocker, rf):
adapter.get_provider.return_value = prov_ret
request = rf.get("/")
request.session = {"socialaccount_state": (None, "some-verifier")}
token = mock.MagicMock()
token.token = fernet_encrypt("token")

ret = adapter.complete_login(
request, None, None, response={"instance_url": "https://example.com"}
request, None, token, response={"instance_url": "https://example.com"}
)
assert ret.account.extra_data["instance_url"] == "https://example.com"

Expand All @@ -51,7 +55,7 @@ def test_complete_login_fail(self, rf, mocker):
"userSettings": {"canModifyAllData": False}
}
get.side_effect = [mock.MagicMock(), insufficient_perms_mock]
adapter = SaveInstanceUrlMixin()
adapter = SalesforceOAuth2Mixin()
adapter.userinfo_url = None
adapter.get_provider = mock.MagicMock()
slfr = mock.MagicMock()
Expand All @@ -61,10 +65,19 @@ def test_complete_login_fail(self, rf, mocker):
adapter.get_provider.return_value = prov_ret
request = rf.get("/")
request.session = {"socialaccount_state": (None, "some-verifier")}
token = mock.MagicMock()
token.token = fernet_encrypt("token")

ret = adapter.complete_login(request, None, None, response={})
ret = adapter.complete_login(request, None, token, response={})
assert ret.account.extra_data["organization_details"] is None

def test_parse_token(self):
adapter = SalesforceOAuth2CustomAdapter(request=None)
data = {"access_token": "token", "refresh_token": "token"}

token = adapter.parse_token(data)
assert "token" == fernet_decrypt(token.token)


class TestLoggingOAuth2LoginView:
def test_dispatch(self, rf, mocker):
Expand Down
33 changes: 27 additions & 6 deletions metadeploy/multisalesforce/views.py
Expand Up @@ -9,6 +9,8 @@
SalesforceOAuth2Adapter as SalesforceOAuth2BaseAdapter,
)
from allauth.utils import get_request_param
from cryptography.fernet import Fernet
from django.conf import settings

from metadeploy.api.constants import ORGANIZATION_DETAILS

Expand All @@ -19,15 +21,24 @@
)

logger = logging.getLogger(__name__)
FERNET = Fernet(settings.DB_ENCRYPTION_KEY)


def fernet_encrypt(s):
return FERNET.encrypt(s.encode("utf-8")).decode("utf-8")


def fernet_decrypt(s):
return FERNET.decrypt(s.encode("utf-8")).decode("utf-8")


class SalesforcePermissionsError(Exception):
pass


class SaveInstanceUrlMixin:
class SalesforceOAuth2Mixin:
def get_org_details(self, extra_data, token):
headers = {"Authorization": "Bearer {}".format(token)}
headers = {"Authorization": f"Bearer {token}"}

# Confirm canModifyAllData:
org_info_url = (extra_data["urls"]["rest"] + "connect/organization").format(
Expand All @@ -50,12 +61,14 @@ def get_org_details(self, extra_data, token):
return resp.json()

def complete_login(self, request, app, token, **kwargs):
token = fernet_decrypt(token.token)
headers = {"Authorization": f"Bearer {token}"}
verifier = request.session["socialaccount_state"][1]
logger.info(
"Calling back to Salesforce to complete login.",
extra={"tag": "oauth", "context": {"verifier": verifier}},
)
resp = requests.get(self.userinfo_url, params={"oauth_token": token})
resp = requests.get(self.userinfo_url, headers=headers)
resp.raise_for_status()
extra_data = resp.json()
instance_url = kwargs.get("response", {}).get("instance_url", None)
Expand All @@ -69,18 +82,26 @@ def complete_login(self, request, app, token, **kwargs):
ret.account.extra_data[ORGANIZATION_DETAILS] = org_details
return ret

def parse_token(self, data):
# Encrypt tokens for storage in database
data["access_token"] = fernet_encrypt(data["access_token"])
data["refresh_token"] = fernet_encrypt(data["refresh_token"])
return super().parse_token(data)


class SalesforceOAuth2ProductionAdapter(
SaveInstanceUrlMixin, SalesforceOAuth2BaseAdapter
SalesforceOAuth2Mixin, SalesforceOAuth2BaseAdapter
):
provider_id = SalesforceProductionProvider.id


class SalesforceOAuth2SandboxAdapter(SaveInstanceUrlMixin, SalesforceOAuth2BaseAdapter):
class SalesforceOAuth2SandboxAdapter(
SalesforceOAuth2Mixin, SalesforceOAuth2BaseAdapter
):
provider_id = SalesforceTestProvider.id


class SalesforceOAuth2CustomAdapter(SaveInstanceUrlMixin, SalesforceOAuth2BaseAdapter):
class SalesforceOAuth2CustomAdapter(SalesforceOAuth2Mixin, SalesforceOAuth2BaseAdapter):
provider_id = SalesforceCustomProvider.id

@property
Expand Down

0 comments on commit 60b136e

Please sign in to comment.