-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathadafruit_jwt.py
217 lines (186 loc) · 7.38 KB
/
adafruit_jwt.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_jwt`
==============
JSON Web Token Authentication
* Author(s): Brent Rubell
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
* Adafruit's RSA library:
https://github.com/adafruit/Adafruit_CircuitPython_RSA
* Adafruit's binascii library:
https://github.com/adafruit/Adafruit_CircuitPython_RSA
"""
try:
from typing import Tuple, Union, Optional
from circuitpython_typing import ReadableBuffer
except ImportError:
pass
import io
import json
from adafruit_rsa import PrivateKey, sign
from adafruit_binascii import b2a_base64, a2b_base64
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_JWT.git"
# pylint: disable=no-member
class JWT:
"""JSON Web Token helper for CircuitPython. Warning: JWTs are
credentials, which can grant access to resources. Be careful
where you paste them!
:param str algo: Encryption algorithm used for claims. Can be None.
"""
@staticmethod
def validate(jwt: str) -> Tuple[str, dict]:
"""Validates a provided JWT. Does not support validating
nested signing. Returns JOSE Header and claim set.
:param str jwt: JSON Web Token.
:returns: The message's decoded JOSE header and claims.
:rtype: tuple
"""
# Verify JWT contains at least one period ('.')
if jwt.find(".") == -1:
raise ValueError("ProvidedJWT must have at least one period")
# Attempt to decode JOSE header
try:
jose_header = STRING_TOOLS.urlsafe_b64decode(jwt.split(".")[0])
except UnicodeError as unicode_error:
raise UnicodeError("Unable to decode JOSE header.") from unicode_error
# Check for typ and alg in decoded JOSE header
if "typ" not in jose_header:
raise TypeError("JOSE Header does not contain required type key.")
if "alg" not in jose_header:
raise TypeError("Jose Header does not contain an alg key.")
# Attempt to decode claim set
try:
claims = json.loads(STRING_TOOLS.urlsafe_b64decode(jwt.split(".")[1]))
except UnicodeError as unicode_error:
raise UnicodeError("Invalid claims encoding.") from unicode_error
if not hasattr(claims, "keys"):
raise TypeError("Provided claims is not a JSON dict. object")
return (jose_header, claims)
@staticmethod
def generate(
claims: dict,
private_key_data: Optional[Tuple[int, int, int, int, int]] = None,
algo: Optional[str] = None,
headers: Optional[dict] = None,
) -> str:
"""Generates and returns a new JSON Web Token.
:param dict claims: JWT claims set
:param tuple private_key_data: Decoded RSA private key data.
:param str algo: algorithm to be used. One of None, RS256, RS384 or RS512.
:param dict headers: additional headers for the claim.
:rtype: str
"""
# Allow for unencrypted JWTs
if algo is not None:
priv_key = PrivateKey(*private_key_data)
else:
algo = "none"
# Create the JOSE Header
# https://tools.ietf.org/html/rfc7519#section-5
jose_header = {"typ": "JWT", "alg": algo}
if headers:
jose_header.update(headers)
payload = "{}.{}".format(
STRING_TOOLS.urlsafe_b64encode(json.dumps(jose_header).encode("utf-8")),
STRING_TOOLS.urlsafe_b64encode(json.dumps(claims).encode("utf-8")),
)
# Compute the signature
if algo == "none":
jwt = "{}.{}".format(jose_header, claims)
return jwt
if algo == "RS256":
signature = STRING_TOOLS.urlsafe_b64encode(
sign(payload, priv_key, "SHA-256")
)
elif algo == "RS384":
signature = STRING_TOOLS.urlsafe_b64encode(
sign(payload, priv_key, "SHA-384")
)
elif algo == "RS512":
signature = STRING_TOOLS.urlsafe_b64encode(
sign(payload, priv_key, "SHA-512")
)
else:
raise TypeError(
"Adafruit_JWT is currently only compatible with algorithms within"
"the Adafruit_RSA module."
)
jwt = payload + "." + signature
return jwt
# pylint: disable=invalid-name
class STRING_TOOLS:
"""Tools and helpers for URL-safe string encoding."""
# Some strings for ctype-style character classification
whitespace = " \t\n\r\v\f"
ascii_lowercase = "abcdefghijklmnopqrstuvwxyz"
ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
ascii_letters = ascii_lowercase + ascii_uppercase
digits = "0123456789"
hexdigits = digits + "abcdef" + "ABCDEF"
octdigits = "01234567"
punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
printable = digits + ascii_letters + punctuation + whitespace
@staticmethod
def urlsafe_b64encode(payload: ReadableBuffer) -> str:
"""Encode bytes-like object using the URL- and filesystem-safe alphabet,
which substitutes - instead of + and _ instead of / in
the standard Base64 alphabet, and return the encoded bytes.
:param bytes payload: bytes-like object.
"""
return STRING_TOOLS.translate(
b2a_base64(payload)[:-1].decode("utf-8"), {ord("+"): "-", ord("/"): "_"}
)
@staticmethod
def urlsafe_b64decode(payload: Union[ReadableBuffer, str]) -> str:
"""Decode bytes-like object or ASCII string using the URL
and filesystem-safe alphabet
:param bytes payload: bytes-like object or ASCII string
"""
return a2b_base64(STRING_TOOLS._bytes_from_decode_data(payload)).decode("utf-8")
@staticmethod
def _bytes_from_decode_data(str_data: Union[ReadableBuffer, str]) -> bytes:
# Types acceptable as binary data
bit_types = (bytes, bytearray)
if isinstance(str_data, str):
try:
return str_data.encode("ascii")
except BaseException as error:
raise ValueError(
"string argument should contain only ASCII characters"
) from error
elif isinstance(str_data, bit_types):
return str_data
else:
raise TypeError(
"argument should be bytes or ASCII string, not %s"
% str_data.__class__.__name__
)
# Port of CPython str.translate to Pure-Python by Johan Brichau, 2019
# https://github.com/jbrichau/TrackingPrototype/blob/master/Device/lib/string.py
@staticmethod
def translate(s: str, table: dict) -> str:
"""Return a copy of the string in which each character
has been mapped through the given translation table.
:param string s: String to-be-character-table.
:param dict table: Translation table.
"""
sb = io.StringIO()
for c in s:
v = ord(c)
if v in table:
v = table[v]
if isinstance(v, int):
sb.write(chr(v))
elif v is not None:
sb.write(v)
else:
sb.write(c)
return sb.getvalue()