mail/tools/jwt.py

92 lines
3.5 KiB
Python
Raw Permalink Normal View History

2024-05-03 12:40:35 +03:00
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import hashlib
import json
import binascii
import time
import enum
import hmac
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
class Algorithm(enum.Enum):
ES256 = "ES256" # ECDSA SHA-256
HS256 = "HS256" # HMAC SHA-256
def _generate_keys(key_encoding, key_format) -> (bytes, bytes):
private_object = ec.generate_private_key(ec.SECP256R1(), default_backend())
private_int = private_object.private_numbers().private_value
private_bytes = private_int.to_bytes(32, "big")
public_object = private_object.public_key()
public_bytes = public_object.public_bytes(
encoding=key_encoding,
format=key_format,
)
return private_bytes, public_bytes
def generate_vapid_keys() -> (str, str):
"""
Generate the VAPID (Voluntary Application Server Identification) used for the Web Push
This function generates a signing key pair usable with the Elliptic Curve Digital
Signature Algorithm (ECDSA) over the P-256 curve.
https://www.rfc-editor.org/rfc/rfc8292
:return: tuple (private_key, public_key)
"""
private, public = _generate_keys(serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint)
private_string = base64.urlsafe_b64encode(private).decode("ascii").strip("=")
public_string = base64.urlsafe_b64encode(public).decode("ascii").strip("=")
return private_string, public_string
def base64_decode_with_padding(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "==")
def _generate_jwt(claims: dict, key: str, algorithm: Algorithm) -> str:
JOSE_header = base64.urlsafe_b64encode(json.dumps({"typ": "JWT", "alg": algorithm.value}).encode())
payload = base64.urlsafe_b64encode(json.dumps(claims).encode())
unsigned_token = "{}.{}".format(JOSE_header.decode().strip("="), payload.decode().strip("="))
key_decoded = base64_decode_with_padding(key)
match algorithm:
case Algorithm.HS256:
signature = hmac.new(key_decoded, unsigned_token.encode(), hashlib.sha256).digest()
sig = base64.urlsafe_b64encode(signature)
case Algorithm.ES256:
# Retrieve the private key using a P256 elliptic curve
private_key = ec.derive_private_key(
int(binascii.hexlify(key_decoded), 16), ec.SECP256R1(), default_backend()
)
signature = private_key.sign(unsigned_token.encode(), ec.ECDSA(hashes.SHA256()))
(r, s) = utils.decode_dss_signature(signature)
sig = base64.urlsafe_b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big"))
case _:
raise ValueError(f"Unsupported algorithm: {algorithm}")
return "{}.{}".format(unsigned_token, sig.decode().strip("="))
def sign(claims: dict, key: str, ttl: int, algorithm: Algorithm) -> str:
"""
A JSON Web Token is a signed pair of JSON objects, turned into base64 strings.
RFC: https://www.rfc-editor.org/rfc/rfc7519
:param claims: the payload of the jwt: https://www.rfc-editor.org/rfc/rfc7519#section-4.1
:param key: base64 string
:param ttl: the time to live of the token in seconds ('exp' claim)
:param algorithm: to use to sign the token
:return: JSON Web Token
"""
non_padded_key = key.strip("=")
assert ttl
claims["exp"] = int(time.time()) + ttl
return _generate_jwt(claims, non_padded_key, algorithm=algorithm)