diff options
author | Calvin Morrison <calvin@pobox.com> | 2025-09-03 21:15:36 -0400 |
---|---|---|
committer | Calvin Morrison <calvin@pobox.com> | 2025-09-03 21:15:36 -0400 |
commit | 49fa5aa2a127bdf8924d02bf77e5086b39c7a447 (patch) | |
tree | 61d86a7705dacc9fddccc29fa79d075d83ab8059 /server/_build/default/lib/jwt/src |
Diffstat (limited to 'server/_build/default/lib/jwt/src')
-rw-r--r-- | server/_build/default/lib/jwt/src/jwk.erl | 69 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwt.app.src | 8 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwt.erl | 340 | ||||
-rw-r--r-- | server/_build/default/lib/jwt/src/jwt_ecdsa.erl | 76 |
4 files changed, 493 insertions, 0 deletions
diff --git a/server/_build/default/lib/jwt/src/jwk.erl b/server/_build/default/lib/jwt/src/jwk.erl new file mode 100644 index 0000000..9226aae --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwk.erl @@ -0,0 +1,69 @@ +%% @doc RFC 7517: JSON Web Key (JWK) + +-module(jwk). +-include_lib("public_key/include/OTP-PUB-KEY.hrl"). + +-export([encode/2, decode/2]). + +-type id() :: binary(). +-type public_key() :: #'RSAPublicKey'{} | pem(). +-type pem() :: binary(). +-type json() :: binary(). + +-spec encode(id(), public_key()) -> {ok, json()} | {error, _}. +%% @doc encode Erlang/OTP Key to JWK +encode(Id, #'RSAPublicKey'{modulus = N, publicExponent = E}) -> + {ok, jsx:encode( + #{ + keys => + [ + #{ + kid => Id, + kty => <<"RSA">>, + n => encode_int(N), + e => encode_int(E) + } + ] + } + )}; +encode(Id, PEM) when is_binary(PEM) -> + [RSAEntry] = public_key:pem_decode(PEM), + encode(Id, public_key:pem_entry_decode(RSAEntry)); +encode(_, _) -> + {error, not_supported}. + +-spec decode(id(), json()) -> {ok, public_key()} | {error, _}. +%% @doc decode JWK to Erlang/OTP Key +decode(Id, #{<<"keys">> := JWTs}) -> + decode( + lists:dropwhile( + fun(X) -> + maps:get(<<"kid">>, X, undefined) /= Id + end, + JWTs + ) + ); +decode(Id, Json) when is_binary(Json) -> + decode(Id, jsx:decode(Json, [return_maps])). + +%% @private +decode([#{<<"kty">> := <<"RSA">>, <<"n">> := N, <<"e">> := E} | _]) -> + {ok, + #'RSAPublicKey'{ + modulus = decode_int(N), + publicExponent = decode_int(E) + } + }; +decode([]) -> + {error, not_found}; +decode(_) -> + {error, not_supported}. + + +%% @private +encode_int(X) -> + base64url:encode(binary:encode_unsigned(X)). + +%% @private +decode_int(X) -> + binary:decode_unsigned(base64url:decode(X)). diff --git a/server/_build/default/lib/jwt/src/jwt.app.src b/server/_build/default/lib/jwt/src/jwt.app.src new file mode 100644 index 0000000..b39bcb6 --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwt.app.src @@ -0,0 +1,8 @@ +{application,jwt, + [{description,"Erlang JWT library"}, + {vsn,"0.1.11"}, + {registered,[]}, + {applications,[kernel,stdlib,crypto,public_key,jsx,base64url]}, + {env,[]}, + {licenses,["MIT"]}, + {links,[{"GitHub","https://github.com/artemeff/jwt"}]}]}. diff --git a/server/_build/default/lib/jwt/src/jwt.erl b/server/_build/default/lib/jwt/src/jwt.erl new file mode 100644 index 0000000..b17b679 --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwt.erl @@ -0,0 +1,340 @@ +%% @doc JWT Library for Erlang. +%% +%% Written by Peter Hizalev at Kato (http://kato.im) +%% +%% Rewritten by Yuri Artemev (http://artemff.com) +%% +%% @end +-module(jwt). + +-export([decode/2, decode/3]). +-export([encode/3, encode/4]). + +-define(HOUR, 3600). +-define(DAY, (?HOUR * 24)). + +%% Handle version compatibility for crypto +-ifdef(OTP_RELEASE). + -if(?OTP_RELEASE >= 23). + -define(HMAC(Type, Key, Data), crypto:mac(hmac, Type, Key, Data)). + -else. + -define(HMAC(Type, Key, Data), crypto:hmac(Type, Key, Data)). + -endif. +-else. + -define(HMAC(Type, Key, Data), crypto:hmac(Type, Key, Data)). +-endif. + +-type expiration() :: {hourly, non_neg_integer()} | {daily, non_neg_integer()} | non_neg_integer(). +-type context() :: map(). + +%% +%% API +%% +-spec encode( + Alg :: binary(), + ClaimsSet :: map() | list(), + Key :: binary() | public_key:private_key() +) -> {ok, Token :: binary()} | {error, any()}. +%% @doc Creates a token from given data and signs it with a given secret +%% +%% Parameters are +%% <ul> +%% <li> +%% `Alg' is a binary one of +%% +%% [HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512] +%% +%% But only [HS256, HS384, HS512, RS256] are supported +%% </li> +%% <li>`ClaimsSet' the payload of the token. Can be both map and proplist</li> +%% <li>`Key' binary in case of hmac encryption and private key if rsa</li> +%% </ul> +%% +%% @end +encode(Alg, ClaimsSet, Key) -> + Claims = base64url:encode(jsx:encode(ClaimsSet)), + Header = base64url:encode(jsx:encode(jwt_header(Alg))), + Payload = <<Header/binary, ".", Claims/binary>>, + case jwt_sign(Alg, Payload, Key) of + undefined -> {error, algorithm_not_supported}; + Signature -> {ok, <<Payload/binary, ".", Signature/binary>>} + end. + +-spec encode( + Alg :: binary(), + ClaimsSet :: map() | list(), + Expiration :: expiration(), + Key :: binary() | public_key:private_key() +) -> {ok, Token :: binary()} | {error, any()}. +%% @doc Creates a token from given data and signs it with a given secret +%% +%% and also adds `exp' claim to payload +%% +%% `Expiration' can be one of the tuples: +%% `{hourly, SecondsAfterBeginningOfCurrentHour}', +%% `{daily, SecondsAfterBeginningOfCurrentDay}' +%% or can be just an integer representing the amount of seconds +%% the token will live +%% +%% @end +encode(Alg, ClaimsSet, Expiration, Key) -> + Claims = jwt_add_exp(ClaimsSet, Expiration), + encode(Alg, Claims, Key). + +-spec decode( + Token :: binary(), + Key :: binary() | public_key:public_key() | public_key:private_key() +) -> {ok, Claims :: map()} | {error, any()}. +%% @doc Decodes a token, checks the signature and returns the content of the token +%% +%% <ul> +%% <li>`Token' is a JWT itself</li> +%% <li>`Key' is a secret phrase or public/private key depend on encryption algorithm</li> +%% </ul> +%% +%% @end +decode(Token, Key) -> + decode(Token, Key, #{}). + +% When there are multiple issuers and keys are on a per issuer bases +% then apply those keys instead +-spec decode( + Token :: binary(), + DefaultKey :: binary() | public_key:public_key() | public_key:private_key(), + IssuerKeyMapping :: map() +) -> {ok, Claims :: map()} | {error, any()}. +%% @doc Decode with an issuer key mapping +%% +%% Receives the issuer key mapping as the last parameter +%% +%% @end +decode(Token, DefaultKey, IssuerKeyMapping) -> + result(reduce_while(fun(F, Acc) -> apply(F, [Acc]) end, #{token => Token}, [ + fun split_token/1, + fun decode_jwt/1, + fun (Context) -> + get_key(Context, DefaultKey, IssuerKeyMapping) + end, + fun check_signature/1, + fun check_expired/1 + ])). + +result(#{claims_json := ClaimsJSON}) -> + {ok, ClaimsJSON}; +result({error, _} = Error) -> + Error. + +reduce_while(_Fun, Acc, []) -> + Acc; +reduce_while(Fun, Acc, [Item|Rest]) -> + case Fun(Item, Acc) of + {cont, NewAcc} -> + reduce_while(Fun, NewAcc, Rest); + {halt, Result} -> + Result + end. + +-spec split_token(Context :: context()) -> + {cont, context()} | {halt, {error, invalid_token}}. +%% @private +split_token(#{token := Token} = Context) -> + case binary:split(Token, <<".">>, [global]) of + [Header, Claims, Signature] -> + {cont, maps:merge(Context, #{ + header => Header, + claims => Claims, + signature => Signature + })}; + _ -> + {halt, {error, invalid_token}} + end. + +-spec decode_jwt(context()) -> {cont, context()} | {halt, {error, invalid_token}}. +%% @private +decode_jwt(#{header := Header, claims := Claims} = Context) -> + try + [HeaderJSON, ClaimsJSON] = + Decoded = [jsx_decode_safe(base64url:decode(X)) || X <- [Header, Claims]], + case lists:any(fun(E) -> E =:= invalid end, Decoded) of + false -> + {cont, maps:merge(Context, #{ + header_json => HeaderJSON, + claims_json => ClaimsJSON + })}; + true -> + {halt, {error, invalid_token}} + end + catch _:_ -> + {halt, {error, invalid_token}} + end. + +%% @private +get_key(#{claims_json := Claims} = Context, DefaultKey, IssuerKeyMapping) -> + Issuer = maps:get(<<"iss">>, Claims, undefined), + Key = maps:get(Issuer, IssuerKeyMapping, DefaultKey), + {cont, maps:merge(Context, #{key => Key})}. + +%% @private +check_signature(#{ + key := Key, + header := Header, + claims := Claims, + signature := Signature, + header_json := #{<<"alg">> := Alg} +} = Context) -> + case jwt_check_sig(Alg, Header, Claims, Signature, Key) of + true -> + {cont, Context}; + false -> + {halt, {error, invalid_signature}} + end. + +%% @private +check_expired(#{claims_json := ClaimsJSON} = Context) -> + case jwt_is_expired(ClaimsJSON) of + true -> + {halt, {error, expired}}; + false -> + {cont, Context} + end. + +%% +%% Decoding helpers +%% +-spec jsx_decode_safe(binary()) -> map() | invalid. +%% @private +jsx_decode_safe(Bin) -> + try + jsx:decode(Bin, [return_maps]) + catch _ -> + invalid + end. + +-spec jwt_is_expired(map()) -> boolean(). +%% @private +jwt_is_expired(#{<<"exp">> := Exp} = _ClaimsJSON) -> + case (Exp - epoch()) of + DeltaSecs when DeltaSecs > 0 -> false; + _ -> true + end; +jwt_is_expired(_) -> + false. + +-spec jwt_check_sig( + Alg :: binary(), + Header :: binary(), + Claims :: binary(), + Signature :: binary(), + Key :: binary() | public_key:public_key() | public_key:private_key() +) -> boolean(). +%% @private +jwt_check_sig(Alg, Header, Claims, Signature, Key) -> + jwt_check_sig(algorithm_to_crypto(Alg), <<Header/binary, ".", Claims/binary>>, Signature, Key). + +-spec jwt_check_sig( + {atom(), atom()}, + Payload :: binary(), + Signature :: binary(), + Key :: binary() | public_key:public_key() | public_key:private_key() +) -> boolean(). +%% @private +jwt_check_sig({hmac, _} = Alg, Payload, Signature, Key) -> + jwt_sign_with_crypto(Alg, Payload, Key) =:= Signature; + +jwt_check_sig({Algo, Crypto}, Payload, Signature, Pem) + when (Algo =:= rsa orelse Algo =:= ecdsa) andalso is_binary(Pem) -> + jwt_check_sig({Algo, Crypto}, Payload, Signature, pem_to_key(Pem)); + +jwt_check_sig({rsa, Crypto}, Payload, Signature, Key) -> + public_key:verify(Payload, Crypto, base64url:decode(Signature), Key); + +jwt_check_sig({ecdsa, Crypto}, Payload, Signature, Key) -> + public_key:verify(Payload, Crypto, jwt_ecdsa:signature(Signature), Key); + +jwt_check_sig(_, _, _, _) -> + false. + +%% +%% Encoding helpers +%% +-spec jwt_add_exp(ClaimsSet :: map() | list(), Expiration :: expiration()) -> map() | list(). +%% @private +jwt_add_exp(ClaimsSet, Expiration) -> + Exp = expiration_to_epoch(Expiration), + append_claim(ClaimsSet, <<"exp">>, Exp). + +-spec jwt_header(Alg :: binary()) -> list(). +jwt_header(Alg) -> + [ {<<"alg">>, Alg} + , {<<"typ">>, <<"JWT">>} + ]. + +%% +%% Helpers +%% +-spec jwt_sign( + Alg :: binary(), + Payload :: binary(), + Key :: binary() | public_key:private_key() +) -> binary() | undefined. +%% @private +jwt_sign(Alg, Payload, Key) -> + jwt_sign_with_crypto(algorithm_to_crypto(Alg), Payload, Key). + +jwt_sign_with_crypto({hmac, Crypto}, Payload, Key) -> + base64url:encode(?HMAC(Crypto, Key, Payload)); + +jwt_sign_with_crypto({Algo, Crypto}, Payload, Pem) + when (Algo =:= rsa orelse Algo =:= ecdsa) andalso is_binary(Pem) -> + jwt_sign_with_crypto({Algo, Crypto}, Payload, pem_to_key(Pem)); + +jwt_sign_with_crypto({rsa, Crypto}, Payload, Key) -> + base64url:encode(public_key:sign(Payload, Crypto, Key)); + +jwt_sign_with_crypto({ecdsa, Crypto}, Payload, Key) -> + base64url:encode(jwt_ecdsa:signature(Payload, Crypto, Key)); + +jwt_sign_with_crypto(_, _Payload, _Key) -> + undefined. + +-spec algorithm_to_crypto(binary()) -> {atom(), atom()} | undefined. +%% @private +algorithm_to_crypto(<<"HS256">>) -> {hmac, sha256}; +algorithm_to_crypto(<<"HS384">>) -> {hmac, sha384}; +algorithm_to_crypto(<<"HS512">>) -> {hmac, sha512}; +algorithm_to_crypto(<<"RS256">>) -> {rsa, sha256}; +algorithm_to_crypto(<<"RS384">>) -> {rsa, sha384}; +algorithm_to_crypto(<<"RS512">>) -> {rsa, sha512}; +algorithm_to_crypto(<<"ES256">>) -> {ecdsa, sha256}; +algorithm_to_crypto(_) -> undefined. + +-spec epoch() -> non_neg_integer(). +%% @private +epoch() -> erlang:system_time(seconds). + +-spec expiration_to_epoch(Expiration :: expiration()) -> neg_integer(). +%% @private +expiration_to_epoch(Expiration) -> + expiration_to_epoch(Expiration, epoch()). + +expiration_to_epoch(Expiration, Ts) -> + case Expiration of + {hourly, Expiration0} -> (Ts - (Ts rem ?HOUR)) + Expiration0; + {daily, Expiration0} -> (Ts - (Ts rem ?DAY)) + Expiration0; + _ -> epoch() + Expiration + end. + +-spec append_claim(ClaimsSet :: map() | list(), binary(), any()) -> map() | list(). +%% @private +append_claim(ClaimsSet, Key, Val) when is_map(ClaimsSet) -> + ClaimsSet#{ Key => Val }; +append_claim(ClaimsSet, Key, Val) -> [{ Key, Val } | ClaimsSet]. + +pem_to_key(Pem) -> + Decoded = case public_key:pem_decode(Pem) of + [_, Key] -> + Key; + [Key] -> + Key + end, + public_key:pem_entry_decode(Decoded). diff --git a/server/_build/default/lib/jwt/src/jwt_ecdsa.erl b/server/_build/default/lib/jwt/src/jwt_ecdsa.erl new file mode 100644 index 0000000..7596e90 --- /dev/null +++ b/server/_build/default/lib/jwt/src/jwt_ecdsa.erl @@ -0,0 +1,76 @@ +%% @doc Eliptic curve digital signature algorithm +%% +%% Helper functions for encoding/decoding ECDSA signature +%% +%% @end +-module(jwt_ecdsa). + +-include_lib("jwt_ecdsa.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-export([ + signature/1, + signature/3 +]). + +%% @doc Signature for JWT verification +%% +%% Transcode the ECDSA Base64-encoded signature into ASN.1/DER format +%% +%% @end +signature(Base64Sig) -> + Signature = base64url:decode(Base64Sig), + SignatureLen = byte_size(Signature), + {RBin, SBin} = split_binary(Signature, (SignatureLen div 2)), + R = crypto:bytes_to_integer(RBin), + S = crypto:bytes_to_integer(SBin), + public_key:der_encode('ECDSA-Sig-Value', #'ECDSA-Sig-Value'{ r = R, s = S }). + +%% @doc Signature to sign JWT +%% +%% Transcodes the JCA ASN.1/DER-encoded signature into the concatenated R + S format +%% a.k.a <em>raw</em> format +%% +%% @end +signature(Payload, Crypto, Key) -> + Der = public_key:sign(Payload, Crypto, Key), + raw(Der, Key). + +raw(Der, #'ECPrivateKey'{parameters = {namedCurve, NamedCurve}}) -> + #'ECDSA-Sig-Value'{ r = R, s = S } = public_key:der_decode('ECDSA-Sig-Value', Der), + CurveName = pubkey_cert_records:namedCurves(NamedCurve), + GroupDegree = group_degree(CurveName), + Size = (GroupDegree + 7) div 8, + RBin = int_to_bin(R), + SBin = int_to_bin(S), + RPad = pad(RBin, Size), + SPad = pad(SBin, Size), + <<RPad/binary, SPad/binary>>. + +%% @private +int_to_bin(X) when X < 0 -> + int_to_bin_neg(X, []); +int_to_bin(X) -> + int_to_bin_pos(X, []). + +%% @private +int_to_bin_pos(0, Ds = [_|_]) -> + list_to_binary(Ds); +int_to_bin_pos(X, Ds) -> + int_to_bin_pos(X bsr 8, [(X band 255)|Ds]). + +%% @private +int_to_bin_neg(-1, Ds = [MSB|_]) when MSB >= 16#80 -> + list_to_binary(Ds); +int_to_bin_neg(X, Ds) -> + int_to_bin_neg(X bsr 8, [(X band 255)|Ds]). + +%% @private +pad(Bin, Size) when byte_size(Bin) =:= Size -> + Bin; +pad(Bin, Size) when byte_size(Bin) < Size -> + pad(<<0, Bin/binary>>, Size). + +%% See the OpenSSL documentation for EC_GROUP_get_degree() +group_degree(CurveName) -> + maps:get(CurveName, ?EC_GROUP_DEGREE). |