Skip to content

Commit 43241c8

Browse files
author
Roland Hedberg
committed
Merge pull request #23 from rohe/2and3
2and3
2 parents 785160d + 70a3006 commit 43241c8

23 files changed

+925
-565
lines changed

.idea/libraries/sass_stdlib.xml

Lines changed: 0 additions & 8 deletions
This file was deleted.

.travis.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
language: python
2-
python: "2.7"
32

43
env:
54
- TOX_ENV=py27
6-
7-
python:
8-
- "2.7"
5+
- TOX_ENV=py34
96

107
install:
118
- pip install -U tox
129
- sudo apt-get -qq install libgmp-dev
13-
- pip install -e .
14-
10+
1511
script:
1612
- tox -e $TOX_ENV

README.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ Implementation of JWT, JWS, JWE and JWK as defined in:
1010
Installation
1111
============
1212

13-
Pyjwkest is written and tested using Python version 2.7. A Python 3.4 version
14-
is in the works.
13+
Pyjwkest is written and tested using Python version 2.7 and 3.4.
1514

1615
You should be able to simply run 'python setup.py install' to install it.
1716

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,18 @@ def run_tests(self):
3939

4040
setup(
4141
name="pyjwkest",
42-
version="0.6.2",
42+
version="1.0.0",
4343
description="Python implementation of JWT, JWE, JWS and JWK",
4444
author="Roland Hedberg",
4545
author_email="[email protected]",
4646
license="Apache 2.0",
47-
packages=["jwkest", "cryptlib"],
47+
packages=["jwkest"],
4848
package_dir={"": "src"},
4949
classifiers=["Development Status :: 4 - Beta",
5050
"License :: OSI Approved :: Apache Software License",
5151
"Topic :: Software Development :: Libraries :: Python "
5252
"Modules"],
53-
install_requires=["pycrypto >= 2.6.1", "requests"],
53+
install_requires=["pycrypto >= 2.6.1", "requests", "six", "future"],
5454
tests_require=['pytest'],
5555
zip_safe=False,
5656
cmdclass={'test': PyTest},

src/cryptlib/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/cryptlib/PBKDF2.py renamed to src/jwkest/PBKDF2.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,15 @@
7070

7171
__version__ = "1.2"
7272

73+
from builtins import chr
74+
from builtins import zip
75+
from builtins import range
76+
from builtins import object
77+
78+
import string
7379
from struct import pack
7480
from binascii import b2a_hex
7581
from random import randint
76-
import string
7782

7883
try:
7984
# Use PyCrypto (if available)
@@ -149,7 +154,7 @@ def __f(self, i):
149154
assert 1 <= i <= 0xffffffff
150155
U = self.__prf(self.__passphrase, self.__salt + pack("!L", i))
151156
result = U
152-
for j in xrange(2, 1+self.__iterations):
157+
for j in range(2, 1+self.__iterations):
153158
U = self.__prf(self.__passphrase, U)
154159
result = strxor(result, U)
155160
return result
@@ -166,17 +171,17 @@ def _setup(self, passphrase, salt, iterations, prf):
166171

167172
# passphrase and salt must be str or unicode (in the latter
168173
# case, we convert to UTF-8)
169-
if isinstance(passphrase, unicode):
174+
if isinstance(passphrase, str):
170175
passphrase = passphrase.encode("UTF-8")
171176
if not isinstance(passphrase, str):
172177
raise TypeError("passphrase must be str or unicode")
173-
if isinstance(salt, unicode):
178+
if isinstance(salt, str):
174179
salt = salt.encode("UTF-8")
175180
if not isinstance(salt, str):
176181
raise TypeError("salt must be str or unicode")
177182

178183
# iterations must be an integer >= 1
179-
if not isinstance(iterations, (int, long)):
184+
if not isinstance(iterations, (int, int)):
180185
raise TypeError("iterations must be an integer")
181186
if iterations < 1:
182187
raise ValueError("iterations must be at least 1")
@@ -218,13 +223,13 @@ def crypt(word, salt=None, iterations=None):
218223
salt = _makesalt()
219224

220225
# salt must be a string or the us-ascii subset of unicode
221-
if isinstance(salt, unicode):
226+
if isinstance(salt, str):
222227
salt = salt.encode("us-ascii")
223228
if not isinstance(salt, str):
224229
raise TypeError("salt must be a string")
225230

226231
# word must be a string or unicode (in the latter case, we convert to UTF-8)
227-
if isinstance(word, unicode):
232+
if isinstance(word, str):
228233
word = word.encode("UTF-8")
229234
if not isinstance(word, str):
230235
raise TypeError("word must be a string or unicode")

src/jwkest/__init__.py

Lines changed: 83 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""JSON Web Token"""
2-
3-
# Most of the code herein I have borrowed/stolen from other people
4-
# Most notably Jeff Lindsay, Ryan Kelly
5-
62
import base64
7-
from binascii import unhexlify
8-
from binascii import hexlify
9-
import json
103
import logging
114
import re
5+
import struct
6+
7+
from builtins import zip
8+
from builtins import hex
9+
from builtins import str
10+
11+
from binascii import unhexlify
1212

1313
logger = logging.getLogger(__name__)
1414

@@ -61,6 +61,68 @@ class MissingKey(JWKESTException):
6161
""" No usable key """
6262

6363

64+
# ---------------------------------------------------------------------------
65+
# Helper functions
66+
67+
68+
def intarr2bin(arr):
69+
return unhexlify(''.join(["%02x" % byte for byte in arr]))
70+
71+
72+
def long2hexseq(l):
73+
try:
74+
return unhexlify(hex(l)[2:])
75+
except TypeError:
76+
return unhexlify(hex(l)[2:-1])
77+
78+
79+
def intarr2long(arr):
80+
return int(''.join(["%02x" % byte for byte in arr]), 16)
81+
82+
83+
def long2intarr(long_int):
84+
_bytes = []
85+
while long_int:
86+
long_int, r = divmod(long_int, 256)
87+
_bytes.insert(0, r)
88+
return _bytes
89+
90+
91+
def long_to_base64(n):
92+
bys = long2intarr(n)
93+
data = struct.pack('%sB' % len(bys), *bys)
94+
if not len(data):
95+
data = '\x00'
96+
s = base64.urlsafe_b64encode(data).rstrip(b'=')
97+
return s
98+
99+
100+
def base64_to_long(data):
101+
# if isinstance(data, str):
102+
# data = bytes(data)
103+
# urlsafe_b64decode will happily convert b64encoded data
104+
_d = base64.urlsafe_b64decode(bytes(data) + b'==')
105+
return intarr2long(struct.unpack('%sB' % len(_d), _d))
106+
107+
108+
def base64url_to_long(data):
109+
"""
110+
Stricter then base64_to_long since it really checks that it's
111+
base64url encoded
112+
113+
:param data: The base64 string
114+
:return:
115+
"""
116+
_d = base64.urlsafe_b64decode(bytes(data) + b'==')
117+
# verify that it's base64url encoded and not just base64
118+
# that is no '+' and '/' characters and not trailing "="s.
119+
if [e for e in [b'+', b'/', b'='] if e in data]:
120+
raise ValueError("Not base64url encoded")
121+
return intarr2long(struct.unpack('%sB' % len(_d), _d))
122+
123+
124+
# =============================================================================
125+
64126
def b64e(b):
65127
"""Base64 encode some bytes.
66128
@@ -88,10 +150,13 @@ def add_padding(b):
88150
def b64d(b):
89151
"""Decode some base64-encoded bytes.
90152
91-
Raises BadSyntax if the string contains invalid characters or padding."""
153+
Raises BadSyntax if the string contains invalid characters or padding.
92154
93-
if b.endswith("="): # shouldn't but there you are
94-
cb = b.split("=")[0]
155+
:param b: bytes
156+
"""
157+
158+
if b.endswith(b'='): # shouldn't but there you are
159+
cb = b.split(b'=')[0]
95160
else:
96161
cb = b
97162

@@ -106,13 +171,6 @@ def b64d(b):
106171
return base64.urlsafe_b64decode(b)
107172

108173

109-
def split_token(token):
110-
if not token.count(b"."):
111-
raise BadSyntax(token,
112-
"expected token to contain at least one dot")
113-
return tuple(token.split(b"."))
114-
115-
116174
# 'Stolen' from Werkzeug
117175
def safe_str_cmp(a, b):
118176
"""Compare two strings in constant time."""
@@ -124,64 +182,11 @@ def safe_str_cmp(a, b):
124182
return r == 0
125183

126184

127-
def unpack(token):
128-
"""
129-
Unpacks a JWT into its parts and base64 decodes the parts individually
130-
131-
:param token: The JWT
132-
:return: A tuple of the header, claim, crypto parts plus the header
133-
and claims part before base64 decoding
134-
"""
135-
if isinstance(token, unicode):
136-
token = str(token)
137-
138-
part = split_token(token)
139-
140-
res = [b64d(p) for p in part]
141-
142-
res[0] = json.loads(res[0])
143-
res.append(part[0])
144-
res.append(part[1])
145-
return res
146-
147-
148-
def pack(payload):
149-
"""
150-
Unsigned JWT
151-
"""
152-
header = {'alg': 'none'}
153-
154-
header_b64 = b64e(json.dumps(header, separators=(",", ":")))
155-
if isinstance(payload, basestring):
156-
payload_b64 = b64e(payload)
157-
else:
158-
payload_b64 = b64e(json.dumps(payload, separators=(",", ":")))
159-
160-
token = header_b64 + b"." + payload_b64 + b"."
161-
162-
return token
163-
164-
# ---------------------------------------------------------------------------
165-
# Helper functions
166-
167-
168-
def intarr2bin(arr):
169-
return unhexlify(''.join(["%02x" % byte for byte in arr]))
170-
171-
172-
def intarr2long(arr):
173-
return long(''.join(["%02x" % byte for byte in arr]), 16)
174-
175-
176-
def hd2ia(s):
177-
#half = len(s)/2
178-
return [int(s[i] + s[i + 1], 16) for i in range(0, len(s), 2)]
179-
180-
181-
def dehexlify(bi):
182-
s = hexlify(bi)
183-
return [int(s[i] + s[i + 1], 16) for i in range(0, len(s), 2)]
184-
185-
186-
def long2hexseq(l):
187-
return unhexlify(hex(l)[2:-1])
185+
def constant_time_compare(a, b):
186+
"""Compare two strings in constant time."""
187+
if len(a) != len(b):
188+
return False
189+
r = 0
190+
for c, d in zip(a, b):
191+
r |= c ^ d
192+
return r == 0

src/cryptlib/aes_gcm.py renamed to src/jwkest/aes_gcm.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
DEALINGS IN THE SOFTWARE.
2323
"""
2424
from __future__ import print_function
25+
from __future__ import division
26+
from builtins import str
27+
from builtins import hex
28+
from builtins import range
29+
from builtins import object
30+
#from past.utils import old_div
2531

2632
from Crypto.Cipher import AES
2733
from Crypto.Util import Counter
@@ -55,7 +61,7 @@ def __str__(self):
5561

5662

5763
# Galois/Counter Mode with AES-128 and 96-bit IV
58-
class AES_GCM:
64+
class AES_GCM(object):
5965
def __init__(self, master_key):
6066
self.prev_init_value = None
6167
self._master_key = ""
@@ -107,7 +113,7 @@ def __ghash(self, aad, txt):
107113

108114
tag = 0
109115
assert len(data) % 16 == 0
110-
for i in range(len(data) / 16):
116+
for i in range(int(len(data) / 16)):
111117
tag ^= bytes_to_long(data[i * 16: (i + 1) * 16])
112118
tag = self.__times_auth_key(tag)
113119
# print 'X\t', hex(tag)

src/cryptlib/aes_key_wrap.py renamed to src/jwkest/aes_key_wrap.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@
1515
Performance should be reasonable, since the heavy lifting is all done in
1616
PyCrypto's AES.
1717
"""
18-
18+
from __future__ import division
19+
from builtins import hex
20+
from builtins import range
1921
import struct
2022
from Crypto.Cipher import AES
2123

2224
QUAD = struct.Struct('>Q')
2325

2426

2527
def aes_unwrap_key_and_iv(kek, wrapped):
26-
n = len(wrapped) / 8 - 1
28+
n = (len(wrapped) // 8) - 1
2729
#NOTE: R[0] is never accessed, left in for consistency with RFC indices
2830
r = [None] + [wrapped[i * 8:i * 8 + 8] for i in range(1, n + 1)]
2931
a = QUAD.unpack(wrapped[:8])[0]
@@ -57,19 +59,16 @@ def aes_unwrap_key_withpad(kek, wrapped):
5759

5860

5961
def aes_wrap_key(kek, plaintext, iv=0xa6a6a6a6a6a6a6a6):
60-
n = len(plaintext) / 8
62+
n = len(plaintext) // 8
6163
r = [None] + [plaintext[i * 8:i * 8 + 8] for i in range(0, n)]
6264
a = iv
6365
encrypt = AES.new(kek).encrypt
6466
for j in range(6):
65-
#import binascii
66-
#print hex(a), binascii.hexlify(r[1]), binascii.hexlify(r[2])
67-
6867
for i in range(1, n + 1):
6968
b = encrypt(QUAD.pack(a) + r[i])
7069
a = QUAD.unpack(b[:8])[0] ^ (n * j + i)
7170
r[i] = b[8:]
72-
return QUAD.pack(a) + "".join(r[1:])
71+
return QUAD.pack(a) + b''.join(r[1:])
7372

7473

7574
def aes_wrap_key_withpad(kek, plaintext):
File renamed without changes.

0 commit comments

Comments
 (0)