Skip to content

Commit 9b2aab9

Browse files
committed
Python 3 Support
1 parent b283799 commit 9b2aab9

6 files changed

Lines changed: 191 additions & 139 deletions

File tree

.travis.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
language: python
22
python:
33
- "2.7"
4+
- "3.3"
5+
- "3.4"
6+
- "3.5"
7+
- "3.5-dev"
8+
- "nightly"
9+
- "pypy"
410
install:
511
- pip install tox
6-
script: tox
12+
script:
13+
- tox -e travis

jose.py

Lines changed: 104 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1+
import binascii
2+
import datetime
13
import logging
2-
logger = logging.getLogger(__name__)
3-
4-
try:
5-
from cjson import encode as json_encode, decode as json_decode
6-
except ImportError: # pragma: nocover
7-
logger.warn('cjson not found, falling back to stdlib json')
8-
from json import loads as json_decode, dumps as json_encode
9-
4+
import six
105
import zlib
11-
import datetime
126

137
from base64 import urlsafe_b64encode, urlsafe_b64decode
148
from collections import namedtuple
159
from copy import deepcopy
16-
from time import time
10+
from json import loads as json_decode, dumps as json_encode
1711
from struct import pack
12+
from time import time
1813

1914
from Crypto.Hash import HMAC, SHA256, SHA384, SHA512
2015
from Crypto.Cipher import PKCS1_OAEP, AES
2116
from Crypto.PublicKey import RSA
2217
from Crypto.Random import get_random_bytes
2318
from Crypto.Signature import PKCS1_v1_5 as PKCS1_v1_5_SIG
2419

20+
logger = logging.getLogger(__name__)
21+
2522

2623
__all__ = ['encrypt', 'decrypt', 'sign', 'verify']
2724

@@ -63,7 +60,8 @@
6360
class Error(Exception):
6461
""" The base error type raised by jose
6562
"""
66-
pass
63+
def __init__(self, message):
64+
self.message = message
6765

6866

6967
class Expired(Error):
@@ -85,7 +83,7 @@ def serialize_compact(jwt):
8583
:returns: A string, representing the compact serialization of a
8684
:class:`~jose.JWE` or :class:`~jose.JWS`.
8785
"""
88-
return '.'.join(jwt)
86+
return six.b('.').join(jwt)
8987

9088

9189
def deserialize_compact(jwt):
@@ -95,7 +93,7 @@ def deserialize_compact(jwt):
9593
:rtype: :class:`~jose.JWT`.
9694
:raises: :class:`~jose.Error` if the JWT is malformed
9795
"""
98-
parts = jwt.split('.')
96+
parts = jwt.split(six.b('.'))
9997

10098
# http://tools.ietf.org/html/
10199
# draft-ietf-jose-json-web-encryption-23#section-9
@@ -109,8 +107,8 @@ def deserialize_compact(jwt):
109107
return token_type(*parts)
110108

111109

112-
def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP',
113-
enc='A128CBC-HS256', rng=get_random_bytes, compression=None):
110+
def encrypt(claims, jwk, adata=six.b(''), add_header=None, alg='RSA-OAEP',
111+
enc='A128CBC-HS256', rng=get_random_bytes, compression=None):
114112
""" Encrypts the given claims and produces a :class:`~jose.JWE`
115113
116114
:param claims: A `dict` representing the claims for this
@@ -139,14 +137,15 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP',
139137
assert _TEMP_VER_KEY not in claims
140138
claims[_TEMP_VER_KEY] = _TEMP_VER
141139

142-
header = dict((add_header or {}).items() + [
143-
('enc', enc), ('alg', alg)])
140+
header = dict(
141+
list((add_header or {}).items()) + [('enc', enc), ('alg', alg)]
142+
)
144143

145144
# promote the temp key to the header
146145
assert _TEMP_VER_KEY not in header
147146
header[_TEMP_VER_KEY] = claims[_TEMP_VER_KEY]
148147

149-
plaintext = json_encode(claims)
148+
plaintext = six.b(json_encode(claims))
150149

151150
# compress (if required)
152151
if compression is not None:
@@ -162,24 +161,29 @@ def encrypt(claims, jwk, adata='', add_header=None, alg='RSA-OAEP',
162161
((cipher, _), key_size), ((hash_fn, _), hash_mod) = JWA[enc]
163162
iv = rng(AES.block_size)
164163
encryption_key = rng(hash_mod.digest_size)
164+
encryption_key_index = hash_mod.digest_size // 2
165165

166-
ciphertext = cipher(plaintext, encryption_key[-hash_mod.digest_size/2:], iv)
167-
hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata),
168-
encryption_key[:-hash_mod.digest_size/2], hash_mod)
166+
ciphertext = cipher(
167+
plaintext, encryption_key[-encryption_key_index:], iv
168+
)
169+
hash = hash_fn(
170+
_jwe_hash_str(ciphertext, iv, adata),
171+
encryption_key[:-encryption_key_index], hash_mod
172+
)
169173

170174
# cek encryption
171175
(cipher, _), _ = JWA[alg]
172176
encryption_key_ciphertext = cipher(encryption_key, jwk)
173177

174-
return JWE(*map(b64encode_url,
175-
(json_encode(header),
176-
encryption_key_ciphertext,
177-
iv,
178-
ciphertext,
179-
auth_tag(hash))))
178+
jwe_components = (
179+
json_encode(header), encryption_key_ciphertext, iv, ciphertext,
180+
auth_tag(hash)
181+
)
182+
return JWE(*map(b64encode_url, jwe_components))
180183

181184

182-
def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None):
185+
def decrypt(jwe, jwk, adata=six.b(''), validate_claims=True,
186+
expiry_seconds=None):
183187
""" Decrypts a deserialized :class:`~jose.JWE`
184188
185189
:param jwe: An instance of :class:`~jose.JWE`
@@ -199,8 +203,9 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None):
199203
:raises: :class:`~jose.Error` if there is an error decrypting the JWE
200204
"""
201205
header, encryption_key_ciphertext, iv, ciphertext, tag = map(
202-
b64decode_url, jwe)
203-
header = json_decode(header)
206+
b64decode_url, jwe
207+
)
208+
header = json_decode(header.decode())
204209

205210
# decrypt cek
206211
(_, decipher), _ = JWA[header['alg']]
@@ -211,9 +216,13 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None):
211216

212217
version = header.get(_TEMP_VER_KEY)
213218
if version:
214-
plaintext = decipher(ciphertext, encryption_key[-mod.digest_size/2:], iv)
215-
hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version),
216-
encryption_key[:-mod.digest_size/2], mod=mod)
219+
plaintext = decipher(
220+
ciphertext, encryption_key[-mod.digest_size // 2:], iv
221+
)
222+
hash = hash_fn(
223+
_jwe_hash_str(ciphertext, iv, adata, version),
224+
encryption_key[:-mod.digest_size // 2], mod=mod
225+
)
217226
else:
218227
plaintext = decipher(ciphertext, encryption_key[:-mod.digest_size], iv)
219228
hash = hash_fn(_jwe_hash_str(ciphertext, iv, adata, version),
@@ -231,7 +240,7 @@ def decrypt(jwe, jwk, adata='', validate_claims=True, expiry_seconds=None):
231240

232241
plaintext = decompress(plaintext)
233242

234-
claims = json_decode(plaintext)
243+
claims = json_decode(plaintext.decode())
235244
try:
236245
del claims[_TEMP_VER_KEY]
237246
except KeyError:
@@ -257,11 +266,12 @@ def sign(claims, jwk, add_header=None, alg='HS256'):
257266
"""
258267
(hash_fn, _), mod = JWA[alg]
259268

260-
header = dict((add_header or {}).items() + [('alg', alg)])
269+
header = dict(list((add_header or {}).items()) + [('alg', alg)])
261270
header, payload = map(b64encode_url, map(json_encode, (header, claims)))
262271

263-
sig = b64encode_url(hash_fn(_jws_hash_str(header, payload), jwk['k'],
264-
mod=mod))
272+
sig = b64encode_url(
273+
hash_fn(_jws_hash_str(header, payload), jwk['k'], mod=mod)
274+
)
265275

266276
return JWS(header, payload, sig)
267277

@@ -285,17 +295,18 @@ def verify(jws, jwk, alg, validate_claims=True, expiry_seconds=None):
285295
:raises: :class:`~jose.Error` if there is an error decrypting the JWE
286296
"""
287297
header, payload, sig = map(b64decode_url, jws)
288-
header = json_decode(header)
298+
header = json_decode(header.decode())
289299
if alg != header['alg']:
290300
raise Error('Invalid algorithm')
291301

292302
(_, verify_fn), mod = JWA[header['alg']]
293303

294-
if not verify_fn(_jws_hash_str(jws.header, jws.payload),
295-
jwk['k'], sig, mod=mod):
304+
if not verify_fn(
305+
_jws_hash_str(jws.header, jws.payload), jwk['k'], sig, mod=mod
306+
):
296307
raise Error('Mismatched signatures')
297308

298-
claims = json_decode(b64decode_url(jws.payload))
309+
claims = json_decode(b64decode_url(jws.payload).decode())
299310
_validate(claims, validate_claims, expiry_seconds)
300311

301312
return JWT(header, claims)
@@ -305,27 +316,32 @@ def b64decode_url(istr):
305316
""" JWT Tokens may be truncated without the usual trailing padding '='
306317
symbols. Compensate by padding to the nearest 4 bytes.
307318
"""
308-
istr = encode_safe(istr)
309319
try:
310-
return urlsafe_b64decode(istr + '=' * (4 - (len(istr) % 4)))
311-
except TypeError as e:
320+
return urlsafe_b64decode(istr + six.b('=') * (4 - (len(istr) % 4)))
321+
except (TypeError, binascii.Error) as e:
312322
raise Error('Unable to decode base64: %s' % (e))
313323

314324

315325
def b64encode_url(istr):
316326
""" JWT Tokens may be truncated without the usual trailing padding '='
317327
symbols. Compensate by padding to the nearest 4 bytes.
318328
"""
319-
return urlsafe_b64encode(encode_safe(istr)).rstrip('=')
329+
return urlsafe_b64encode(encode_safe(istr)).rstrip(six.b('='))
320330

321331

322-
def encode_safe(istr, encoding='utf8'):
323-
try:
324-
return istr.encode(encoding)
325-
except UnicodeDecodeError:
326-
# this will fail if istr is already encoded
327-
pass
328-
return istr
332+
if six.PY3:
333+
def encode_safe(istr, encoding='utf8'):
334+
if not isinstance(istr, bytes):
335+
return bytes(istr, encoding=encoding)
336+
return istr
337+
else:
338+
def encode_safe(istr, encoding='utf8'):
339+
try:
340+
return istr.encode(encoding)
341+
except UnicodeDecodeError:
342+
# this will fail if istr is already encoded
343+
pass
344+
return istr
329345

330346

331347
def auth_tag(hmac):
@@ -336,11 +352,15 @@ def auth_tag(hmac):
336352

337353
def pad_pkcs7(s):
338354
sz = AES.block_size - (len(s) % AES.block_size)
339-
return s + (chr(sz) * sz)
355+
return s + (six.int2byte(sz) * sz)
340356

341357

342-
def unpad_pkcs7(s):
343-
return s[:-ord(s[-1])]
358+
if six.PY3:
359+
def unpad_pkcs7(s):
360+
return s[:-s[-1]]
361+
else:
362+
def unpad_pkcs7(s):
363+
return s[:-ord(s[-1])]
344364

345365

346366
def encrypt_oaep(plaintext, jwk):
@@ -391,14 +411,24 @@ def decrypt_aescbc(ciphertext, key, iv):
391411
return unpad_pkcs7(AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext))
392412

393413

394-
def const_compare(stra, strb):
395-
if len(stra) != len(strb):
396-
return False
414+
if six.PY3:
415+
def const_compare(stra, strb):
416+
if len(stra) != len(strb):
417+
return False
418+
419+
res = 0
420+
for a, b in zip(stra, strb):
421+
res |= a ^ b
422+
return res == 0
423+
else:
424+
def const_compare(stra, strb):
425+
if len(stra) != len(strb):
426+
return False
397427

398-
res = 0
399-
for a, b in zip(stra, strb):
400-
res |= ord(a) ^ ord(b)
401-
return res == 0
428+
res = 0
429+
for a, b in zip(stra, strb):
430+
res |= ord(a) ^ ord(b)
431+
return res == 0
402432

403433

404434
class _JWA(object):
@@ -525,34 +555,36 @@ def _validate(claims, validate_claims, expiry_seconds):
525555
_check_not_before(now, not_before)
526556

527557

528-
def _jwe_hash_str(ciphertext, iv, adata='', version=_TEMP_VER):
558+
def _jwe_hash_str(ciphertext, iv, adata=six.b(''), version=_TEMP_VER):
529559
# http://tools.ietf.org/html/
530560
# draft-ietf-jose-json-web-algorithms-24#section-5.2.2.1
531561
# Both tokens without version and with version 1 should be ignored in
532562
# the future as they use incorrect hashing. The version parameter
533563
# should also be removed.
534564
if not version:
535-
return '.'.join((adata, iv, ciphertext, str(len(adata))))
565+
return six.b('.').join(
566+
(adata, iv, ciphertext, six.b(str(len(adata))))
567+
)
536568
elif version == 1:
537-
return '.'.join((adata, iv, ciphertext, pack("!Q", len(adata) * 8)))
538-
return ''.join((adata, iv, ciphertext, pack("!Q", len(adata) * 8)))
569+
return six.b('.').join(
570+
(adata, iv, ciphertext, pack("!Q", len(adata) * 8))
571+
)
572+
return six.b('').join(
573+
(adata, iv, ciphertext, pack("!Q", len(adata) * 8))
574+
)
539575

540576

541577
def _jws_hash_str(header, claims):
542-
return '.'.join((header, claims))
578+
return six.b('.').join((header, claims))
543579

544580

545581
def cli_decrypt(jwt, key):
546-
print decrypt(deserialize_compact(jwt), {'k':key},
547-
validate_claims=False)
582+
print(decrypt(deserialize_compact(jwt), {'k': key}, validate_claims=False))
548583

549584

550585
def _cli():
551586
import inspect
552-
import sys
553-
554587
from argparse import ArgumentParser
555-
from copy import copy
556588

557589
parser = ArgumentParser()
558590
subparsers = parser.add_subparsers(dest='subparser_name')

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pycrypto >= 2.6
2+
six

0 commit comments

Comments
 (0)