launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17031
[Merge] lp:~wgrant/launchpad/oauth-secret-sha256 into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/oauth-secret-sha256 into lp:launchpad with lp:~wgrant/launchpad/oauth-secret-concealment as a prerequisite.
Commit message:
Provide a feature flag to let OAuth secrets be stored as SHA-256 rather than plaintext. Either form is accepted while we migrate.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/oauth-secret-sha256/+merge/224778
Provide a feature flag to let OAuth secrets be stored as SHA-256 rather than plaintext. Either form is accepted while we migrate.
We don't need a computationally expensive KDF here, as the tokens already have log(50, 2) * 80 ~~ 300 bits of entropy, so they're not feasible to attack with bruteforce, rainbow tables, or similar approaches.
--
https://code.launchpad.net/~wgrant/launchpad/oauth-secret-sha256/+merge/224778
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/oauth-secret-sha256 into lp:launchpad.
=== modified file 'lib/lp/services/oauth/model.py'
--- lib/lp/services/oauth/model.py 2014-06-27 09:38:50 +0000
+++ lib/lp/services/oauth/model.py 2014-06-27 09:38:50 +0000
@@ -16,6 +16,7 @@
datetime,
timedelta,
)
+import hashlib
import re
import pytz
@@ -43,6 +44,7 @@
from lp.services.database.interfaces import IMasterStore
from lp.services.database.sqlbase import SQLBase
from lp.services.database.stormbase import StormBase
+from lp.services.features import getFeatureFlag
from lp.services.librarian.model import LibraryFileAlias
from lp.services.oauth.interfaces import (
ClockSkew,
@@ -114,6 +116,11 @@
key = StringCol(notNull=True)
_secret = StringCol(dbName="secret", notNull=False, default='')
+ def __init__(self, key, secret):
+ if getFeatureFlag('auth.hash_oauth_secrets'):
+ secret = hashlib.sha256(secret).hexdigest()
+ super(OAuthConsumer, self).__init__(key=key, _secret=secret)
+
# This regular expression singles out a consumer key that
# represents any and all apps running on a specific computer. The
# regular expression identifies the system type (eg. the OS) and
@@ -173,13 +180,13 @@
def isSecretValid(self, secret):
"""See `IOAuthConsumer`."""
- return secret == self._secret
+ return self._secret in (secret, hashlib.sha256(secret).hexdigest())
def newRequestToken(self):
"""See `IOAuthConsumer`."""
key, secret = create_token_key_and_secret(table=OAuthRequestToken)
return (
- OAuthRequestToken(consumer=self, key=key, _secret=secret), secret)
+ OAuthRequestToken(consumer=self, key=key, secret=secret), secret)
def getAccessToken(self, key):
"""See `IOAuthConsumer`."""
@@ -198,7 +205,7 @@
"""See `IOAuthConsumerSet`."""
assert self.getByKey(key) is None, (
"The key '%s' is already in use by another consumer." % key)
- return OAuthConsumer(key=key, _secret=secret)
+ return OAuthConsumer(key=key, secret=secret)
def getByKey(self, key):
"""See `IOAuthConsumerSet`."""
@@ -232,6 +239,17 @@
dbName='distribution', foreignKey='Distribution',
notNull=False, default=None)
+ def __init__(self, consumer, permission, key, secret='', person=None,
+ date_expires=None, product=None, project=None,
+ distribution=None, sourcepackagename=None):
+ if getFeatureFlag('auth.hash_oauth_secrets'):
+ secret = hashlib.sha256(secret).hexdigest()
+ super(OAuthAccessToken, self).__init__(
+ consumer=consumer, permission=permission, key=key,
+ _secret=secret, person=person, date_expires=date_expires,
+ product=product, project=project, distribution=distribution,
+ sourcepackagename=sourcepackagename)
+
@property
def context(self):
"""See `IOAuthToken`."""
@@ -255,7 +273,7 @@
def isSecretValid(self, secret):
"""See `IOAuthConsumer`."""
- return secret == self._secret
+ return self._secret in (secret, hashlib.sha256(secret).hexdigest())
def checkNonceAndTimestamp(self, nonce, timestamp):
"""See `IOAuthAccessToken`."""
@@ -320,6 +338,17 @@
dbName='distribution', foreignKey='Distribution',
notNull=False, default=None)
+ def __init__(self, consumer, key, secret='', permission=None, person=None,
+ date_expires=None, product=None, project=None,
+ distribution=None, sourcepackagename=None):
+ if getFeatureFlag('auth.hash_oauth_secrets'):
+ secret = hashlib.sha256(secret).hexdigest()
+ super(OAuthRequestToken, self).__init__(
+ consumer=consumer, permission=permission, key=key,
+ _secret=secret, person=person, date_expires=date_expires,
+ product=product, project=project, distribution=distribution,
+ sourcepackagename=sourcepackagename)
+
@property
def context(self):
"""See `IOAuthToken`."""
@@ -344,7 +373,7 @@
def isSecretValid(self, secret):
"""See `IOAuthConsumer`."""
- return secret == self._secret
+ return self._secret in (secret, hashlib.sha256(secret).hexdigest())
def review(self, user, permission, context=None, date_expires=None):
"""See `IOAuthRequestToken`."""
@@ -389,7 +418,7 @@
access_level = AccessLevel.items[self.permission.name]
access_token = OAuthAccessToken(
consumer=self.consumer, person=self.person, key=key,
- _secret=secret, permission=access_level,
+ secret=secret, permission=access_level,
date_expires=self.date_expires, product=self.product,
project=self.project, distribution=self.distribution,
sourcepackagename=self.sourcepackagename)
=== modified file 'lib/lp/services/oauth/tests/test_tokens.py'
--- lib/lp/services/oauth/tests/test_tokens.py 2014-06-27 09:38:50 +0000
+++ lib/lp/services/oauth/tests/test_tokens.py 2014-06-27 09:38:50 +0000
@@ -11,6 +11,7 @@
datetime,
timedelta,
)
+import hashlib
import pytz
import transaction
@@ -18,6 +19,7 @@
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
+from lp.services.features.testing import FeatureFixture
from lp.services.mail import stub
from lp.services.oauth.interfaces import (
IOAuthAccessToken,
@@ -105,6 +107,8 @@
class TestRequestTokens(TestOAuth):
"""Tests for OAuth request token objects."""
+ secret_len = 80
+
def test_newRequestToken(self):
request_token, secret = self.consumer.newRequestToken()
verifyObject(IOAuthRequestToken, request_token)
@@ -114,7 +118,8 @@
def test_key_and_secret_automatically_generated(self):
request_token, secret = self.consumer.newRequestToken()
self.assertEqual(len(request_token.key), 20)
- self.assertEqual(len(removeSecurityProxy(request_token)._secret), 80)
+ self.assertEqual(
+ len(removeSecurityProxy(request_token)._secret), self.secret_len)
def test_date_created(self):
request_token, _ = self.consumer.newRequestToken()
@@ -263,6 +268,23 @@
self.assertEquals(person.oauth_request_tokens.count(), 0)
+class TestRequestTokensHashed(TestRequestTokens):
+
+ secret_len = 64
+
+ def setUp(self):
+ super(TestRequestTokensHashed, self).setUp()
+ self.useFixture(FeatureFixture({'auth.hash_oauth_secrets': 'on'}))
+
+ def test_newRequestToken(self):
+ request_token, secret = self.consumer.newRequestToken()
+ verifyObject(IOAuthRequestToken, request_token)
+ self.assertIsInstance(secret, unicode)
+ self.assertEqual(
+ removeSecurityProxy(request_token)._secret,
+ hashlib.sha256(secret).hexdigest())
+
+
class TestAccessTokens(TestOAuth):
"""Tests for OAuth access tokens."""
@@ -389,6 +411,24 @@
self.assertEquals(person.oauth_access_tokens.count(), 0)
+class TestAccessTokensHashed(TestAccessTokens):
+
+ def setUp(self):
+ super(TestAccessTokensHashed, self).setUp()
+ self.useFixture(FeatureFixture({'auth.hash_oauth_secrets': 'on'}))
+
+ def test_exchange_request_token_for_access_token(self):
+ # Make sure the basic exchange of request token for access
+ # token works.
+ request_token, access_token, access_secret = (
+ self._exchange_request_token_for_access_token())
+ verifyObject(IOAuthAccessToken, access_token)
+ self.assertIsInstance(access_secret, unicode)
+ self.assertEqual(
+ removeSecurityProxy(access_token)._secret,
+ hashlib.sha256(access_secret).hexdigest())
+
+
class TestHelperFunctions(TestOAuth):
def setUp(self):
Follow ups