From 1ff8420499395a2060cfb11cc55df515cac512d4 Mon Sep 17 00:00:00 2001 From: zachmann <gabriel.zachmann@kit.edu> Date: Tue, 2 Aug 2022 17:37:08 +0200 Subject: [PATCH] WIP rework tokeninfo webinterface --- internal/jws/jws.go | 3 + .../server/web/partials/create-at.mustache | 3 +- .../server/web/partials/create-mt.mustache | 2 +- .../server/web/partials/list-mts.mustache | 2 +- internal/server/web/partials/scripts.mustache | 4 +- .../server/web/partials/tokeninfo.mustache | 93 ++++++++--- internal/server/web/sites/home.mustache | 11 +- .../server/web/sites/settings-ssh.mustache | 2 +- internal/server/web/static/js/discovery.js | 3 +- internal/server/web/static/js/home.js | 12 +- .../server/web/static/js/lib/jose/index.js | 32 ++++ .../static/js/lib/jose/jws/compact/sign.js | 18 +++ .../static/js/lib/jose/jws/compact/verify.js | 22 +++ .../static/js/lib/jose/jws/flattened/sign.js | 82 ++++++++++ .../js/lib/jose/jws/flattened/verify.js | 105 +++++++++++++ .../static/js/lib/jose/jws/general/sign.js | 68 ++++++++ .../static/js/lib/jose/jws/general/verify.js | 25 +++ .../web/static/js/lib/jose/jwt/decrypt.js | 24 +++ .../web/static/js/lib/jose/jwt/encrypt.js | 69 ++++++++ .../web/static/js/lib/jose/jwt/produce.js | 55 +++++++ .../server/web/static/js/lib/jose/jwt/sign.js | 22 +++ .../web/static/js/lib/jose/jwt/unsecured.js | 33 ++++ .../web/static/js/lib/jose/jwt/verify.js | 17 ++ .../web/static/js/lib/jose/key/export.js | 12 ++ .../js/lib/jose/key/generate_key_pair.js | 5 + .../static/js/lib/jose/key/generate_secret.js | 5 + .../web/static/js/lib/jose/key/import.js | 120 ++++++++++++++ .../web/static/js/lib/jose/lib/aesgcmkw.js | 15 ++ .../static/js/lib/jose/lib/buffer_utils.js | 52 +++++++ .../server/web/static/js/lib/jose/lib/cek.js | 21 +++ .../static/js/lib/jose/lib/check_iv_length.js | 9 ++ .../static/js/lib/jose/lib/check_key_type.js | 46 ++++++ .../web/static/js/lib/jose/lib/check_p2s.js | 7 + .../web/static/js/lib/jose/lib/crypto_key.js | 146 +++++++++++++++++ .../js/lib/jose/lib/decrypt_key_management.js | 96 ++++++++++++ .../js/lib/jose/lib/encrypt_key_management.js | 88 +++++++++++ .../web/static/js/lib/jose/lib/epoch.js | 1 + .../web/static/js/lib/jose/lib/format_pem.js | 4 + .../js/lib/jose/lib/invalid_key_input.js | 25 +++ .../web/static/js/lib/jose/lib/is_disjoint.js | 22 +++ .../web/static/js/lib/jose/lib/is_object.js | 16 ++ .../server/web/static/js/lib/jose/lib/iv.js | 21 +++ .../static/js/lib/jose/lib/jwt_claims_set.js | 92 +++++++++++ .../server/web/static/js/lib/jose/lib/secs.js | 44 ++++++ .../js/lib/jose/lib/validate_algorithms.js | 11 ++ .../static/js/lib/jose/lib/validate_crit.js | 35 +++++ .../web/static/js/lib/jose/runtime/aeskw.js | 33 ++++ .../web/static/js/lib/jose/runtime/asn1.js | 119 ++++++++++++++ .../static/js/lib/jose/runtime/base64url.js | 38 +++++ .../web/static/js/lib/jose/runtime/bogus.js | 6 + .../js/lib/jose/runtime/check_cek_length.js | 8 + .../js/lib/jose/runtime/check_key_length.js | 8 + .../web/static/js/lib/jose/runtime/decrypt.js | 86 ++++++++++ .../web/static/js/lib/jose/runtime/digest.js | 7 + .../web/static/js/lib/jose/runtime/ecdhes.js | 34 ++++ .../web/static/js/lib/jose/runtime/encrypt.js | 69 ++++++++ .../web/static/js/lib/jose/runtime/env.js | 3 + .../static/js/lib/jose/runtime/fetch_jwks.js | 35 +++++ .../static/js/lib/jose/runtime/generate.js | 127 +++++++++++++++ .../lib/jose/runtime/get_sign_verify_key.js | 18 +++ .../static/js/lib/jose/runtime/is_key_like.js | 6 + .../static/js/lib/jose/runtime/jwk_to_key.js | 136 ++++++++++++++++ .../static/js/lib/jose/runtime/key_to_jwk.js | 22 +++ .../web/static/js/lib/jose/runtime/pbes2kw.js | 52 +++++++ .../web/static/js/lib/jose/runtime/random.js | 3 + .../web/static/js/lib/jose/runtime/rsaes.js | 38 +++++ .../web/static/js/lib/jose/runtime/sign.js | 12 ++ .../static/js/lib/jose/runtime/subtle_dsa.js | 29 ++++ .../js/lib/jose/runtime/subtle_rsaes.js | 13 ++ .../js/lib/jose/runtime/timing_safe_equal.js | 19 +++ .../web/static/js/lib/jose/runtime/verify.js | 17 ++ .../static/js/lib/jose/runtime/webcrypto.js | 2 + .../web/static/js/lib/jose/runtime/zlib.js | 8 + .../web/static/js/lib/jose/util/base64url.js | 4 + .../web/static/js/lib/jose/util/decode_jwt.js | 33 ++++ .../lib/jose/util/decode_protected_header.js | 35 +++++ .../web/static/js/lib/jose/util/errors.js | 147 ++++++++++++++++++ .../server/web/static/js/logged-in-utils.js | 9 ++ internal/server/web/static/js/ssh.js | 10 -- .../server/web/static/js/tokeninfo-status.js | 123 +++++++++++++++ internal/server/web/static/js/tokeninfo.js | 25 +-- internal/server/web/static/js/utils.js | 4 + 82 files changed, 2872 insertions(+), 66 deletions(-) create mode 100644 internal/server/web/static/js/lib/jose/index.js create mode 100644 internal/server/web/static/js/lib/jose/jws/compact/sign.js create mode 100644 internal/server/web/static/js/lib/jose/jws/compact/verify.js create mode 100644 internal/server/web/static/js/lib/jose/jws/flattened/sign.js create mode 100644 internal/server/web/static/js/lib/jose/jws/flattened/verify.js create mode 100644 internal/server/web/static/js/lib/jose/jws/general/sign.js create mode 100644 internal/server/web/static/js/lib/jose/jws/general/verify.js create mode 100644 internal/server/web/static/js/lib/jose/jwt/decrypt.js create mode 100644 internal/server/web/static/js/lib/jose/jwt/encrypt.js create mode 100644 internal/server/web/static/js/lib/jose/jwt/produce.js create mode 100644 internal/server/web/static/js/lib/jose/jwt/sign.js create mode 100644 internal/server/web/static/js/lib/jose/jwt/unsecured.js create mode 100644 internal/server/web/static/js/lib/jose/jwt/verify.js create mode 100644 internal/server/web/static/js/lib/jose/key/export.js create mode 100644 internal/server/web/static/js/lib/jose/key/generate_key_pair.js create mode 100644 internal/server/web/static/js/lib/jose/key/generate_secret.js create mode 100644 internal/server/web/static/js/lib/jose/key/import.js create mode 100644 internal/server/web/static/js/lib/jose/lib/aesgcmkw.js create mode 100644 internal/server/web/static/js/lib/jose/lib/buffer_utils.js create mode 100644 internal/server/web/static/js/lib/jose/lib/cek.js create mode 100644 internal/server/web/static/js/lib/jose/lib/check_iv_length.js create mode 100644 internal/server/web/static/js/lib/jose/lib/check_key_type.js create mode 100644 internal/server/web/static/js/lib/jose/lib/check_p2s.js create mode 100644 internal/server/web/static/js/lib/jose/lib/crypto_key.js create mode 100644 internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js create mode 100644 internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js create mode 100644 internal/server/web/static/js/lib/jose/lib/epoch.js create mode 100644 internal/server/web/static/js/lib/jose/lib/format_pem.js create mode 100644 internal/server/web/static/js/lib/jose/lib/invalid_key_input.js create mode 100644 internal/server/web/static/js/lib/jose/lib/is_disjoint.js create mode 100644 internal/server/web/static/js/lib/jose/lib/is_object.js create mode 100644 internal/server/web/static/js/lib/jose/lib/iv.js create mode 100644 internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js create mode 100644 internal/server/web/static/js/lib/jose/lib/secs.js create mode 100644 internal/server/web/static/js/lib/jose/lib/validate_algorithms.js create mode 100644 internal/server/web/static/js/lib/jose/lib/validate_crit.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/aeskw.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/asn1.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/base64url.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/bogus.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/check_cek_length.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/check_key_length.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/decrypt.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/digest.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/ecdhes.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/encrypt.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/env.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/generate.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/is_key_like.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/pbes2kw.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/random.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/rsaes.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/sign.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/verify.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/webcrypto.js create mode 100644 internal/server/web/static/js/lib/jose/runtime/zlib.js create mode 100644 internal/server/web/static/js/lib/jose/util/base64url.js create mode 100644 internal/server/web/static/js/lib/jose/util/decode_jwt.js create mode 100644 internal/server/web/static/js/lib/jose/util/decode_protected_header.js create mode 100644 internal/server/web/static/js/lib/jose/util/errors.js create mode 100644 internal/server/web/static/js/logged-in-utils.js create mode 100644 internal/server/web/static/js/tokeninfo-status.js diff --git a/internal/jws/jws.go b/internal/jws/jws.go index 89a9d47c..97cba344 100644 --- a/internal/jws/jws.go +++ b/internal/jws/jws.go @@ -139,5 +139,8 @@ func LoadKey() { if err = key.Set(jwk.KeyUsageKey, string(jwk.ForSignature)); err != nil { panic(err) } + if err = key.Set(jwk.AlgorithmKey, config.Get().Signing.Alg); err != nil { + panic(err) + } jwks.Add(key) } diff --git a/internal/server/web/partials/create-at.mustache b/internal/server/web/partials/create-at.mustache index 0a16b54b..be84c017 100644 --- a/internal/server/web/partials/create-at.mustache +++ b/internal/server/web/partials/create-at.mustache @@ -34,7 +34,8 @@ <div id="at-pending-spinner" class="spinner-border text-primary d-none" role="status"> <span class="sr-only">Loading...</span> </div> - <button class="btn btn-copy" id="at-result-copy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#at-result-msg"> + <button class="btn btn-copy copier" id="at-result-copy" data-toggle="tooltip" data-placement="bottom" + title="Copy to clipboard" data-clipboard-target="#at-result-msg"> <i class="far fa-copy"></i> </button> <pre class="card-text" id="at-result-msg"></pre> diff --git a/internal/server/web/partials/create-mt.mustache b/internal/server/web/partials/create-mt.mustache index 87393c73..6a4032c3 100644 --- a/internal/server/web/partials/create-mt.mustache +++ b/internal/server/web/partials/create-mt.mustache @@ -67,7 +67,7 @@ <div id="mt-pending-spinner" class="spinner-border text-primary d-none" role="status"> <span class="sr-only">Loading...</span> </div> - <button class="btn btn-copy d-none" id="mt-result-copy" data-toggle="tooltip" data-placement="bottom" + <button class="btn btn-copy copier d-none" id="mt-result-copy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#mt-result-msg"> <i class="far fa-copy"></i> </button> diff --git a/internal/server/web/partials/list-mts.mustache b/internal/server/web/partials/list-mts.mustache index dbc7cbc6..231827f6 100644 --- a/internal/server/web/partials/list-mts.mustache +++ b/internal/server/web/partials/list-mts.mustache @@ -3,7 +3,7 @@ title="Reload data"> <i class="fas fa-sync"></i> </button> -<button class="btn btn-copy d-none" id="list-copy" data-toggle="tooltip" data-placement="bottom" +<button class="btn btn-copy copier d-none" id="list-copy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#list-msg"> <i class="far fa-copy"></i> </button> diff --git a/internal/server/web/partials/scripts.mustache b/internal/server/web/partials/scripts.mustache index df58c3c0..595ca0d6 100644 --- a/internal/server/web/partials/scripts.mustache +++ b/internal/server/web/partials/scripts.mustache @@ -7,16 +7,18 @@ <script src="/static/js/login.js"></script> {{/logged-in}} {{#logged-in}} + <script src="/static/js/lib/clipboard.min.js"></script> + <script src="/static/js/logged-in-utils.js"></script> <script src="/static/js/lib/bootstrap4-toggle.min.js"></script> <script src="/static/js/logout.js"></script> {{/logged-in}} {{#home}} - <script src="/static/js/lib/clipboard.min.js"></script> <script src="/static/js/lib/user-agent.min.js"></script> <script src="/static/js/mt-helper.js"></script> <script src="/static/js/home.js"></script> <script src="/static/js/create-at.js"></script> <script src="/static/js/tokeninfo.js"></script> + <script type="module" src="/static/js/tokeninfo-status.js"></script> <script src="/static/js/lib/behave.min.js"></script> <script src="/static/js/restrictions.js"></script> <script src="/static/js/rotation.js"></script> diff --git a/internal/server/web/partials/tokeninfo.mustache b/internal/server/web/partials/tokeninfo.mustache index 4928792b..7fa0fdc8 100644 --- a/internal/server/web/partials/tokeninfo.mustache +++ b/internal/server/web/partials/tokeninfo.mustache @@ -1,58 +1,109 @@ -<div class="card border-primary"> - <div class="card-header"> +<div id="tokeninfo-head"> + <div id="tokeninfo-token-status" class="mb-2"> + <h4> + <span class="badge badge-pill bg-my_green_dark" id="tokeninfo-token-name" data-toggle="tooltip" + data-placement="top" title="Name of this mytoken"></span> + + <span class="ml-2 badge badge-pill bg-my_blue_dark tokeninfo-token-type d-none" + id="tokeninfo-token-type-short">Short Token</span> + <span class="ml-2 badge badge-pill badge-success tokeninfo-token-type d-none" + id="tokeninfo-token-type-JWT-valid" + data-toggle="tooltip" data-placement="top" title="Signature valid">JWT</span> + <span class="ml-2 badge badge-pill badge-danger tokeninfo-token-type d-none" + id="tokeninfo-token-type-JWT-invalid" + data-toggle="tooltip" data-placement="top" title="Signature invalid">JWT</span> + + <span class="ml-2 badge badge-pill badge-success" id="tokeninfo-token-valid" + data-toggle="tooltip" data-placement="top" title="Mytoken is valid and can be used"> + <i class="fas fa-check"></i> + </span> + <span class="ml-2 badge badge-pill badge-danger d-none" id="tokeninfo-token-invalid" + data-toggle="tooltip" data-placement="top" title="Mytoken is invalid and cannot be used"> + <i class="fas fa-times"></i> + </span> + + <span class="ml-2 badge badge-pill badge-light" id="tokeninfo-token-mytoken-iss" + data-toggle="tooltip" data-placement="top" title="Mytoken is issued by this instance"></span> + + <span class="ml-2 badge badge-pill badge-primary" id="tokeninfo-token-oidc-iss" + data-toggle="tooltip" data-placement="top" title="Mytoken is linked to this OpenID Provider"></span> + + <span class="ml-2 badge badge-pill badge-light" id="tokeninfo-token-iat" + data-toggle="tooltip" data-placement="top" title="Mytoken was issued at this time"> + <i class="fas fa-clock mr-1"></i><span id="tokeninfo-token-iat-date"></span></span> + + <span class="ml-2 badge badge-pill badge-warning" id="tokeninfo-token-exp" + data-toggle="tooltip" data-placement="top" title="Mytoken expires at this time"> + <i class="fas fa-stopwatch mr-2"></i><span id="tokeninfo-token-exp-date"></span></span> + </h4> + </div> + + <div class="input-group"> + <input type="text" class="form-control" placeholder="Mytoken" id="tokeninfo-token" autofocus + style="padding-right: 40px;"> + <button class="btn copier text-secondary" id="tokeninfo-token-copy" data-toggle="tooltip" + data-placement="bottom" + title="Copy to clipboard" data-clipboard-target="#tokeninfo-token" + style="position: relative; left: -40px;"> + <i class="far fa-copy"></i> + </button> + <button type="button" class="btn btn-light ml-n4" id="create-tc">Create Transfercode</button> + </div> +</div> + + <div class="card-header mt-3"> <ul class="nav nav-tabs card-header-tabs"> <li class="nav-item"> - <a class="nav-link active" id="session-info-tab" data-toggle="tab" href="#session" role="tab" - aria-controls="session" aria-selected="true">Session Token Info</a> + <a class="nav-link active" id="info-tab" data-toggle="tab" href="#token-info" role="tab" + aria-controls="token-info" aria-selected="true">Info</a> </li> <li class="nav-item"> - <a class="nav-link" id="history-tab" data-toggle="tab" href="#history" role="tab" - aria-controls="history" aria-selected="false">Session Token History</a> + <a class="nav-link" id="history-tab" data-toggle="tab" href="#token-history" role="tab" + aria-controls="token-history" aria-selected="false">History</a> </li> <li class="nav-item"> - <a class="nav-link" id="tree-tab" data-toggle="tab" href="#tree" role="tab" aria-controls="tree" - aria-selected="false">Session Token Subtokens</a> + <a class="nav-link" id="tree-tab" data-toggle="tab" href="#token-tree" role="tab" + aria-controls="token-tree" aria-selected="false">Subtokens</a> </li> </ul> </div> <div class="card-body tab-content"> - <div class="tab-pane show active" id="session" role="tabpanel" aria-labelledby="session-info-tab"> - <h4>Information About the Session's Mytoken</h4> - <button class="btn btn-reload" id="session-reload" data-toggle="tooltip" data-placement="bottom" + <div class="tab-pane show active" id="token-info" role="tabpanel" aria-labelledby="info-tab"> + <h4>Information About a Mytoken</h4> + <button class="btn btn-reload" id="info-reload" data-toggle="tooltip" data-placement="bottom" title="Reload data"> <i class="fas fa-sync"></i> </button> - <button class="btn btn-copy d-none" id="session-copy" data-toggle="tooltip" data-placement="bottom" - title="Copy to clipboard" data-clipboard-target="#session-token-info-msg"> + <button class="btn btn-copy copier d-none" id="info-copy" data-toggle="tooltip" data-placement="bottom" + title="Copy to clipboard" data-clipboard-target="#tokeninfo-token-content"> <i class="far fa-copy"></i> </button> - <pre class="card-text" id="session-token-info-msg"></pre> + <pre class="card-text" id="tokeninfo-token-content"></pre> </div> - <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="history-tab"> - <h4>Event History for Session's Mytoken</h4> + <div class="tab-pane" id="token-history" role="tabpanel" aria-labelledby="history-tab"> + <h4>Event History for this Mytoken</h4> <button class="btn btn-reload" id="history-reload" data-toggle="tooltip" data-placement="bottom" title="Reload data"> <i class="fas fa-sync"></i> </button> - <button class="btn btn-copy d-none" id="history-copy" data-toggle="tooltip" data-placement="bottom" + <button class="btn btn-copy copier d-none" id="history-copy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#history-msg"> <i class="far fa-copy"></i> </button> <p class="card-text" id="history-msg"></p> </div> - <div class="tab-pane" id="tree" role="tabpanel" aria-labelledby="tree-tab"> - <h4>Subtokens for Session's Mytoken</h4> + <div class="tab-pane" id="token-tree" role="tabpanel" aria-labelledby="tree-tab"> + <h4>Subtokens for this Mytoken</h4> <button class="btn btn-reload" id="tree-reload" data-toggle="tooltip" data-placement="bottom" title="Reload data"> <i class="fas fa-sync"></i> </button> - <button class="btn btn-copy d-none" id="tree-copy" data-toggle="tooltip" data-placement="bottom" + <button class="btn btn-copy copier d-none" id="tree-copy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#tree-msg"> <i class="far fa-copy"></i> </button> <p class="card-text" id="tree-msg"></p> </div> </div> -</div> diff --git a/internal/server/web/sites/home.mustache b/internal/server/web/sites/home.mustache index 6aecd812..59858aa4 100644 --- a/internal/server/web/sites/home.mustache +++ b/internal/server/web/sites/home.mustache @@ -12,20 +12,19 @@ </div> + <div class="card"> <div class="card-header"> <ul class="nav nav-tabs card-header-tabs"> <li class="nav-item"> - <a class="nav-link active" id="at-tab" data-toggle="tab" href="#at" role="tab" aria-controls="at" - aria-selected="true">Access Token</a> + <a class="nav-link active" id="at-tab" data-toggle="tab" href="#at" role="tab" aria-controls="at" aria-selected="true">Access Token</a> </li> <li class="nav-item"> <a class="nav-link" id="mt-tab" data-toggle="tab" href="#mt" role="tab" aria-controls="mt" aria-selected="false">Create Mytoken</a> </li> <li class="nav-item"> - <a class="nav-link" id="info-tab" data-toggle="tab" href="#info" role="tab" aria-controls="info" - aria-selected="false">Tokeninfo</a> + <a class="nav-link" id="info-tab" data-toggle="tab" href="#info" role="tab" aria-controls="info" aria-selected="false">Tokeninfo</a> </li> <li class="nav-item"> <a class="nav-link" id="list-mts-tab" data-toggle="tab" href="#list-mts" role="tab" @@ -38,9 +37,9 @@ {{> create-at}} </div> <div class="tab-pane bg-secondary break-out" id="mt" role="tabpanel" aria-labelledby="mt-tab"> - <div class="bg-secondary "> + <div class="bg-secondary "> {{> create-mt}} - </div> + </div> </div> <div class="tab-pane" id="info" role="tabpanel" aria-labelledby="info-tab"> {{> tokeninfo}} diff --git a/internal/server/web/sites/settings-ssh.mustache b/internal/server/web/sites/settings-ssh.mustache index c6877756..fe8e31de 100644 --- a/internal/server/web/sites/settings-ssh.mustache +++ b/internal/server/web/sites/settings-ssh.mustache @@ -134,7 +134,7 @@ </div> <div id="sshHostConfigDiv" class="d-none"> <p>You can save and use the following ssh host entry:</p> - <button class="btn btn-copy" id="sshHostConfigCopy" data-toggle="tooltip" + <button class="btn btn-copy copier" id="sshHostConfigCopy" data-toggle="tooltip" data-placement="bottom" title="Copy to clipboard" data-clipboard-target="#sshHostConfig"> <i class="far fa-copy"></i> diff --git a/internal/server/web/static/js/discovery.js b/internal/server/web/static/js/discovery.js index 86c548a8..a58e6c44 100644 --- a/internal/server/web/static/js/discovery.js +++ b/internal/server/web/static/js/discovery.js @@ -5,7 +5,8 @@ const configElements = [ "usersettings_endpoint", "revocation_endpoint", "tokeninfo_endpoint", - "providers_supported" + "providers_supported", + "jwks_uri" ] function discovery(...next) { diff --git a/internal/server/web/static/js/home.js b/internal/server/web/static/js/home.js index e8d2b822..260b2fb1 100644 --- a/internal/server/web/static/js/home.js +++ b/internal/server/web/static/js/home.js @@ -1,13 +1,3 @@ - -let clipboard = new ClipboardJS('.btn-copy'); -clipboard.on('success', function (e) { - e.clearSelection(); - let el = $(e.trigger); - let originalText = el.attr('data-original-title'); - el.attr('data-original-title', 'Copied!').tooltip('show'); - el.attr('data-original-title', originalText); -}); - $(function (){ chainFunctions( checkIfLoggedIn, @@ -19,7 +9,7 @@ $(function (){ let url = document.location.toString(); if (url.match('#')) { let hash = url.split('#')[1]; - if (['session', 'history', 'tree', 'list'].includes(hash)) { + if (['token-info', 'token-history', 'token-tree'].includes(hash)) { $('.nav-tabs a[href="#info"]').tab('show') ; } $('.nav-tabs a[href="#'+hash+'"]').tab('show') ; diff --git a/internal/server/web/static/js/lib/jose/index.js b/internal/server/web/static/js/lib/jose/index.js new file mode 100644 index 00000000..26d70fbe --- /dev/null +++ b/internal/server/web/static/js/lib/jose/index.js @@ -0,0 +1,32 @@ +// export { compactDecrypt } from './jwe/compact/decrypt.js'; +// export { flattenedDecrypt } from './jwe/flattened/decrypt.js'; +// export { generalDecrypt } from './jwe/general/decrypt.js'; +// export { GeneralEncrypt } from './jwe/general/encrypt.js'; +// export { compactVerify } from './jws/compact/verify.js'; +// export { flattenedVerify } from './jws/flattened/verify.js'; +// export { generalVerify } from './jws/general/verify.js'; +export { jwtVerify } from './jwt/verify.js'; +// export { jwtDecrypt } from './jwt/decrypt.js'; +// export { CompactEncrypt } from './jwe/compact/encrypt.js'; +// export { FlattenedEncrypt } from './jwe/flattened/encrypt.js'; +// export { CompactSign } from './jws/compact/sign.js'; +// export { FlattenedSign } from './jws/flattened/sign.js'; +// export { GeneralSign } from './jws/general/sign.js'; +// export { SignJWT } from './jwt/sign.js'; +// export { EncryptJWT } from './jwt/encrypt.js'; +// export { calculateJwkThumbprint } from './jwk/thumbprint.js'; +// export { EmbeddedJWK } from './jwk/embedded.js'; +// export { createLocalJWKSet } from './jwks/local.js'; +// export { createRemoteJWKSet } from './jwks/remote.js'; +// export { UnsecuredJWT } from './jwt/unsecured.js'; +// export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js'; +export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js'; +export { decodeProtectedHeader } from './util/decode_protected_header.js'; +export { decodeJwt } from './util/decode_jwt.js'; +import * as errors_1 from './util/errors.js'; +// export { generateKeyPair } from './key/generate_key_pair.js'; +// export { generateSecret } from './key/generate_secret.js'; +import * as base64url_1 from './util/base64url.js'; + +export { errors_1 as errors }; +export { base64url_1 as base64url }; diff --git a/internal/server/web/static/js/lib/jose/jws/compact/sign.js b/internal/server/web/static/js/lib/jose/jws/compact/sign.js new file mode 100644 index 00000000..1f159015 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jws/compact/sign.js @@ -0,0 +1,18 @@ +import {FlattenedSign} from '../flattened/sign.js'; + +export class CompactSign { + constructor(payload) { + this._flattened = new FlattenedSign(payload); + } + setProtectedHeader(protectedHeader) { + this._flattened.setProtectedHeader(protectedHeader); + return this; + } + async sign(key, options) { + const jws = await this._flattened.sign(key, options); + if (jws.payload === undefined) { + throw new TypeError('use the flattened module for creating JWS with b64: false'); + } + return `${jws.protected}.${jws.payload}.${jws.signature}`; + } +} diff --git a/internal/server/web/static/js/lib/jose/jws/compact/verify.js b/internal/server/web/static/js/lib/jose/jws/compact/verify.js new file mode 100644 index 00000000..a8d000b7 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jws/compact/verify.js @@ -0,0 +1,22 @@ +import {flattenedVerify} from '../flattened/verify.js'; +import {JWSInvalid} from '../../util/errors.js'; +import {decoder} from '../../lib/buffer_utils.js'; + +export async function compactVerify(jws, key, options) { + if (jws instanceof Uint8Array) { + jws = decoder.decode(jws); + } + if (typeof jws !== 'string') { + throw new JWSInvalid('Compact JWS must be a string or Uint8Array'); + } + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.'); + if (length !== 3) { + throw new JWSInvalid('Invalid Compact JWS'); + } + const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options); + const result = { payload: verified.payload, protectedHeader: verified.protectedHeader }; + if (typeof key === 'function') { + return { ...result, key: verified.key }; + } + return result; +} diff --git a/internal/server/web/static/js/lib/jose/jws/flattened/sign.js b/internal/server/web/static/js/lib/jose/jws/flattened/sign.js new file mode 100644 index 00000000..d239dc75 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jws/flattened/sign.js @@ -0,0 +1,82 @@ +import {encode as base64url} from '../../runtime/base64url.js'; +import sign from '../../runtime/sign.js'; +import isDisjoint from '../../lib/is_disjoint.js'; +import {JWSInvalid} from '../../util/errors.js'; +import {concat, decoder, encoder} from '../../lib/buffer_utils.js'; +import checkKeyType from '../../lib/check_key_type.js'; +import validateCrit from '../../lib/validate_crit.js'; + +export class FlattenedSign { + constructor(payload) { + if (!(payload instanceof Uint8Array)) { + throw new TypeError('payload must be an instance of Uint8Array'); + } + this._payload = payload; + } + setProtectedHeader(protectedHeader) { + if (this._protectedHeader) { + throw new TypeError('setProtectedHeader can only be called once'); + } + this._protectedHeader = protectedHeader; + return this; + } + setUnprotectedHeader(unprotectedHeader) { + if (this._unprotectedHeader) { + throw new TypeError('setUnprotectedHeader can only be called once'); + } + this._unprotectedHeader = unprotectedHeader; + return this; + } + async sign(key, options) { + if (!this._protectedHeader && !this._unprotectedHeader) { + throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()'); + } + if (!isDisjoint(this._protectedHeader, this._unprotectedHeader)) { + throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint'); + } + const joseHeader = { + ...this._protectedHeader, + ...this._unprotectedHeader, + }; + const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options === null || options === void 0 ? void 0 : options.crit, this._protectedHeader, joseHeader); + let b64 = true; + if (extensions.has('b64')) { + b64 = this._protectedHeader.b64; + if (typeof b64 !== 'boolean') { + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + } + } + const { alg } = joseHeader; + if (typeof alg !== 'string' || !alg) { + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + } + checkKeyType(alg, key, 'sign'); + let payload = this._payload; + if (b64) { + payload = encoder.encode(base64url(payload)); + } + let protectedHeader; + if (this._protectedHeader) { + protectedHeader = encoder.encode(base64url(JSON.stringify(this._protectedHeader))); + } + else { + protectedHeader = encoder.encode(''); + } + const data = concat(protectedHeader, encoder.encode('.'), payload); + const signature = await sign(alg, key, data); + const jws = { + signature: base64url(signature), + payload: '', + }; + if (b64) { + jws.payload = decoder.decode(payload); + } + if (this._unprotectedHeader) { + jws.header = this._unprotectedHeader; + } + if (this._protectedHeader) { + jws.protected = decoder.decode(protectedHeader); + } + return jws; + } +} diff --git a/internal/server/web/static/js/lib/jose/jws/flattened/verify.js b/internal/server/web/static/js/lib/jose/jws/flattened/verify.js new file mode 100644 index 00000000..2f30e86b --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jws/flattened/verify.js @@ -0,0 +1,105 @@ +import {decode as base64url} from '../../runtime/base64url.js'; +import verify from '../../runtime/verify.js'; +import {JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed} from '../../util/errors.js'; +import {concat, decoder, encoder} from '../../lib/buffer_utils.js'; +import isDisjoint from '../../lib/is_disjoint.js'; +import isObject from '../../lib/is_object.js'; +import checkKeyType from '../../lib/check_key_type.js'; +import validateCrit from '../../lib/validate_crit.js'; +import validateAlgorithms from '../../lib/validate_algorithms.js'; + +export async function flattenedVerify(jws, key, options) { + var _a; + if (!isObject(jws)) { + throw new JWSInvalid('Flattened JWS must be an object'); + } + if (jws.protected === undefined && jws.header === undefined) { + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + } + if (jws.protected !== undefined && typeof jws.protected !== 'string') { + throw new JWSInvalid('JWS Protected Header incorrect type'); + } + if (jws.payload === undefined) { + throw new JWSInvalid('JWS Payload missing'); + } + if (typeof jws.signature !== 'string') { + throw new JWSInvalid('JWS Signature missing or incorrect type'); + } + if (jws.header !== undefined && !isObject(jws.header)) { + throw new JWSInvalid('JWS Unprotected Header incorrect type'); + } + let parsedProt = {}; + if (jws.protected) { + const protectedHeader = base64url(jws.protected); + try { + parsedProt = JSON.parse(decoder.decode(protectedHeader)); + } + catch (_b) { + throw new JWSInvalid('JWS Protected Header is invalid'); + } + } + if (!isDisjoint(parsedProt, jws.header)) { + throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint'); + } + const joseHeader = { + ...parsedProt, + ...jws.header, + }; + const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options === null || options === void 0 ? void 0 : options.crit, parsedProt, joseHeader); + let b64 = true; + if (extensions.has('b64')) { + b64 = parsedProt.b64; + if (typeof b64 !== 'boolean') { + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + } + } + const { alg } = joseHeader; + if (typeof alg !== 'string' || !alg) { + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + } + const algorithms = options && validateAlgorithms('algorithms', options.algorithms); + if (algorithms && !algorithms.has(alg)) { + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter not allowed'); + } + if (b64) { + if (typeof jws.payload !== 'string') { + throw new JWSInvalid('JWS Payload must be a string'); + } + } + else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) { + throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance'); + } + let resolvedKey = false; + if (typeof key === 'function') { + key = await key(parsedProt, jws); + resolvedKey = true; + } + checkKeyType(alg, key, 'verify'); + const data = concat(encoder.encode((_a = jws.protected) !== null && _a !== void 0 ? _a : ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload); + const signature = base64url(jws.signature); + const verified = await verify(alg, key, signature, data); + if (!verified) { + throw new JWSSignatureVerificationFailed(); + } + let payload; + if (b64) { + payload = base64url(jws.payload); + } + else if (typeof jws.payload === 'string') { + payload = encoder.encode(jws.payload); + } + else { + payload = jws.payload; + } + const result = { payload }; + if (jws.protected !== undefined) { + result.protectedHeader = parsedProt; + } + if (jws.header !== undefined) { + result.unprotectedHeader = jws.header; + } + if (resolvedKey) { + return { ...result, key }; + } + return result; +} diff --git a/internal/server/web/static/js/lib/jose/jws/general/sign.js b/internal/server/web/static/js/lib/jose/jws/general/sign.js new file mode 100644 index 00000000..3e1edd39 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jws/general/sign.js @@ -0,0 +1,68 @@ +import {FlattenedSign} from '../flattened/sign.js'; +import {JWSInvalid} from '../../util/errors.js'; + +class IndividualSignature { + constructor(sig, key, options) { + this.parent = sig; + this.key = key; + this.options = options; + } + setProtectedHeader(protectedHeader) { + if (this.protectedHeader) { + throw new TypeError('setProtectedHeader can only be called once'); + } + this.protectedHeader = protectedHeader; + return this; + } + setUnprotectedHeader(unprotectedHeader) { + if (this.unprotectedHeader) { + throw new TypeError('setUnprotectedHeader can only be called once'); + } + this.unprotectedHeader = unprotectedHeader; + return this; + } + addSignature(...args) { + return this.parent.addSignature(...args); + } + sign(...args) { + return this.parent.sign(...args); + } + done() { + return this.parent; + } +} +export class GeneralSign { + constructor(payload) { + this._signatures = []; + this._payload = payload; + } + addSignature(key, options) { + const signature = new IndividualSignature(this, key, options); + this._signatures.push(signature); + return signature; + } + async sign() { + if (!this._signatures.length) { + throw new JWSInvalid('at least one signature must be added'); + } + const jws = { + signatures: [], + payload: '', + }; + for (let i = 0; i < this._signatures.length; i++) { + const signature = this._signatures[i]; + const flattened = new FlattenedSign(this._payload); + flattened.setProtectedHeader(signature.protectedHeader); + flattened.setUnprotectedHeader(signature.unprotectedHeader); + const { payload, ...rest } = await flattened.sign(signature.key, signature.options); + if (i === 0) { + jws.payload = payload; + } + else if (jws.payload !== payload) { + throw new JWSInvalid('inconsistent use of JWS Unencoded Payload Option (RFC7797)'); + } + jws.signatures.push(rest); + } + return jws; + } +} diff --git a/internal/server/web/static/js/lib/jose/jws/general/verify.js b/internal/server/web/static/js/lib/jose/jws/general/verify.js new file mode 100644 index 00000000..c0a73eae --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jws/general/verify.js @@ -0,0 +1,25 @@ +import {flattenedVerify} from '../flattened/verify.js'; +import {JWSInvalid, JWSSignatureVerificationFailed} from '../../util/errors.js'; +import isObject from '../../lib/is_object.js'; + +export async function generalVerify(jws, key, options) { + if (!isObject(jws)) { + throw new JWSInvalid('General JWS must be an object'); + } + if (!Array.isArray(jws.signatures) || !jws.signatures.every(isObject)) { + throw new JWSInvalid('JWS Signatures missing or incorrect type'); + } + for (const signature of jws.signatures) { + try { + return await flattenedVerify({ + header: signature.header, + payload: jws.payload, + protected: signature.protected, + signature: signature.signature, + }, key, options); + } + catch (_a) { + } + } + throw new JWSSignatureVerificationFailed(); +} diff --git a/internal/server/web/static/js/lib/jose/jwt/decrypt.js b/internal/server/web/static/js/lib/jose/jwt/decrypt.js new file mode 100644 index 00000000..6ae0014b --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jwt/decrypt.js @@ -0,0 +1,24 @@ +import {compactDecrypt} from '../jwe/compact/decrypt.js'; +import jwtPayload from '../lib/jwt_claims_set.js'; +import {JWTClaimValidationFailed} from '../util/errors.js'; + +export async function jwtDecrypt(jwt, key, options) { + const decrypted = await compactDecrypt(jwt, key, options); + const payload = jwtPayload(decrypted.protectedHeader, decrypted.plaintext, options); + const { protectedHeader } = decrypted; + if (protectedHeader.iss !== undefined && protectedHeader.iss !== payload.iss) { + throw new JWTClaimValidationFailed('replicated "iss" claim header parameter mismatch', 'iss', 'mismatch'); + } + if (protectedHeader.sub !== undefined && protectedHeader.sub !== payload.sub) { + throw new JWTClaimValidationFailed('replicated "sub" claim header parameter mismatch', 'sub', 'mismatch'); + } + if (protectedHeader.aud !== undefined && + JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud)) { + throw new JWTClaimValidationFailed('replicated "aud" claim header parameter mismatch', 'aud', 'mismatch'); + } + const result = { payload, protectedHeader }; + if (typeof key === 'function') { + return { ...result, key: decrypted.key }; + } + return result; +} diff --git a/internal/server/web/static/js/lib/jose/jwt/encrypt.js b/internal/server/web/static/js/lib/jose/jwt/encrypt.js new file mode 100644 index 00000000..bad7be2e --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jwt/encrypt.js @@ -0,0 +1,69 @@ +import {CompactEncrypt} from '../jwe/compact/encrypt.js'; +import {encoder} from '../lib/buffer_utils.js'; +import {ProduceJWT} from './produce.js'; + +export class EncryptJWT extends ProduceJWT { + setProtectedHeader(protectedHeader) { + if (this._protectedHeader) { + throw new TypeError('setProtectedHeader can only be called once'); + } + this._protectedHeader = protectedHeader; + return this; + } + setKeyManagementParameters(parameters) { + if (this._keyManagementParameters) { + throw new TypeError('setKeyManagementParameters can only be called once'); + } + this._keyManagementParameters = parameters; + return this; + } + setContentEncryptionKey(cek) { + if (this._cek) { + throw new TypeError('setContentEncryptionKey can only be called once'); + } + this._cek = cek; + return this; + } + setInitializationVector(iv) { + if (this._iv) { + throw new TypeError('setInitializationVector can only be called once'); + } + this._iv = iv; + return this; + } + replicateIssuerAsHeader() { + this._replicateIssuerAsHeader = true; + return this; + } + replicateSubjectAsHeader() { + this._replicateSubjectAsHeader = true; + return this; + } + replicateAudienceAsHeader() { + this._replicateAudienceAsHeader = true; + return this; + } + async encrypt(key, options) { + const enc = new CompactEncrypt(encoder.encode(JSON.stringify(this._payload))); + if (this._replicateIssuerAsHeader) { + this._protectedHeader = { ...this._protectedHeader, iss: this._payload.iss }; + } + if (this._replicateSubjectAsHeader) { + this._protectedHeader = { ...this._protectedHeader, sub: this._payload.sub }; + } + if (this._replicateAudienceAsHeader) { + this._protectedHeader = { ...this._protectedHeader, aud: this._payload.aud }; + } + enc.setProtectedHeader(this._protectedHeader); + if (this._iv) { + enc.setInitializationVector(this._iv); + } + if (this._cek) { + enc.setContentEncryptionKey(this._cek); + } + if (this._keyManagementParameters) { + enc.setKeyManagementParameters(this._keyManagementParameters); + } + return enc.encrypt(key, options); + } +} diff --git a/internal/server/web/static/js/lib/jose/jwt/produce.js b/internal/server/web/static/js/lib/jose/jwt/produce.js new file mode 100644 index 00000000..40a69077 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jwt/produce.js @@ -0,0 +1,55 @@ +import epoch from '../lib/epoch.js'; +import isObject from '../lib/is_object.js'; +import secs from '../lib/secs.js'; + +export class ProduceJWT { + constructor(payload) { + if (!isObject(payload)) { + throw new TypeError('JWT Claims Set MUST be an object'); + } + this._payload = payload; + } + setIssuer(issuer) { + this._payload = { ...this._payload, iss: issuer }; + return this; + } + setSubject(subject) { + this._payload = { ...this._payload, sub: subject }; + return this; + } + setAudience(audience) { + this._payload = { ...this._payload, aud: audience }; + return this; + } + setJti(jwtId) { + this._payload = { ...this._payload, jti: jwtId }; + return this; + } + setNotBefore(input) { + if (typeof input === 'number') { + this._payload = { ...this._payload, nbf: input }; + } + else { + this._payload = { ...this._payload, nbf: epoch(new Date()) + secs(input) }; + } + return this; + } + setExpirationTime(input) { + if (typeof input === 'number') { + this._payload = { ...this._payload, exp: input }; + } + else { + this._payload = { ...this._payload, exp: epoch(new Date()) + secs(input) }; + } + return this; + } + setIssuedAt(input) { + if (typeof input === 'undefined') { + this._payload = { ...this._payload, iat: epoch(new Date()) }; + } + else { + this._payload = { ...this._payload, iat: input }; + } + return this; + } +} diff --git a/internal/server/web/static/js/lib/jose/jwt/sign.js b/internal/server/web/static/js/lib/jose/jwt/sign.js new file mode 100644 index 00000000..410c8c43 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jwt/sign.js @@ -0,0 +1,22 @@ +import {CompactSign} from '../jws/compact/sign.js'; +import {JWTInvalid} from '../util/errors.js'; +import {encoder} from '../lib/buffer_utils.js'; +import {ProduceJWT} from './produce.js'; + +export class SignJWT extends ProduceJWT { + setProtectedHeader(protectedHeader) { + this._protectedHeader = protectedHeader; + return this; + } + async sign(key, options) { + var _a; + const sig = new CompactSign(encoder.encode(JSON.stringify(this._payload))); + sig.setProtectedHeader(this._protectedHeader); + if (Array.isArray((_a = this._protectedHeader) === null || _a === void 0 ? void 0 : _a.crit) && + this._protectedHeader.crit.includes('b64') && + this._protectedHeader.b64 === false) { + throw new JWTInvalid('JWTs MUST NOT use unencoded payload'); + } + return sig.sign(key, options); + } +} diff --git a/internal/server/web/static/js/lib/jose/jwt/unsecured.js b/internal/server/web/static/js/lib/jose/jwt/unsecured.js new file mode 100644 index 00000000..c9611abf --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jwt/unsecured.js @@ -0,0 +1,33 @@ +import * as base64url from '../runtime/base64url.js'; +import {decoder} from '../lib/buffer_utils.js'; +import {JWTInvalid} from '../util/errors.js'; +import jwtPayload from '../lib/jwt_claims_set.js'; +import {ProduceJWT} from './produce.js'; + +export class UnsecuredJWT extends ProduceJWT { + encode() { + const header = base64url.encode(JSON.stringify({ alg: 'none' })); + const payload = base64url.encode(JSON.stringify(this._payload)); + return `${header}.${payload}.`; + } + static decode(jwt, options) { + if (typeof jwt !== 'string') { + throw new JWTInvalid('Unsecured JWT must be a string'); + } + const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split('.'); + if (length !== 3 || signature !== '') { + throw new JWTInvalid('Invalid Unsecured JWT'); + } + let header; + try { + header = JSON.parse(decoder.decode(base64url.decode(encodedHeader))); + if (header.alg !== 'none') + throw new Error(); + } + catch (_a) { + throw new JWTInvalid('Invalid Unsecured JWT'); + } + const payload = jwtPayload(header, base64url.decode(encodedPayload), options); + return { payload, header }; + } +} diff --git a/internal/server/web/static/js/lib/jose/jwt/verify.js b/internal/server/web/static/js/lib/jose/jwt/verify.js new file mode 100644 index 00000000..cb991a8e --- /dev/null +++ b/internal/server/web/static/js/lib/jose/jwt/verify.js @@ -0,0 +1,17 @@ +import {compactVerify} from '../jws/compact/verify.js'; +import jwtPayload from '../lib/jwt_claims_set.js'; +import {JWTInvalid} from '../util/errors.js'; + +export async function jwtVerify(jwt, key, options) { + var _a; + const verified = await compactVerify(jwt, key, options); + if (((_a = verified.protectedHeader.crit) === null || _a === void 0 ? void 0 : _a.includes('b64')) && verified.protectedHeader.b64 === false) { + throw new JWTInvalid('JWTs MUST NOT use unencoded payload'); + } + const payload = jwtPayload(verified.protectedHeader, verified.payload, options); + const result = { payload, protectedHeader: verified.protectedHeader }; + if (typeof key === 'function') { + return { ...result, key: verified.key }; + } + return result; +} diff --git a/internal/server/web/static/js/lib/jose/key/export.js b/internal/server/web/static/js/lib/jose/key/export.js new file mode 100644 index 00000000..bf612ec0 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/key/export.js @@ -0,0 +1,12 @@ +import {toPKCS8 as exportPrivate, toSPKI as exportPublic} from '../runtime/asn1.js'; +import keyToJWK from '../runtime/key_to_jwk.js'; + +export async function exportSPKI(key) { + return exportPublic(key); +} +export async function exportPKCS8(key) { + return exportPrivate(key); +} +export async function exportJWK(key) { + return keyToJWK(key); +} diff --git a/internal/server/web/static/js/lib/jose/key/generate_key_pair.js b/internal/server/web/static/js/lib/jose/key/generate_key_pair.js new file mode 100644 index 00000000..88be5954 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/key/generate_key_pair.js @@ -0,0 +1,5 @@ +import {generateKeyPair as generate} from '../runtime/generate.js'; + +export async function generateKeyPair(alg, options) { + return generate(alg, options); +} diff --git a/internal/server/web/static/js/lib/jose/key/generate_secret.js b/internal/server/web/static/js/lib/jose/key/generate_secret.js new file mode 100644 index 00000000..d80e08f7 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/key/generate_secret.js @@ -0,0 +1,5 @@ +import {generateSecret as generate} from '../runtime/generate.js'; + +export async function generateSecret(alg, options) { + return generate(alg, options); +} diff --git a/internal/server/web/static/js/lib/jose/key/import.js b/internal/server/web/static/js/lib/jose/key/import.js new file mode 100644 index 00000000..693446d3 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/key/import.js @@ -0,0 +1,120 @@ +import {decode as decodeBase64URL, decodeBase64, encodeBase64} from '../runtime/base64url.js'; +import {fromPKCS8 as importPrivate, fromSPKI as importPublic} from '../runtime/asn1.js'; +import asKeyObject from '../runtime/jwk_to_key.js'; +import {JOSENotSupported} from '../util/errors.js'; +import formatPEM from '../lib/format_pem.js'; +import isObject from '../lib/is_object.js'; + +function getElement(seq) { + let result = []; + let next = 0; + while (next < seq.length) { + let nextPart = parseElement(seq.subarray(next)); + result.push(nextPart); + next += nextPart.byteLength; + } + return result; +} +function parseElement(bytes) { + let position = 0; + let tag = bytes[0] & 0x1f; + position++; + if (tag === 0x1f) { + tag = 0; + while (bytes[position] >= 0x80) { + tag = tag * 128 + bytes[position] - 0x80; + position++; + } + tag = tag * 128 + bytes[position] - 0x80; + position++; + } + let length = 0; + if (bytes[position] < 0x80) { + length = bytes[position]; + position++; + } + else { + let numberOfDigits = bytes[position] & 0x7f; + position++; + length = 0; + for (let i = 0; i < numberOfDigits; i++) { + length = length * 256 + bytes[position]; + position++; + } + } + if (length === 0x80) { + length = 0; + while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) { + length++; + } + const byteLength = position + length + 2; + return { + byteLength, + contents: bytes.subarray(position, position + length), + raw: bytes.subarray(0, byteLength), + }; + } + const byteLength = position + length; + return { + byteLength, + contents: bytes.subarray(position, byteLength), + raw: bytes.subarray(0, byteLength), + }; +} +function spkiFromX509(buf) { + const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents); + return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw); +} +function getSPKI(x509) { + const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, ''); + const raw = decodeBase64(pem); + return formatPEM(spkiFromX509(raw), 'PUBLIC KEY'); +} +export async function importSPKI(spki, alg, options) { + if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) { + throw new TypeError('"spki" must be SPKI formatted string'); + } + return importPublic(spki, alg, options); +} +export async function importX509(x509, alg, options) { + if (typeof x509 !== 'string' || x509.indexOf('-----BEGIN CERTIFICATE-----') !== 0) { + throw new TypeError('"x509" must be X.509 formatted string'); + } + const spki = getSPKI(x509); + return importPublic(spki, alg, options); +} +export async function importPKCS8(pkcs8, alg, options) { + if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) { + throw new TypeError('"pkcs8" must be PCKS8 formatted string'); + } + return importPrivate(pkcs8, alg, options); +} +export async function importJWK(jwk, alg, octAsKeyObject) { + if (!isObject(jwk)) { + throw new TypeError('JWK must be an object'); + } + alg || (alg = jwk.alg); + if (typeof alg !== 'string' || !alg) { + throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); + } + switch (jwk.kty) { + case 'oct': + if (typeof jwk.k !== 'string' || !jwk.k) { + throw new TypeError('missing "k" (Key Value) Parameter value'); + } + octAsKeyObject !== null && octAsKeyObject !== void 0 ? octAsKeyObject : (octAsKeyObject = jwk.ext !== true); + if (octAsKeyObject) { + return asKeyObject({ ...jwk, alg, ext: false }); + } + return decodeBase64URL(jwk.k); + case 'RSA': + if (jwk.oth !== undefined) { + throw new JOSENotSupported('RSA JWK "oth" (Other Primes Info) Parameter value is not supported'); + } + case 'EC': + case 'OKP': + return asKeyObject({ ...jwk, alg }); + default: + throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); + } +} diff --git a/internal/server/web/static/js/lib/jose/lib/aesgcmkw.js b/internal/server/web/static/js/lib/jose/lib/aesgcmkw.js new file mode 100644 index 00000000..663781ee --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/aesgcmkw.js @@ -0,0 +1,15 @@ +import encrypt from '../runtime/encrypt.js'; +import decrypt from '../runtime/decrypt.js'; +import generateIv from './iv.js'; +import {encode as base64url} from '../runtime/base64url.js'; + +export async function wrap(alg, key, cek, iv) { + const jweAlgorithm = alg.slice(0, 7); + iv || (iv = generateIv(jweAlgorithm)); + const { ciphertext: encryptedKey, tag } = await encrypt(jweAlgorithm, cek, key, iv, new Uint8Array(0)); + return { encryptedKey, iv: base64url(iv), tag: base64url(tag) }; +} +export async function unwrap(alg, key, encryptedKey, iv, tag) { + const jweAlgorithm = alg.slice(0, 7); + return decrypt(jweAlgorithm, key, encryptedKey, iv, tag, new Uint8Array(0)); +} diff --git a/internal/server/web/static/js/lib/jose/lib/buffer_utils.js b/internal/server/web/static/js/lib/jose/lib/buffer_utils.js new file mode 100644 index 00000000..d8a22e79 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/buffer_utils.js @@ -0,0 +1,52 @@ +import digest from '../runtime/digest.js'; + +export const encoder = new TextEncoder(); +export const decoder = new TextDecoder(); +const MAX_INT32 = 2 ** 32; +export function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0); + const buf = new Uint8Array(size); + let i = 0; + buffers.forEach((buffer) => { + buf.set(buffer, i); + i += buffer.length; + }); + return buf; +} +export function p2s(alg, p2sInput) { + return concat(encoder.encode(alg), new Uint8Array([0]), p2sInput); +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) { + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + } + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset); +} +export function uint64be(value) { + const high = Math.floor(value / MAX_INT32); + const low = value % MAX_INT32; + const buf = new Uint8Array(8); + writeUInt32BE(buf, high, 0); + writeUInt32BE(buf, low, 4); + return buf; +} +export function uint32be(value) { + const buf = new Uint8Array(4); + writeUInt32BE(buf, value); + return buf; +} +export function lengthAndInput(input) { + return concat(uint32be(input.length), input); +} +export async function concatKdf(secret, bits, value) { + const iterations = Math.ceil((bits >> 3) / 32); + const res = new Uint8Array(iterations * 32); + for (let iter = 0; iter < iterations; iter++) { + const buf = new Uint8Array(4 + secret.length + value.length); + buf.set(uint32be(iter + 1)); + buf.set(secret, 4); + buf.set(value, 4 + secret.length); + res.set(await digest('sha256', buf), iter * 32); + } + return res.slice(0, bits >> 3); +} diff --git a/internal/server/web/static/js/lib/jose/lib/cek.js b/internal/server/web/static/js/lib/jose/lib/cek.js new file mode 100644 index 00000000..f00c8377 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/cek.js @@ -0,0 +1,21 @@ +import {JOSENotSupported} from '../util/errors.js'; +import random from '../runtime/random.js'; + +export function bitLength(alg) { + switch (alg) { + case 'A128GCM': + return 128; + case 'A192GCM': + return 192; + case 'A256GCM': + case 'A128CBC-HS256': + return 256; + case 'A192CBC-HS384': + return 384; + case 'A256CBC-HS512': + return 512; + default: + throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`); + } +} +export default (alg) => random(new Uint8Array(bitLength(alg) >> 3)); diff --git a/internal/server/web/static/js/lib/jose/lib/check_iv_length.js b/internal/server/web/static/js/lib/jose/lib/check_iv_length.js new file mode 100644 index 00000000..7e361c25 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/check_iv_length.js @@ -0,0 +1,9 @@ +import {JWEInvalid} from '../util/errors.js'; +import {bitLength} from './iv.js'; + +const checkIvLength = (enc, iv) => { + if (iv.length << 3 !== bitLength(enc)) { + throw new JWEInvalid('Invalid Initialization Vector length'); + } +}; +export default checkIvLength; diff --git a/internal/server/web/static/js/lib/jose/lib/check_key_type.js b/internal/server/web/static/js/lib/jose/lib/check_key_type.js new file mode 100644 index 00000000..a78030b4 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/check_key_type.js @@ -0,0 +1,46 @@ +import invalidKeyInput from './invalid_key_input.js'; +import isKeyLike, {types} from '../runtime/is_key_like.js'; + +const symmetricTypeCheck = (key) => { + if (key instanceof Uint8Array) + return; + if (!isKeyLike(key)) { + throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array')); + } + if (key.type !== 'secret') { + throw new TypeError(`${types.join(' or ')} instances for symmetric algorithms must be of type "secret"`); + } +}; +const asymmetricTypeCheck = (key, usage) => { + if (!isKeyLike(key)) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + if (key.type === 'secret') { + throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithms must not be of type "secret"`); + } + if (usage === 'sign' && key.type === 'public') { + throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm signing must be of type "private"`); + } + if (usage === 'decrypt' && key.type === 'public') { + throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm decryption must be of type "private"`); + } + if (key.algorithm && usage === 'verify' && key.type === 'private') { + throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm verifying must be of type "public"`); + } + if (key.algorithm && usage === 'encrypt' && key.type === 'private') { + throw new TypeError(`${types.join(' or ')} instances for asymmetric algorithm encryption must be of type "public"`); + } +}; +const checkKeyType = (alg, key, usage) => { + const symmetric = alg.startsWith('HS') || + alg === 'dir' || + alg.startsWith('PBES2') || + /^A\d{3}(?:GCM)?KW$/.test(alg); + if (symmetric) { + symmetricTypeCheck(key); + } + else { + asymmetricTypeCheck(key, usage); + } +}; +export default checkKeyType; diff --git a/internal/server/web/static/js/lib/jose/lib/check_p2s.js b/internal/server/web/static/js/lib/jose/lib/check_p2s.js new file mode 100644 index 00000000..ffd04658 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/check_p2s.js @@ -0,0 +1,7 @@ +import {JWEInvalid} from '../util/errors.js'; + +export default function checkP2s(p2s) { + if (!(p2s instanceof Uint8Array) || p2s.length < 8) { + throw new JWEInvalid('PBES2 Salt Input must be 8 or more octets'); + } +} diff --git a/internal/server/web/static/js/lib/jose/lib/crypto_key.js b/internal/server/web/static/js/lib/jose/lib/crypto_key.js new file mode 100644 index 00000000..d1b9edbf --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/crypto_key.js @@ -0,0 +1,146 @@ +import {isCloudflareWorkers} from '../runtime/env.js'; + +function unusable(name, prop = 'algorithm.name') { + return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +} +function isAlgorithm(algorithm, name) { + return algorithm.name === name; +} +function getHashLength(hash) { + return parseInt(hash.name.slice(4), 10); +} +function getNamedCurve(alg) { + switch (alg) { + case 'ES256': + return 'P-256'; + case 'ES384': + return 'P-384'; + case 'ES512': + return 'P-521'; + default: + throw new Error('unreachable'); + } +} +function checkUsage(key, usages) { + if (usages.length && !usages.some((expected) => key.usages.includes(expected))) { + let msg = 'CryptoKey does not support this operation, its usages must include '; + if (usages.length > 2) { + const last = usages.pop(); + msg += `one of ${usages.join(', ')}, or ${last}.`; + } + else if (usages.length === 2) { + msg += `one of ${usages[0]} or ${usages[1]}.`; + } + else { + msg += `${usages[0]}.`; + } + throw new TypeError(msg); + } +} +export function checkSigCryptoKey(key, alg, ...usages) { + switch (alg) { + case 'HS256': + case 'HS384': + case 'HS512': { + if (!isAlgorithm(key.algorithm, 'HMAC')) + throw unusable('HMAC'); + const expected = parseInt(alg.slice(2), 10); + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, 'algorithm.hash'); + break; + } + case 'RS256': + case 'RS384': + case 'RS512': { + if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5')) + throw unusable('RSASSA-PKCS1-v1_5'); + const expected = parseInt(alg.slice(2), 10); + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, 'algorithm.hash'); + break; + } + case 'PS256': + case 'PS384': + case 'PS512': { + if (!isAlgorithm(key.algorithm, 'RSA-PSS')) + throw unusable('RSA-PSS'); + const expected = parseInt(alg.slice(2), 10); + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, 'algorithm.hash'); + break; + } + case isCloudflareWorkers() && 'EdDSA': { + if (!isAlgorithm(key.algorithm, 'NODE-ED25519')) + throw unusable('NODE-ED25519'); + break; + } + case 'ES256': + case 'ES384': + case 'ES512': { + if (!isAlgorithm(key.algorithm, 'ECDSA')) + throw unusable('ECDSA'); + const expected = getNamedCurve(alg); + const actual = key.algorithm.namedCurve; + if (actual !== expected) + throw unusable(expected, 'algorithm.namedCurve'); + break; + } + default: + throw new TypeError('CryptoKey does not support this operation'); + } + checkUsage(key, usages); +} +export function checkEncCryptoKey(key, alg, ...usages) { + switch (alg) { + case 'A128GCM': + case 'A192GCM': + case 'A256GCM': { + if (!isAlgorithm(key.algorithm, 'AES-GCM')) + throw unusable('AES-GCM'); + const expected = parseInt(alg.slice(1, 4), 10); + const actual = key.algorithm.length; + if (actual !== expected) + throw unusable(expected, 'algorithm.length'); + break; + } + case 'A128KW': + case 'A192KW': + case 'A256KW': { + if (!isAlgorithm(key.algorithm, 'AES-KW')) + throw unusable('AES-KW'); + const expected = parseInt(alg.slice(1, 4), 10); + const actual = key.algorithm.length; + if (actual !== expected) + throw unusable(expected, 'algorithm.length'); + break; + } + case 'ECDH': + if (!isAlgorithm(key.algorithm, 'ECDH')) + throw unusable('ECDH'); + break; + case 'PBES2-HS256+A128KW': + case 'PBES2-HS384+A192KW': + case 'PBES2-HS512+A256KW': + if (!isAlgorithm(key.algorithm, 'PBKDF2')) + throw unusable('PBKDF2'); + break; + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': { + if (!isAlgorithm(key.algorithm, 'RSA-OAEP')) + throw unusable('RSA-OAEP'); + const expected = parseInt(alg.slice(9), 10) || 1; + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, 'algorithm.hash'); + break; + } + default: + throw new TypeError('CryptoKey does not support this operation'); + } + checkUsage(key, usages); +} diff --git a/internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js b/internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js new file mode 100644 index 00000000..92529791 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/decrypt_key_management.js @@ -0,0 +1,96 @@ +import {unwrap as aesKw} from '../runtime/aeskw.js'; +import * as ECDH from '../runtime/ecdhes.js'; +import {decrypt as pbes2Kw} from '../runtime/pbes2kw.js'; +import {decrypt as rsaEs} from '../runtime/rsaes.js'; +import {decode as base64url} from '../runtime/base64url.js'; +import {JOSENotSupported, JWEInvalid} from '../util/errors.js'; +import {bitLength as cekLength} from '../lib/cek.js'; +import {importJWK} from '../key/import.js'; +import checkKeyType from './check_key_type.js'; +import isObject from './is_object.js'; +import {unwrap as aesGcmKw} from './aesgcmkw.js'; + +async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) { + checkKeyType(alg, key, 'decrypt'); + switch (alg) { + case 'dir': { + if (encryptedKey !== undefined) + throw new JWEInvalid('Encountered unexpected JWE Encrypted Key'); + return key; + } + case 'ECDH-ES': + if (encryptedKey !== undefined) + throw new JWEInvalid('Encountered unexpected JWE Encrypted Key'); + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': { + if (!isObject(joseHeader.epk)) + throw new JWEInvalid(`JOSE Header "epk" (Ephemeral Public Key) missing or invalid`); + if (!ECDH.ecdhAllowed(key)) + throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime'); + const epk = await importJWK(joseHeader.epk, alg); + let partyUInfo; + let partyVInfo; + if (joseHeader.apu !== undefined) { + if (typeof joseHeader.apu !== 'string') + throw new JWEInvalid(`JOSE Header "apu" (Agreement PartyUInfo) invalid`); + partyUInfo = base64url(joseHeader.apu); + } + if (joseHeader.apv !== undefined) { + if (typeof joseHeader.apv !== 'string') + throw new JWEInvalid(`JOSE Header "apv" (Agreement PartyVInfo) invalid`); + partyVInfo = base64url(joseHeader.apv); + } + const sharedSecret = await ECDH.deriveKey(epk, key, alg === 'ECDH-ES' ? joseHeader.enc : alg, alg === 'ECDH-ES' ? cekLength(joseHeader.enc) : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo); + if (alg === 'ECDH-ES') + return sharedSecret; + if (encryptedKey === undefined) + throw new JWEInvalid('JWE Encrypted Key missing'); + return aesKw(alg.slice(-6), sharedSecret, encryptedKey); + } + case 'RSA1_5': + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': { + if (encryptedKey === undefined) + throw new JWEInvalid('JWE Encrypted Key missing'); + return rsaEs(alg, key, encryptedKey); + } + case 'PBES2-HS256+A128KW': + case 'PBES2-HS384+A192KW': + case 'PBES2-HS512+A256KW': { + if (encryptedKey === undefined) + throw new JWEInvalid('JWE Encrypted Key missing'); + if (typeof joseHeader.p2c !== 'number') + throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`); + if (typeof joseHeader.p2s !== 'string') + throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`); + return pbes2Kw(alg, key, encryptedKey, joseHeader.p2c, base64url(joseHeader.p2s)); + } + case 'A128KW': + case 'A192KW': + case 'A256KW': { + if (encryptedKey === undefined) + throw new JWEInvalid('JWE Encrypted Key missing'); + return aesKw(alg, key, encryptedKey); + } + case 'A128GCMKW': + case 'A192GCMKW': + case 'A256GCMKW': { + if (encryptedKey === undefined) + throw new JWEInvalid('JWE Encrypted Key missing'); + if (typeof joseHeader.iv !== 'string') + throw new JWEInvalid(`JOSE Header "iv" (Initialization Vector) missing or invalid`); + if (typeof joseHeader.tag !== 'string') + throw new JWEInvalid(`JOSE Header "tag" (Authentication Tag) missing or invalid`); + const iv = base64url(joseHeader.iv); + const tag = base64url(joseHeader.tag); + return aesGcmKw(alg, key, encryptedKey, iv, tag); + } + default: { + throw new JOSENotSupported('Invalid or unsupported "alg" (JWE Algorithm) header value'); + } + } +} +export default decryptKeyManagement; diff --git a/internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js b/internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js new file mode 100644 index 00000000..0ec9c6bc --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/encrypt_key_management.js @@ -0,0 +1,88 @@ +import {wrap as aesKw} from '../runtime/aeskw.js'; +import * as ECDH from '../runtime/ecdhes.js'; +import {encrypt as pbes2Kw} from '../runtime/pbes2kw.js'; +import {encrypt as rsaEs} from '../runtime/rsaes.js'; +import {encode as base64url} from '../runtime/base64url.js'; +import generateCek, {bitLength as cekLength} from '../lib/cek.js'; +import {JOSENotSupported} from '../util/errors.js'; +import {exportJWK} from '../key/export.js'; +import checkKeyType from './check_key_type.js'; +import {wrap as aesGcmKw} from './aesgcmkw.js'; + +async function encryptKeyManagement(alg, enc, key, providedCek, providedParameters = {}) { + let encryptedKey; + let parameters; + let cek; + checkKeyType(alg, key, 'encrypt'); + switch (alg) { + case 'dir': { + cek = key; + break; + } + case 'ECDH-ES': + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': { + if (!ECDH.ecdhAllowed(key)) { + throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime'); + } + const { apu, apv } = providedParameters; + let { epk: ephemeralKey } = providedParameters; + ephemeralKey || (ephemeralKey = (await ECDH.generateEpk(key)).privateKey); + const { x, y, crv, kty } = await exportJWK(ephemeralKey); + const sharedSecret = await ECDH.deriveKey(key, ephemeralKey, alg === 'ECDH-ES' ? enc : alg, alg === 'ECDH-ES' ? cekLength(enc) : parseInt(alg.slice(-5, -2), 10), apu, apv); + parameters = { epk: { x, crv, kty } }; + if (kty === 'EC') + parameters.epk.y = y; + if (apu) + parameters.apu = base64url(apu); + if (apv) + parameters.apv = base64url(apv); + if (alg === 'ECDH-ES') { + cek = sharedSecret; + break; + } + cek = providedCek || generateCek(enc); + const kwAlg = alg.slice(-6); + encryptedKey = await aesKw(kwAlg, sharedSecret, cek); + break; + } + case 'RSA1_5': + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': { + cek = providedCek || generateCek(enc); + encryptedKey = await rsaEs(alg, key, cek); + break; + } + case 'PBES2-HS256+A128KW': + case 'PBES2-HS384+A192KW': + case 'PBES2-HS512+A256KW': { + cek = providedCek || generateCek(enc); + const { p2c, p2s } = providedParameters; + ({ encryptedKey, ...parameters } = await pbes2Kw(alg, key, cek, p2c, p2s)); + break; + } + case 'A128KW': + case 'A192KW': + case 'A256KW': { + cek = providedCek || generateCek(enc); + encryptedKey = await aesKw(alg, key, cek); + break; + } + case 'A128GCMKW': + case 'A192GCMKW': + case 'A256GCMKW': { + cek = providedCek || generateCek(enc); + const { iv } = providedParameters; + ({ encryptedKey, ...parameters } = await aesGcmKw(alg, key, cek, iv)); + break; + } + default: { + throw new JOSENotSupported('Invalid or unsupported "alg" (JWE Algorithm) header value'); + } + } + return { cek, encryptedKey, parameters }; +} +export default encryptKeyManagement; diff --git a/internal/server/web/static/js/lib/jose/lib/epoch.js b/internal/server/web/static/js/lib/jose/lib/epoch.js new file mode 100644 index 00000000..e405e4b2 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/epoch.js @@ -0,0 +1 @@ +export default (date) => Math.floor(date.getTime() / 1000); diff --git a/internal/server/web/static/js/lib/jose/lib/format_pem.js b/internal/server/web/static/js/lib/jose/lib/format_pem.js new file mode 100644 index 00000000..81673f25 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/format_pem.js @@ -0,0 +1,4 @@ +export default (b64, descriptor) => { + const newlined = (b64.match(/.{1,64}/g) || []).join('\n'); + return `-----BEGIN ${descriptor}-----\n${newlined}\n-----END ${descriptor}-----`; +}; diff --git a/internal/server/web/static/js/lib/jose/lib/invalid_key_input.js b/internal/server/web/static/js/lib/jose/lib/invalid_key_input.js new file mode 100644 index 00000000..468ad288 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/invalid_key_input.js @@ -0,0 +1,25 @@ +export default (actual, ...types) => { + let msg = 'Key must be '; + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(', ')}, or ${last}.`; + } + else if (types.length === 2) { + msg += `one of type ${types[0]} or ${types[1]}.`; + } + else { + msg += `of type ${types[0]}.`; + } + if (actual == null) { + msg += ` Received ${actual}`; + } + else if (typeof actual === 'function' && actual.name) { + msg += ` Received function ${actual.name}`; + } + else if (typeof actual === 'object' && actual != null) { + if (actual.constructor && actual.constructor.name) { + msg += ` Received an instance of ${actual.constructor.name}`; + } + } + return msg; +}; diff --git a/internal/server/web/static/js/lib/jose/lib/is_disjoint.js b/internal/server/web/static/js/lib/jose/lib/is_disjoint.js new file mode 100644 index 00000000..6f643502 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/is_disjoint.js @@ -0,0 +1,22 @@ +const isDisjoint = (...headers) => { + const sources = headers.filter(Boolean); + if (sources.length === 0 || sources.length === 1) { + return true; + } + let acc; + for (const header of sources) { + const parameters = Object.keys(header); + if (!acc || acc.size === 0) { + acc = new Set(parameters); + continue; + } + for (const parameter of parameters) { + if (acc.has(parameter)) { + return false; + } + acc.add(parameter); + } + } + return true; +}; +export default isDisjoint; diff --git a/internal/server/web/static/js/lib/jose/lib/is_object.js b/internal/server/web/static/js/lib/jose/lib/is_object.js new file mode 100644 index 00000000..4955e932 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/is_object.js @@ -0,0 +1,16 @@ +function isObjectLike(value) { + return typeof value === 'object' && value !== null; +} +export default function isObject(input) { + if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') { + return false; + } + if (Object.getPrototypeOf(input) === null) { + return true; + } + let proto = input; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + return Object.getPrototypeOf(input) === proto; +} diff --git a/internal/server/web/static/js/lib/jose/lib/iv.js b/internal/server/web/static/js/lib/jose/lib/iv.js new file mode 100644 index 00000000..e477b548 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/iv.js @@ -0,0 +1,21 @@ +import {JOSENotSupported} from '../util/errors.js'; +import random from '../runtime/random.js'; + +export function bitLength(alg) { + switch (alg) { + case 'A128GCM': + case 'A128GCMKW': + case 'A192GCM': + case 'A192GCMKW': + case 'A256GCM': + case 'A256GCMKW': + return 96; + case 'A128CBC-HS256': + case 'A192CBC-HS384': + case 'A256CBC-HS512': + return 128; + default: + throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`); + } +} +export default (alg) => random(new Uint8Array(bitLength(alg) >> 3)); diff --git a/internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js b/internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js new file mode 100644 index 00000000..56feec48 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/jwt_claims_set.js @@ -0,0 +1,92 @@ +import {JWTClaimValidationFailed, JWTExpired, JWTInvalid} from '../util/errors.js'; +import {decoder} from './buffer_utils.js'; +import epoch from './epoch.js'; +import secs from './secs.js'; +import isObject from './is_object.js'; + +const normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, ''); +const checkAudiencePresence = (audPayload, audOption) => { + if (typeof audPayload === 'string') { + return audOption.includes(audPayload); + } + if (Array.isArray(audPayload)) { + return audOption.some(Set.prototype.has.bind(new Set(audPayload))); + } + return false; +}; +export default (protectedHeader, encodedPayload, options = {}) => { + const { typ } = options; + if (typ && + (typeof protectedHeader.typ !== 'string' || + normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) { + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', 'typ', 'check_failed'); + } + let payload; + try { + payload = JSON.parse(decoder.decode(encodedPayload)); + } + catch (_a) { + } + if (!isObject(payload)) { + throw new JWTInvalid('JWT Claims Set must be a top-level JSON object'); + } + const { issuer } = options; + if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) { + throw new JWTClaimValidationFailed('unexpected "iss" claim value', 'iss', 'check_failed'); + } + const { subject } = options; + if (subject && payload.sub !== subject) { + throw new JWTClaimValidationFailed('unexpected "sub" claim value', 'sub', 'check_failed'); + } + const { audience } = options; + if (audience && + !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) { + throw new JWTClaimValidationFailed('unexpected "aud" claim value', 'aud', 'check_failed'); + } + let tolerance; + switch (typeof options.clockTolerance) { + case 'string': + tolerance = secs(options.clockTolerance); + break; + case 'number': + tolerance = options.clockTolerance; + break; + case 'undefined': + tolerance = 0; + break; + default: + throw new TypeError('Invalid clockTolerance option type'); + } + const { currentDate } = options; + const now = epoch(currentDate || new Date()); + if ((payload.iat !== undefined || options.maxTokenAge) && typeof payload.iat !== 'number') { + throw new JWTClaimValidationFailed('"iat" claim must be a number', 'iat', 'invalid'); + } + if (payload.nbf !== undefined) { + if (typeof payload.nbf !== 'number') { + throw new JWTClaimValidationFailed('"nbf" claim must be a number', 'nbf', 'invalid'); + } + if (payload.nbf > now + tolerance) { + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', 'nbf', 'check_failed'); + } + } + if (payload.exp !== undefined) { + if (typeof payload.exp !== 'number') { + throw new JWTClaimValidationFailed('"exp" claim must be a number', 'exp', 'invalid'); + } + if (payload.exp <= now - tolerance) { + throw new JWTExpired('"exp" claim timestamp check failed', 'exp', 'check_failed'); + } + } + if (options.maxTokenAge) { + const age = now - payload.iat; + const max = typeof options.maxTokenAge === 'number' ? options.maxTokenAge : secs(options.maxTokenAge); + if (age - tolerance > max) { + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', 'iat', 'check_failed'); + } + if (age < 0 - tolerance) { + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', 'iat', 'check_failed'); + } + } + return payload; +}; diff --git a/internal/server/web/static/js/lib/jose/lib/secs.js b/internal/server/web/static/js/lib/jose/lib/secs.js new file mode 100644 index 00000000..cf470ed8 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/secs.js @@ -0,0 +1,44 @@ +const minute = 60; +const hour = minute * 60; +const day = hour * 24; +const week = day * 7; +const year = day * 365.25; +const REGEX = /^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i; +export default (str) => { + const matched = REGEX.exec(str); + if (!matched) { + throw new TypeError('Invalid time period format'); + } + const value = parseFloat(matched[1]); + const unit = matched[2].toLowerCase(); + switch (unit) { + case 'sec': + case 'secs': + case 'second': + case 'seconds': + case 's': + return Math.round(value); + case 'minute': + case 'minutes': + case 'min': + case 'mins': + case 'm': + return Math.round(value * minute); + case 'hour': + case 'hours': + case 'hr': + case 'hrs': + case 'h': + return Math.round(value * hour); + case 'day': + case 'days': + case 'd': + return Math.round(value * day); + case 'week': + case 'weeks': + case 'w': + return Math.round(value * week); + default: + return Math.round(value * year); + } +}; diff --git a/internal/server/web/static/js/lib/jose/lib/validate_algorithms.js b/internal/server/web/static/js/lib/jose/lib/validate_algorithms.js new file mode 100644 index 00000000..a6a79185 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/validate_algorithms.js @@ -0,0 +1,11 @@ +const validateAlgorithms = (option, algorithms) => { + if (algorithms !== undefined && + (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) { + throw new TypeError(`"${option}" option must be an array of strings`); + } + if (!algorithms) { + return undefined; + } + return new Set(algorithms); +}; +export default validateAlgorithms; diff --git a/internal/server/web/static/js/lib/jose/lib/validate_crit.js b/internal/server/web/static/js/lib/jose/lib/validate_crit.js new file mode 100644 index 00000000..ebbf4354 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/lib/validate_crit.js @@ -0,0 +1,35 @@ +import {JOSENotSupported} from '../util/errors.js'; + +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== undefined && protectedHeader.crit === undefined) { + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + } + if (!protectedHeader || protectedHeader.crit === undefined) { + return new Set(); + } + if (!Array.isArray(protectedHeader.crit) || + protectedHeader.crit.length === 0 || + protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) { + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + } + let recognized; + if (recognizedOption !== undefined) { + recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]); + } + else { + recognized = recognizedDefault; + } + for (const parameter of protectedHeader.crit) { + if (!recognized.has(parameter)) { + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + } + if (joseHeader[parameter] === undefined) { + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + } + else if (recognized.get(parameter) && protectedHeader[parameter] === undefined) { + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + } + return new Set(protectedHeader.crit); +} +export default validateCrit; diff --git a/internal/server/web/static/js/lib/jose/runtime/aeskw.js b/internal/server/web/static/js/lib/jose/runtime/aeskw.js new file mode 100644 index 00000000..24272399 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/aeskw.js @@ -0,0 +1,33 @@ +import bogusWebCrypto from './bogus.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkEncCryptoKey} from '../lib/crypto_key.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {types} from './is_key_like.js'; + +function checkKeySize(key, alg) { + if (key.algorithm.length !== parseInt(alg.slice(1, 4), 10)) { + throw new TypeError(`Invalid key size for alg: ${alg}`); + } +} +function getCryptoKey(key, alg, usage) { + if (isCryptoKey(key)) { + checkEncCryptoKey(key, alg, usage); + return key; + } + if (key instanceof Uint8Array) { + return crypto.subtle.importKey('raw', key, 'AES-KW', true, [usage]); + } + throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array')); +} +export const wrap = async (alg, key, cek) => { + const cryptoKey = await getCryptoKey(key, alg, 'wrapKey'); + checkKeySize(cryptoKey, alg); + const cryptoKeyCek = await crypto.subtle.importKey('raw', cek, ...bogusWebCrypto); + return new Uint8Array(await crypto.subtle.wrapKey('raw', cryptoKeyCek, cryptoKey, 'AES-KW')); +}; +export const unwrap = async (alg, key, encryptedKey) => { + const cryptoKey = await getCryptoKey(key, alg, 'unwrapKey'); + checkKeySize(cryptoKey, alg); + const cryptoKeyCek = await crypto.subtle.unwrapKey('raw', encryptedKey, cryptoKey, 'AES-KW', ...bogusWebCrypto); + return new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKeyCek)); +}; diff --git a/internal/server/web/static/js/lib/jose/runtime/asn1.js b/internal/server/web/static/js/lib/jose/runtime/asn1.js new file mode 100644 index 00000000..a33a2169 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/asn1.js @@ -0,0 +1,119 @@ +import {isCloudflareWorkers} from './env.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {encodeBase64} from './base64url.js'; +import formatPEM from '../lib/format_pem.js'; +import {JOSENotSupported} from '../util/errors.js'; +import {types} from './is_key_like.js'; + +const genericExport = async (keyType, keyFormat, key) => { + if (!isCryptoKey(key)) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + if (!key.extractable) { + throw new TypeError('CryptoKey is not extractable'); + } + if (key.type !== keyType) { + throw new TypeError(`key is not a ${keyType} key`); + } + return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`); +}; +export const toSPKI = (key) => { + return genericExport('public', 'spki', key); +}; +export const toPKCS8 = (key) => { + return genericExport('private', 'pkcs8', key); +}; +const findOid = (keyData, oid, from = 0) => { + if (from === 0) { + oid.unshift(oid.length); + oid.unshift(0x06); + } + let i = keyData.indexOf(oid[0], from); + if (i === -1) + return false; + const sub = keyData.subarray(i, i + oid.length); + if (sub.length !== oid.length) + return false; + return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1); +}; +const getNamedCurve = (keyData) => { + switch (true) { + case findOid(keyData, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]): + return 'P-256'; + case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x22]): + return 'P-384'; + case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]): + return 'P-521'; + case isCloudflareWorkers() && findOid(keyData, [0x2b, 0x65, 0x70]): + return 'Ed25519'; + default: + throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type'); + } +}; +const genericImport = async (replace, keyFormat, pem, alg, options) => { + var _a; + let algorithm; + let keyUsages; + const keyData = new Uint8Array(atob(pem.replace(replace, '')) + .split('') + .map((c) => c.charCodeAt(0))); + const isPublic = keyFormat === 'spki'; + switch (alg) { + case 'PS256': + case 'PS384': + case 'PS512': + algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` }; + keyUsages = isPublic ? ['verify'] : ['sign']; + break; + case 'RS256': + case 'RS384': + case 'RS512': + algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` }; + keyUsages = isPublic ? ['verify'] : ['sign']; + break; + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': + algorithm = { + name: 'RSA-OAEP', + hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`, + }; + keyUsages = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']; + break; + case 'ES256': + algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; + keyUsages = isPublic ? ['verify'] : ['sign']; + break; + case 'ES384': + algorithm = { name: 'ECDSA', namedCurve: 'P-384' }; + keyUsages = isPublic ? ['verify'] : ['sign']; + break; + case 'ES512': + algorithm = { name: 'ECDSA', namedCurve: 'P-521' }; + keyUsages = isPublic ? ['verify'] : ['sign']; + break; + case 'ECDH-ES': + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': + algorithm = { name: 'ECDH', namedCurve: getNamedCurve(keyData) }; + keyUsages = isPublic ? [] : ['deriveBits']; + break; + case isCloudflareWorkers() && 'EdDSA': + const namedCurve = getNamedCurve(keyData).toUpperCase(); + algorithm = { name: `NODE-${namedCurve}`, namedCurve: `NODE-${namedCurve}` }; + keyUsages = isPublic ? ['verify'] : ['sign']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value'); + } + return crypto.subtle.importKey(keyFormat, keyData, algorithm, (_a = options === null || options === void 0 ? void 0 : options.extractable) !== null && _a !== void 0 ? _a : false, keyUsages); +}; +export const fromPKCS8 = (pem, alg, options) => { + return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, 'pkcs8', pem, alg, options); +}; +export const fromSPKI = (pem, alg, options) => { + return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg, options); +}; diff --git a/internal/server/web/static/js/lib/jose/runtime/base64url.js b/internal/server/web/static/js/lib/jose/runtime/base64url.js new file mode 100644 index 00000000..f9eba390 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/base64url.js @@ -0,0 +1,38 @@ +import {decoder, encoder} from '../lib/buffer_utils.js'; + +export const encodeBase64 = (input) => { + let unencoded = input; + if (typeof unencoded === 'string') { + unencoded = encoder.encode(unencoded); + } + const CHUNK_SIZE = 0x8000; + const arr = []; + for (let i = 0; i < unencoded.length; i += CHUNK_SIZE) { + arr.push(String.fromCharCode.apply(null, unencoded.subarray(i, i + CHUNK_SIZE))); + } + return btoa(arr.join('')); +}; +export const encode = (input) => { + return encodeBase64(input).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); +}; +export const decodeBase64 = (encoded) => { + const binary = atob(encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +}; +export const decode = (input) => { + let encoded = input; + if (encoded instanceof Uint8Array) { + encoded = decoder.decode(encoded); + } + encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''); + try { + return decodeBase64(encoded); + } + catch (_a) { + throw new TypeError('The input to be decoded is not correctly encoded.'); + } +}; diff --git a/internal/server/web/static/js/lib/jose/runtime/bogus.js b/internal/server/web/static/js/lib/jose/runtime/bogus.js new file mode 100644 index 00000000..8fde604e --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/bogus.js @@ -0,0 +1,6 @@ +const bogusWebCrypto = [ + { hash: 'SHA-256', name: 'HMAC' }, + true, + ['sign'], +]; +export default bogusWebCrypto; diff --git a/internal/server/web/static/js/lib/jose/runtime/check_cek_length.js b/internal/server/web/static/js/lib/jose/runtime/check_cek_length.js new file mode 100644 index 00000000..bf64b7f3 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/check_cek_length.js @@ -0,0 +1,8 @@ +import {JWEInvalid} from '../util/errors.js'; + +const checkCekLength = (cek, expected) => { + if (cek.length << 3 !== expected) { + throw new JWEInvalid('Invalid Content Encryption Key length'); + } +}; +export default checkCekLength; diff --git a/internal/server/web/static/js/lib/jose/runtime/check_key_length.js b/internal/server/web/static/js/lib/jose/runtime/check_key_length.js new file mode 100644 index 00000000..33970068 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/check_key_length.js @@ -0,0 +1,8 @@ +export default (alg, key) => { + if (alg.startsWith('RS') || alg.startsWith('PS')) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength !== 'number' || modulusLength < 2048) { + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); + } + } +}; diff --git a/internal/server/web/static/js/lib/jose/runtime/decrypt.js b/internal/server/web/static/js/lib/jose/runtime/decrypt.js new file mode 100644 index 00000000..404a2401 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/decrypt.js @@ -0,0 +1,86 @@ +import {concat, uint64be} from '../lib/buffer_utils.js'; +import checkIvLength from '../lib/check_iv_length.js'; +import checkCekLength from './check_cek_length.js'; +import timingSafeEqual from './timing_safe_equal.js'; +import {JOSENotSupported, JWEDecryptionFailed} from '../util/errors.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkEncCryptoKey} from '../lib/crypto_key.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {types} from './is_key_like.js'; + +async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { + if (!(cek instanceof Uint8Array)) { + throw new TypeError(invalidKeyInput(cek, 'Uint8Array')); + } + const keySize = parseInt(enc.slice(1, 4), 10); + const encKey = await crypto.subtle.importKey('raw', cek.subarray(keySize >> 3), 'AES-CBC', false, ['decrypt']); + const macKey = await crypto.subtle.importKey('raw', cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: 'HMAC', + }, false, ['sign']); + const macData = concat(aad, iv, ciphertext, uint64be(aad.length << 3)); + const expectedTag = new Uint8Array((await crypto.subtle.sign('HMAC', macKey, macData)).slice(0, keySize >> 3)); + let macCheckPassed; + try { + macCheckPassed = timingSafeEqual(tag, expectedTag); + } + catch (_a) { + } + if (!macCheckPassed) { + throw new JWEDecryptionFailed(); + } + let plaintext; + try { + plaintext = new Uint8Array(await crypto.subtle.decrypt({ iv, name: 'AES-CBC' }, encKey, ciphertext)); + } + catch (_b) { + } + if (!plaintext) { + throw new JWEDecryptionFailed(); + } + return plaintext; +} +async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) { + let encKey; + if (cek instanceof Uint8Array) { + encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['decrypt']); + } + else { + checkEncCryptoKey(cek, enc, 'decrypt'); + encKey = cek; + } + try { + return new Uint8Array(await crypto.subtle.decrypt({ + additionalData: aad, + iv, + name: 'AES-GCM', + tagLength: 128, + }, encKey, concat(ciphertext, tag))); + } + catch (_a) { + throw new JWEDecryptionFailed(); + } +} +const decrypt = async (enc, cek, ciphertext, iv, tag, aad) => { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) { + throw new TypeError(invalidKeyInput(cek, ...types, 'Uint8Array')); + } + checkIvLength(enc, iv); + switch (enc) { + case 'A128CBC-HS256': + case 'A192CBC-HS384': + case 'A256CBC-HS512': + if (cek instanceof Uint8Array) + checkCekLength(cek, parseInt(enc.slice(-3), 10)); + return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad); + case 'A128GCM': + case 'A192GCM': + case 'A256GCM': + if (cek instanceof Uint8Array) + checkCekLength(cek, parseInt(enc.slice(1, 4), 10)); + return gcmDecrypt(enc, cek, ciphertext, iv, tag, aad); + default: + throw new JOSENotSupported('Unsupported JWE Content Encryption Algorithm'); + } +}; +export default decrypt; diff --git a/internal/server/web/static/js/lib/jose/runtime/digest.js b/internal/server/web/static/js/lib/jose/runtime/digest.js new file mode 100644 index 00000000..505620bb --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/digest.js @@ -0,0 +1,7 @@ +import crypto from './webcrypto.js'; + +const digest = async (algorithm, data) => { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +}; +export default digest; diff --git a/internal/server/web/static/js/lib/jose/runtime/ecdhes.js b/internal/server/web/static/js/lib/jose/runtime/ecdhes.js new file mode 100644 index 00000000..1ad1b944 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/ecdhes.js @@ -0,0 +1,34 @@ +import {concat, concatKdf, encoder, lengthAndInput, uint32be} from '../lib/buffer_utils.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkEncCryptoKey} from '../lib/crypto_key.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {types} from './is_key_like.js'; + +export async function deriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(0), apv = new Uint8Array(0)) { + if (!isCryptoKey(publicKey)) { + throw new TypeError(invalidKeyInput(publicKey, ...types)); + } + checkEncCryptoKey(publicKey, 'ECDH'); + if (!isCryptoKey(privateKey)) { + throw new TypeError(invalidKeyInput(privateKey, ...types)); + } + checkEncCryptoKey(privateKey, 'ECDH', 'deriveBits'); + const value = concat(lengthAndInput(encoder.encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)); + const sharedSecret = new Uint8Array(await crypto.subtle.deriveBits({ + name: 'ECDH', + public: publicKey, + }, privateKey, Math.ceil(parseInt(privateKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); + return concatKdf(sharedSecret, keyLength, value); +} +export async function generateEpk(key) { + if (!isCryptoKey(key)) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + return crypto.subtle.generateKey(key.algorithm, true, ['deriveBits']); +} +export function ecdhAllowed(key) { + if (!isCryptoKey(key)) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + return ['P-256', 'P-384', 'P-521'].includes(key.algorithm.namedCurve); +} diff --git a/internal/server/web/static/js/lib/jose/runtime/encrypt.js b/internal/server/web/static/js/lib/jose/runtime/encrypt.js new file mode 100644 index 00000000..4212092d --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/encrypt.js @@ -0,0 +1,69 @@ +import {concat, uint64be} from '../lib/buffer_utils.js'; +import checkIvLength from '../lib/check_iv_length.js'; +import checkCekLength from './check_cek_length.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkEncCryptoKey} from '../lib/crypto_key.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {JOSENotSupported} from '../util/errors.js'; +import {types} from './is_key_like.js'; + +async function cbcEncrypt(enc, plaintext, cek, iv, aad) { + if (!(cek instanceof Uint8Array)) { + throw new TypeError(invalidKeyInput(cek, 'Uint8Array')); + } + const keySize = parseInt(enc.slice(1, 4), 10); + const encKey = await crypto.subtle.importKey('raw', cek.subarray(keySize >> 3), 'AES-CBC', false, ['encrypt']); + const macKey = await crypto.subtle.importKey('raw', cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: 'HMAC', + }, false, ['sign']); + const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ + iv, + name: 'AES-CBC', + }, encKey, plaintext)); + const macData = concat(aad, iv, ciphertext, uint64be(aad.length << 3)); + const tag = new Uint8Array((await crypto.subtle.sign('HMAC', macKey, macData)).slice(0, keySize >> 3)); + return { ciphertext, tag }; +} +async function gcmEncrypt(enc, plaintext, cek, iv, aad) { + let encKey; + if (cek instanceof Uint8Array) { + encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']); + } + else { + checkEncCryptoKey(cek, enc, 'encrypt'); + encKey = cek; + } + const encrypted = new Uint8Array(await crypto.subtle.encrypt({ + additionalData: aad, + iv, + name: 'AES-GCM', + tagLength: 128, + }, encKey, plaintext)); + const tag = encrypted.slice(-16); + const ciphertext = encrypted.slice(0, -16); + return { ciphertext, tag }; +} +const encrypt = async (enc, plaintext, cek, iv, aad) => { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) { + throw new TypeError(invalidKeyInput(cek, ...types, 'Uint8Array')); + } + checkIvLength(enc, iv); + switch (enc) { + case 'A128CBC-HS256': + case 'A192CBC-HS384': + case 'A256CBC-HS512': + if (cek instanceof Uint8Array) + checkCekLength(cek, parseInt(enc.slice(-3), 10)); + return cbcEncrypt(enc, plaintext, cek, iv, aad); + case 'A128GCM': + case 'A192GCM': + case 'A256GCM': + if (cek instanceof Uint8Array) + checkCekLength(cek, parseInt(enc.slice(1, 4), 10)); + return gcmEncrypt(enc, plaintext, cek, iv, aad); + default: + throw new JOSENotSupported('Unsupported JWE Content Encryption Algorithm'); + } +}; +export default encrypt; diff --git a/internal/server/web/static/js/lib/jose/runtime/env.js b/internal/server/web/static/js/lib/jose/runtime/env.js new file mode 100644 index 00000000..6751cbb2 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/env.js @@ -0,0 +1,3 @@ +export function isCloudflareWorkers() { + return typeof WebSocketPair === 'function'; +} diff --git a/internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js b/internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js new file mode 100644 index 00000000..9f20b387 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/fetch_jwks.js @@ -0,0 +1,35 @@ +import {JOSEError, JWKSTimeout} from '../util/errors.js'; + +const fetchJwks = async (url, timeout, options) => { + let controller; + let id; + let timedOut = false; + if (typeof AbortController === 'function') { + controller = new AbortController(); + id = setTimeout(() => { + timedOut = true; + controller.abort(); + }, timeout); + } + const response = await fetch(url.href, { + signal: controller ? controller.signal : undefined, + redirect: 'manual', + headers: options.headers, + }).catch((err) => { + if (timedOut) + throw new JWKSTimeout(); + throw err; + }); + if (id !== undefined) + clearTimeout(id); + if (response.status !== 200) { + throw new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response'); + } + try { + return await response.json(); + } + catch (_a) { + throw new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON'); + } +}; +export default fetchJwks; diff --git a/internal/server/web/static/js/lib/jose/runtime/generate.js b/internal/server/web/static/js/lib/jose/runtime/generate.js new file mode 100644 index 00000000..932bfda8 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/generate.js @@ -0,0 +1,127 @@ +import {isCloudflareWorkers} from './env.js'; +import crypto from './webcrypto.js'; +import {JOSENotSupported} from '../util/errors.js'; +import random from './random.js'; + +export async function generateSecret(alg, options) { + var _a; + let length; + let algorithm; + let keyUsages; + switch (alg) { + case 'HS256': + case 'HS384': + case 'HS512': + length = parseInt(alg.slice(-3), 10); + algorithm = { name: 'HMAC', hash: `SHA-${length}`, length }; + keyUsages = ['sign', 'verify']; + break; + case 'A128CBC-HS256': + case 'A192CBC-HS384': + case 'A256CBC-HS512': + length = parseInt(alg.slice(-3), 10); + return random(new Uint8Array(length >> 3)); + case 'A128KW': + case 'A192KW': + case 'A256KW': + length = parseInt(alg.slice(1, 4), 10); + algorithm = { name: 'AES-KW', length }; + keyUsages = ['wrapKey', 'unwrapKey']; + break; + case 'A128GCMKW': + case 'A192GCMKW': + case 'A256GCMKW': + case 'A128GCM': + case 'A192GCM': + case 'A256GCM': + length = parseInt(alg.slice(1, 4), 10); + algorithm = { name: 'AES-GCM', length }; + keyUsages = ['encrypt', 'decrypt']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + return crypto.subtle.generateKey(algorithm, (_a = options === null || options === void 0 ? void 0 : options.extractable) !== null && _a !== void 0 ? _a : false, keyUsages); +} +function getModulusLengthOption(options) { + var _a; + const modulusLength = (_a = options === null || options === void 0 ? void 0 : options.modulusLength) !== null && _a !== void 0 ? _a : 2048; + if (typeof modulusLength !== 'number' || modulusLength < 2048) { + throw new JOSENotSupported('Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used'); + } + return modulusLength; +} +export async function generateKeyPair(alg, options) { + var _a, _b; + let algorithm; + let keyUsages; + switch (alg) { + case 'PS256': + case 'PS384': + case 'PS512': + algorithm = { + name: 'RSA-PSS', + hash: `SHA-${alg.slice(-3)}`, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + modulusLength: getModulusLengthOption(options), + }; + keyUsages = ['sign', 'verify']; + break; + case 'RS256': + case 'RS384': + case 'RS512': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: `SHA-${alg.slice(-3)}`, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + modulusLength: getModulusLengthOption(options), + }; + keyUsages = ['sign', 'verify']; + break; + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': + algorithm = { + name: 'RSA-OAEP', + hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + modulusLength: getModulusLengthOption(options), + }; + keyUsages = ['decrypt', 'unwrapKey', 'encrypt', 'wrapKey']; + break; + case 'ES256': + algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; + keyUsages = ['sign', 'verify']; + break; + case 'ES384': + algorithm = { name: 'ECDSA', namedCurve: 'P-384' }; + keyUsages = ['sign', 'verify']; + break; + case 'ES512': + algorithm = { name: 'ECDSA', namedCurve: 'P-521' }; + keyUsages = ['sign', 'verify']; + break; + case isCloudflareWorkers() && 'EdDSA': + switch (options === null || options === void 0 ? void 0 : options.crv) { + case undefined: + case 'Ed25519': + algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' }; + keyUsages = ['sign', 'verify']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported crv option provided'); + } + break; + case 'ECDH-ES': + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': + algorithm = { name: 'ECDH', namedCurve: (_a = options === null || options === void 0 ? void 0 : options.crv) !== null && _a !== void 0 ? _a : 'P-256' }; + keyUsages = ['deriveKey', 'deriveBits']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + return (crypto.subtle.generateKey(algorithm, (_b = options === null || options === void 0 ? void 0 : options.extractable) !== null && _b !== void 0 ? _b : false, keyUsages)); +} diff --git a/internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js b/internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js new file mode 100644 index 00000000..197904db --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/get_sign_verify_key.js @@ -0,0 +1,18 @@ +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkSigCryptoKey} from '../lib/crypto_key.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {types} from './is_key_like.js'; + +export default function getCryptoKey(alg, key, usage) { + if (isCryptoKey(key)) { + checkSigCryptoKey(key, alg, usage); + return key; + } + if (key instanceof Uint8Array) { + if (!alg.startsWith('HS')) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]); + } + throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array')); +} diff --git a/internal/server/web/static/js/lib/jose/runtime/is_key_like.js b/internal/server/web/static/js/lib/jose/runtime/is_key_like.js new file mode 100644 index 00000000..87192e9a --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/is_key_like.js @@ -0,0 +1,6 @@ +import {isCryptoKey} from './webcrypto.js'; + +export default (key) => { + return isCryptoKey(key); +}; +export const types = ['CryptoKey']; diff --git a/internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js b/internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js new file mode 100644 index 00000000..626f4840 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/jwk_to_key.js @@ -0,0 +1,136 @@ +import {isCloudflareWorkers} from './env.js'; +import crypto from './webcrypto.js'; +import {JOSENotSupported} from '../util/errors.js'; +import {decode as base64url} from './base64url.js'; + +function subtleMapping(jwk) { + let algorithm; + let keyUsages; + switch (jwk.kty) { + case 'oct': { + switch (jwk.alg) { + case 'HS256': + case 'HS384': + case 'HS512': + algorithm = { name: 'HMAC', hash: `SHA-${jwk.alg.slice(-3)}` }; + keyUsages = ['sign', 'verify']; + break; + case 'A128CBC-HS256': + case 'A192CBC-HS384': + case 'A256CBC-HS512': + throw new JOSENotSupported(`${jwk.alg} keys cannot be imported as CryptoKey instances`); + case 'A128GCM': + case 'A192GCM': + case 'A256GCM': + case 'A128GCMKW': + case 'A192GCMKW': + case 'A256GCMKW': + algorithm = { name: 'AES-GCM' }; + keyUsages = ['encrypt', 'decrypt']; + break; + case 'A128KW': + case 'A192KW': + case 'A256KW': + algorithm = { name: 'AES-KW' }; + keyUsages = ['wrapKey', 'unwrapKey']; + break; + case 'PBES2-HS256+A128KW': + case 'PBES2-HS384+A192KW': + case 'PBES2-HS512+A256KW': + algorithm = { name: 'PBKDF2' }; + keyUsages = ['deriveBits']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + break; + } + case 'RSA': { + switch (jwk.alg) { + case 'PS256': + case 'PS384': + case 'PS512': + algorithm = { name: 'RSA-PSS', hash: `SHA-${jwk.alg.slice(-3)}` }; + keyUsages = jwk.d ? ['sign'] : ['verify']; + break; + case 'RS256': + case 'RS384': + case 'RS512': + algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${jwk.alg.slice(-3)}` }; + keyUsages = jwk.d ? ['sign'] : ['verify']; + break; + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': + algorithm = { + name: 'RSA-OAEP', + hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`, + }; + keyUsages = jwk.d ? ['decrypt', 'unwrapKey'] : ['encrypt', 'wrapKey']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + break; + } + case 'EC': { + switch (jwk.alg) { + case 'ES256': + algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; + keyUsages = jwk.d ? ['sign'] : ['verify']; + break; + case 'ES384': + algorithm = { name: 'ECDSA', namedCurve: 'P-384' }; + keyUsages = jwk.d ? ['sign'] : ['verify']; + break; + case 'ES512': + algorithm = { name: 'ECDSA', namedCurve: 'P-521' }; + keyUsages = jwk.d ? ['sign'] : ['verify']; + break; + case 'ECDH-ES': + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': + algorithm = { name: 'ECDH', namedCurve: jwk.crv }; + keyUsages = jwk.d ? ['deriveBits'] : []; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + break; + } + case isCloudflareWorkers() && 'OKP': + if (jwk.alg !== 'EdDSA') { + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + switch (jwk.crv) { + case 'Ed25519': + algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' }; + keyUsages = jwk.d ? ['sign'] : ['verify']; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "crv" (Subtype of Key Pair) Parameter value'); + } + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value'); + } + return { algorithm, keyUsages }; +} +const parse = async (jwk) => { + var _a, _b; + const { algorithm, keyUsages } = subtleMapping(jwk); + const rest = [ + algorithm, + (_a = jwk.ext) !== null && _a !== void 0 ? _a : false, + (_b = jwk.key_ops) !== null && _b !== void 0 ? _b : keyUsages, + ]; + if (algorithm.name === 'PBKDF2') { + return crypto.subtle.importKey('raw', base64url(jwk.k), ...rest); + } + const keyData = { ...jwk }; + delete keyData.alg; + return crypto.subtle.importKey('jwk', keyData, ...rest); +}; +export default parse; diff --git a/internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js b/internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js new file mode 100644 index 00000000..549fd13b --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/key_to_jwk.js @@ -0,0 +1,22 @@ +import crypto, {isCryptoKey} from './webcrypto.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {encode as base64url} from './base64url.js'; +import {types} from './is_key_like.js'; + +const keyToJWK = async (key) => { + if (key instanceof Uint8Array) { + return { + kty: 'oct', + k: base64url(key), + }; + } + if (!isCryptoKey(key)) { + throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array')); + } + if (!key.extractable) { + throw new TypeError('non-extractable CryptoKey cannot be exported as a JWK'); + } + const { ext, key_ops, alg, use, ...jwk } = await crypto.subtle.exportKey('jwk', key); + return jwk; +}; +export default keyToJWK; diff --git a/internal/server/web/static/js/lib/jose/runtime/pbes2kw.js b/internal/server/web/static/js/lib/jose/runtime/pbes2kw.js new file mode 100644 index 00000000..e05d40b2 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/pbes2kw.js @@ -0,0 +1,52 @@ +import random from './random.js'; +import {p2s as concatSalt} from '../lib/buffer_utils.js'; +import {encode as base64url} from './base64url.js'; +import {unwrap, wrap} from './aeskw.js'; +import checkP2s from '../lib/check_p2s.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkEncCryptoKey} from '../lib/crypto_key.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {types} from './is_key_like.js'; + +function getCryptoKey(key, alg) { + if (key instanceof Uint8Array) { + return crypto.subtle.importKey('raw', key, 'PBKDF2', false, ['deriveBits']); + } + if (isCryptoKey(key)) { + checkEncCryptoKey(key, alg, 'deriveBits', 'deriveKey'); + return key; + } + throw new TypeError(invalidKeyInput(key, ...types, 'Uint8Array')); +} +async function deriveKey(p2s, alg, p2c, key) { + checkP2s(p2s); + const salt = concatSalt(alg, p2s); + const keylen = parseInt(alg.slice(13, 16), 10); + const subtleAlg = { + hash: `SHA-${alg.slice(8, 11)}`, + iterations: p2c, + name: 'PBKDF2', + salt, + }; + const wrapAlg = { + length: keylen, + name: 'AES-KW', + }; + const cryptoKey = await getCryptoKey(key, alg); + if (cryptoKey.usages.includes('deriveBits')) { + return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen)); + } + if (cryptoKey.usages.includes('deriveKey')) { + return crypto.subtle.deriveKey(subtleAlg, cryptoKey, wrapAlg, false, ['wrapKey', 'unwrapKey']); + } + throw new TypeError('PBKDF2 key "usages" must include "deriveBits" or "deriveKey"'); +} +export const encrypt = async (alg, key, cek, p2c = 2048, p2s = random(new Uint8Array(16))) => { + const derived = await deriveKey(p2s, alg, p2c, key); + const encryptedKey = await wrap(alg.slice(-6), derived, cek); + return { encryptedKey, p2c, p2s: base64url(p2s) }; +}; +export const decrypt = async (alg, key, encryptedKey, p2c, p2s) => { + const derived = await deriveKey(p2s, alg, p2c, key); + return unwrap(alg.slice(-6), derived, encryptedKey); +}; diff --git a/internal/server/web/static/js/lib/jose/runtime/random.js b/internal/server/web/static/js/lib/jose/runtime/random.js new file mode 100644 index 00000000..7082da00 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/random.js @@ -0,0 +1,3 @@ +import crypto from './webcrypto.js'; + +export default crypto.getRandomValues.bind(crypto); diff --git a/internal/server/web/static/js/lib/jose/runtime/rsaes.js b/internal/server/web/static/js/lib/jose/runtime/rsaes.js new file mode 100644 index 00000000..a81f7af8 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/rsaes.js @@ -0,0 +1,38 @@ +import subtleAlgorithm from './subtle_rsaes.js'; +import bogusWebCrypto from './bogus.js'; +import crypto, {isCryptoKey} from './webcrypto.js'; +import {checkEncCryptoKey} from '../lib/crypto_key.js'; +import checkKeyLength from './check_key_length.js'; +import invalidKeyInput from '../lib/invalid_key_input.js'; +import {types} from './is_key_like.js'; + +export const encrypt = async (alg, key, cek) => { + if (!isCryptoKey(key)) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + checkEncCryptoKey(key, alg, 'encrypt', 'wrapKey'); + checkKeyLength(alg, key); + if (key.usages.includes('encrypt')) { + return new Uint8Array(await crypto.subtle.encrypt(subtleAlgorithm(alg), key, cek)); + } + if (key.usages.includes('wrapKey')) { + const cryptoKeyCek = await crypto.subtle.importKey('raw', cek, ...bogusWebCrypto); + return new Uint8Array(await crypto.subtle.wrapKey('raw', cryptoKeyCek, key, subtleAlgorithm(alg))); + } + throw new TypeError('RSA-OAEP key "usages" must include "encrypt" or "wrapKey" for this operation'); +}; +export const decrypt = async (alg, key, encryptedKey) => { + if (!isCryptoKey(key)) { + throw new TypeError(invalidKeyInput(key, ...types)); + } + checkEncCryptoKey(key, alg, 'decrypt', 'unwrapKey'); + checkKeyLength(alg, key); + if (key.usages.includes('decrypt')) { + return new Uint8Array(await crypto.subtle.decrypt(subtleAlgorithm(alg), key, encryptedKey)); + } + if (key.usages.includes('unwrapKey')) { + const cryptoKeyCek = await crypto.subtle.unwrapKey('raw', encryptedKey, key, subtleAlgorithm(alg), ...bogusWebCrypto); + return new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKeyCek)); + } + throw new TypeError('RSA-OAEP key "usages" must include "decrypt" or "unwrapKey" for this operation'); +}; diff --git a/internal/server/web/static/js/lib/jose/runtime/sign.js b/internal/server/web/static/js/lib/jose/runtime/sign.js new file mode 100644 index 00000000..3ac57c36 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/sign.js @@ -0,0 +1,12 @@ +import subtleAlgorithm from './subtle_dsa.js'; +import crypto from './webcrypto.js'; +import checkKeyLength from './check_key_length.js'; +import getSignKey from './get_sign_verify_key.js'; + +const sign = async (alg, key, data) => { + const cryptoKey = await getSignKey(alg, key, 'sign'); + checkKeyLength(alg, cryptoKey); + const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data); + return new Uint8Array(signature); +}; +export default sign; diff --git a/internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js b/internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js new file mode 100644 index 00000000..14ee42e4 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/subtle_dsa.js @@ -0,0 +1,29 @@ +import {isCloudflareWorkers} from './env.js'; +import {JOSENotSupported} from '../util/errors.js'; + +export default function subtleDsa(alg, algorithm) { + const hash = `SHA-${alg.slice(-3)}`; + switch (alg) { + case 'HS256': + case 'HS384': + case 'HS512': + return { hash, name: 'HMAC' }; + case 'PS256': + case 'PS384': + case 'PS512': + return { hash, name: 'RSA-PSS', saltLength: alg.slice(-3) >> 3 }; + case 'RS256': + case 'RS384': + case 'RS512': + return { hash, name: 'RSASSA-PKCS1-v1_5' }; + case 'ES256': + case 'ES384': + case 'ES512': + return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve }; + case isCloudflareWorkers() && 'EdDSA': + const { namedCurve } = algorithm; + return { name: namedCurve, namedCurve }; + default: + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + } +} diff --git a/internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js b/internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js new file mode 100644 index 00000000..dbf870dd --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/subtle_rsaes.js @@ -0,0 +1,13 @@ +import {JOSENotSupported} from '../util/errors.js'; + +export default function subtleRsaEs(alg) { + switch (alg) { + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': + return 'RSA-OAEP'; + default: + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + } +} diff --git a/internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js b/internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js new file mode 100644 index 00000000..442cdccd --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/timing_safe_equal.js @@ -0,0 +1,19 @@ +const timingSafeEqual = (a, b) => { + if (!(a instanceof Uint8Array)) { + throw new TypeError('First argument must be a buffer'); + } + if (!(b instanceof Uint8Array)) { + throw new TypeError('Second argument must be a buffer'); + } + if (a.length !== b.length) { + throw new TypeError('Input buffers must have the same length'); + } + const len = a.length; + let out = 0; + let i = -1; + while (++i < len) { + out |= a[i] ^ b[i]; + } + return out === 0; +}; +export default timingSafeEqual; diff --git a/internal/server/web/static/js/lib/jose/runtime/verify.js b/internal/server/web/static/js/lib/jose/runtime/verify.js new file mode 100644 index 00000000..982340e5 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/verify.js @@ -0,0 +1,17 @@ +import subtleAlgorithm from './subtle_dsa.js'; +import crypto from './webcrypto.js'; +import checkKeyLength from './check_key_length.js'; +import getVerifyKey from './get_sign_verify_key.js'; + +const verify = async (alg, key, signature, data) => { + const cryptoKey = await getVerifyKey(alg, key, 'verify'); + checkKeyLength(alg, cryptoKey); + const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm); + try { + return await crypto.subtle.verify(algorithm, cryptoKey, signature, data); + } + catch (_a) { + return false; + } +}; +export default verify; diff --git a/internal/server/web/static/js/lib/jose/runtime/webcrypto.js b/internal/server/web/static/js/lib/jose/runtime/webcrypto.js new file mode 100644 index 00000000..f9e1e915 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/webcrypto.js @@ -0,0 +1,2 @@ +export default crypto; +export const isCryptoKey = (key) => key instanceof CryptoKey; diff --git a/internal/server/web/static/js/lib/jose/runtime/zlib.js b/internal/server/web/static/js/lib/jose/runtime/zlib.js new file mode 100644 index 00000000..d912bdb7 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/runtime/zlib.js @@ -0,0 +1,8 @@ +import {JOSENotSupported} from '../util/errors.js'; + +export const inflate = async () => { + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported by your javascript runtime. You need to use the `inflateRaw` decrypt option to provide Inflate Raw implementation.'); +}; +export const deflate = async () => { + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported by your javascript runtime. You need to use the `deflateRaw` encrypt option to provide Deflate Raw implementation.'); +}; diff --git a/internal/server/web/static/js/lib/jose/util/base64url.js b/internal/server/web/static/js/lib/jose/util/base64url.js new file mode 100644 index 00000000..d28523e8 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/util/base64url.js @@ -0,0 +1,4 @@ +import * as base64url from '../runtime/base64url.js'; + +export const encode = base64url.encode; +export const decode = base64url.decode; diff --git a/internal/server/web/static/js/lib/jose/util/decode_jwt.js b/internal/server/web/static/js/lib/jose/util/decode_jwt.js new file mode 100644 index 00000000..09741c36 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/util/decode_jwt.js @@ -0,0 +1,33 @@ +import {decode as base64url} from './base64url.js'; +import {decoder} from '../lib/buffer_utils.js'; +import isObject from '../lib/is_object.js'; +import {JWTInvalid} from './errors.js'; + +export function decodeJwt(jwt) { + if (typeof jwt !== 'string') + throw new JWTInvalid('JWTs must use Compact JWS serialization, JWT must be a string'); + const { 1: payload, length } = jwt.split('.'); + if (length === 5) + throw new JWTInvalid('Only JWTs using Compact JWS serialization can be decoded'); + if (length !== 3) + throw new JWTInvalid('Invalid JWT'); + if (!payload) + throw new JWTInvalid('JWTs must contain a payload'); + let decoded; + try { + decoded = base64url(payload); + } + catch (_a) { + throw new JWTInvalid('Failed to parse the base64url encoded payload'); + } + let result; + try { + result = JSON.parse(decoder.decode(decoded)); + } + catch (_b) { + throw new JWTInvalid('Failed to parse the decoded payload as JSON'); + } + if (!isObject(result)) + throw new JWTInvalid('Invalid JWT Claims Set'); + return result; +} diff --git a/internal/server/web/static/js/lib/jose/util/decode_protected_header.js b/internal/server/web/static/js/lib/jose/util/decode_protected_header.js new file mode 100644 index 00000000..437d8209 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/util/decode_protected_header.js @@ -0,0 +1,35 @@ +import {decode as base64url} from './base64url.js'; +import {decoder} from '../lib/buffer_utils.js'; +import isObject from '../lib/is_object.js'; + +export function decodeProtectedHeader(token) { + let protectedB64u; + if (typeof token === 'string') { + const parts = token.split('.'); + if (parts.length === 3 || parts.length === 5) { + ; + [protectedB64u] = parts; + } + } + else if (typeof token === 'object' && token) { + if ('protected' in token) { + protectedB64u = token.protected; + } + else { + throw new TypeError('Token does not contain a Protected Header'); + } + } + try { + if (typeof protectedB64u !== 'string' || !protectedB64u) { + throw new Error(); + } + const result = JSON.parse(decoder.decode(base64url(protectedB64u))); + if (!isObject(result)) { + throw new Error(); + } + return result; + } + catch (_a) { + throw new TypeError('Invalid Token or Protected Header formatting'); + } +} diff --git a/internal/server/web/static/js/lib/jose/util/errors.js b/internal/server/web/static/js/lib/jose/util/errors.js new file mode 100644 index 00000000..777ce985 --- /dev/null +++ b/internal/server/web/static/js/lib/jose/util/errors.js @@ -0,0 +1,147 @@ +export class JOSEError extends Error { + constructor(message) { + var _a; + super(message); + this.code = 'ERR_JOSE_GENERIC'; + this.name = this.constructor.name; + (_a = Error.captureStackTrace) === null || _a === void 0 ? void 0 : _a.call(Error, this, this.constructor); + } + static get code() { + return 'ERR_JOSE_GENERIC'; + } +} +export class JWTClaimValidationFailed extends JOSEError { + constructor(message, claim = 'unspecified', reason = 'unspecified') { + super(message); + this.code = 'ERR_JWT_CLAIM_VALIDATION_FAILED'; + this.claim = claim; + this.reason = reason; + } + static get code() { + return 'ERR_JWT_CLAIM_VALIDATION_FAILED'; + } +} +export class JWTExpired extends JOSEError { + constructor(message, claim = 'unspecified', reason = 'unspecified') { + super(message); + this.code = 'ERR_JWT_EXPIRED'; + this.claim = claim; + this.reason = reason; + } + static get code() { + return 'ERR_JWT_EXPIRED'; + } +} +export class JOSEAlgNotAllowed extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JOSE_ALG_NOT_ALLOWED'; + } + static get code() { + return 'ERR_JOSE_ALG_NOT_ALLOWED'; + } +} +export class JOSENotSupported extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JOSE_NOT_SUPPORTED'; + } + static get code() { + return 'ERR_JOSE_NOT_SUPPORTED'; + } +} +export class JWEDecryptionFailed extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWE_DECRYPTION_FAILED'; + this.message = 'decryption operation failed'; + } + static get code() { + return 'ERR_JWE_DECRYPTION_FAILED'; + } +} +export class JWEInvalid extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWE_INVALID'; + } + static get code() { + return 'ERR_JWE_INVALID'; + } +} +export class JWSInvalid extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWS_INVALID'; + } + static get code() { + return 'ERR_JWS_INVALID'; + } +} +export class JWTInvalid extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWT_INVALID'; + } + static get code() { + return 'ERR_JWT_INVALID'; + } +} +export class JWKInvalid extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWK_INVALID'; + } + static get code() { + return 'ERR_JWK_INVALID'; + } +} +export class JWKSInvalid extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWKS_INVALID'; + } + static get code() { + return 'ERR_JWKS_INVALID'; + } +} +export class JWKSNoMatchingKey extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWKS_NO_MATCHING_KEY'; + this.message = 'no applicable key found in the JSON Web Key Set'; + } + static get code() { + return 'ERR_JWKS_NO_MATCHING_KEY'; + } +} +export class JWKSMultipleMatchingKeys extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; + this.message = 'multiple matching keys found in the JSON Web Key Set'; + } + static get code() { + return 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; + } +} +export class JWKSTimeout extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWKS_TIMEOUT'; + this.message = 'request timed out'; + } + static get code() { + return 'ERR_JWKS_TIMEOUT'; + } +} +export class JWSSignatureVerificationFailed extends JOSEError { + constructor() { + super(...arguments); + this.code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; + this.message = 'signature verification failed'; + } + static get code() { + return 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; + } +} diff --git a/internal/server/web/static/js/logged-in-utils.js b/internal/server/web/static/js/logged-in-utils.js new file mode 100644 index 00000000..c694f042 --- /dev/null +++ b/internal/server/web/static/js/logged-in-utils.js @@ -0,0 +1,9 @@ + +let clipboard = new ClipboardJS('.copier'); +clipboard.on('success', function (e) { + e.clearSelection(); + let el = $(e.trigger); + let originalText = el.attr('data-original-title'); + el.attr('data-original-title', 'Copied!').tooltip('show'); + el.attr('data-original-title', originalText); +}); diff --git a/internal/server/web/static/js/ssh.js b/internal/server/web/static/js/ssh.js index b2121812..40c74635 100644 --- a/internal/server/web/static/js/ssh.js +++ b/internal/server/web/static/js/ssh.js @@ -19,16 +19,6 @@ const $hostConfigDiv = $('#sshHostConfigDiv'); const $sshFollowInstructions = $('#follow-instructions'); const $noSSHKeyEntry = $('#noSSHKeyEntry') - -let clipboard = new ClipboardJS('.btn-copy'); -clipboard.on('success', function (e) { - e.clearSelection(); - let el = $(e.trigger); - let originalText = el.attr('data-original-title'); - el.attr('data-original-title', 'Copied!').tooltip('show'); - el.attr('data-original-title', originalText); -}); - $('#addModal').on('hidden.bs.modal', function () { window.clearInterval(intervalID); $sshResult.hideB(); diff --git a/internal/server/web/static/js/tokeninfo-status.js b/internal/server/web/static/js/tokeninfo-status.js new file mode 100644 index 00000000..2c6b58b5 --- /dev/null +++ b/internal/server/web/static/js/tokeninfo-status.js @@ -0,0 +1,123 @@ +import * as jose from './lib/jose/index.js'; + +const $tokeninfoBadgeName = $('#tokeninfo-token-name'); +const $tokeninfoBadgeTypeShort = $('#tokeninfo-token-type-short'); +const $tokeninfoBadgeTypeJWTValid = $('#tokeninfo-token-type-JWT-valid'); +const $tokeninfoBadgeTypeJWTInvalid = $('#tokeninfo-token-type-JWT-invalid'); +const $tokeninfoBadgeValid = $('#tokeninfo-token-valid'); +const $tokeninfoBadgeInvalid = $('#tokeninfo-token-invalid'); +const $tokeninfoBadgeMytokenIss = $('#tokeninfo-token-mytoken-iss'); +const $tokeninfoBadgeOIDCIss = $('#tokeninfo-token-oidc-iss'); +const $tokeninfoBadgeIat = $('#tokeninfo-token-iat'); +const $tokeninfoBadgeExp = $('#tokeninfo-token-exp'); +const $tokeninfoBadgeIatDate = $('#tokeninfo-token-iat-date'); +const $tokeninfoBadgeExpDate = $('#tokeninfo-token-exp-date'); + +const $tokeninfoTypeBadges = $('.tokeninfo-token-type'); + +async function update_tokeninfo() { + let token = $tokenInput.val(); + let payload = {}; + let tokeninfoEndpoint = storageGet('tokeninfo_endpoint'); + let jwksUri = storageGet('jwks_uri'); + try { + payload = jose.decodeJwt(token); + let mytokenIss = payload['iss']; + if (!mytokenIss.startsWith(window.location.href)) { + await $.ajax({ + type: "Get", + url: "/.well-known/mytoken-configuration", + success: function(res){ + tokeninfoEndpoint = res['tokeninfo_endpoint']; + jwksUri = res['jwks_uri']; + }, + error: function (errRes) { + $errorModalMsg.text(getErrorMessage(errRes)); + $errorModal.modal(); + } + }); + } + $tokeninfoTypeBadges.hideB(); + + let jwks = await $.ajax({url: jwksUri, type:"GET"}); + try { + const pubKey = await jose.importJWK(jwks['keys'][0]); + await jose.jwtVerify(token, pubKey); + $tokeninfoBadgeTypeJWTValid.showB(); + } catch (e) { + if (e instanceof jose.errors.JWSSignatureVerificationFailed) { + $tokeninfoBadgeTypeJWTInvalid.showB(); + } else { + throw e; + } + } + + } catch (e) { + if (e instanceof jose.errors.JWTInvalid) { + $tokeninfoTypeBadges.hideB(); + $tokeninfoBadgeTypeShort.showB(); + } else { + throw e; + } + } + try { + await $.ajax({ + type: "POST", + url: tokeninfoEndpoint, + data: JSON.stringify({ + 'action': 'introspect', + 'mytoken': token, + }), + dataType: "json", + contentType: "application/json", + success: function (res) { + payload = res['token']; + if (res['valid']) { + $tokeninfoBadgeValid.showB(); + $tokeninfoBadgeInvalid.hideB(); + } else { + $tokeninfoBadgeValid.hideB(); + $tokeninfoBadgeInvalid.showB(); + } + }, + error: function (errRes) { + $tokeninfoBadgeValid.hideB(); + $tokeninfoBadgeInvalid.hideB(); + if (errRes.responseJSON['error'] === 'insufficient_capabilities') { + $tokeninfoBadgeValid.showB(); + } else { + $tokeninfoBadgeInvalid.showB(); + } + } + }); + } catch(e) { + console.log(e); + } + + let oidcIss = payload['oidc_iss']; + let mytokenIss = payload['iss']; + let name = payload['name']; + $tokeninfoBadgeOIDCIss.text(oidcIss!==undefined?oidcIss:""); + $tokeninfoBadgeMytokenIss.text(mytokenIss!==undefined?mytokenIss:""); + $tokeninfoBadgeName.text(name!==undefined?name:""); + let exp = payload['exp']; + if (exp===undefined) { + $tokeninfoBadgeExp.hideB(); + } else { + $tokeninfoBadgeExpDate.text(formatTime(exp)); + $tokeninfoBadgeExp.showB(); + } + let iat = payload['iat']; + if (iat===undefined) { + $tokeninfoBadgeIat.hideB(); + } else { + $tokeninfoBadgeIatDate.text(formatTime(iat)); + $tokeninfoBadgeIat.showB(); + } +} + +$tokenInput.on('change', update_tokeninfo); + +$(function (){ + update_tokeninfo(); +}); \ No newline at end of file diff --git a/internal/server/web/static/js/tokeninfo.js b/internal/server/web/static/js/tokeninfo.js index 2e56518a..7b86acaf 100644 --- a/internal/server/web/static/js/tokeninfo.js +++ b/internal/server/web/static/js/tokeninfo.js @@ -1,3 +1,4 @@ +const $tokenInput = $('#tokeninfo-token'); function _tokeninfo(action, successFnc, errorFnc, token=undefined) { let data = { @@ -18,11 +19,10 @@ function _tokeninfo(action, successFnc, errorFnc, token=undefined) { }); } - -function getSessionTokenInfo(e) { +function getTokenInfo(e) { e.preventDefault(); - let msg = $('#session-token-info-msg'); - let copy = $('#session-copy'); + let msg = $('#tokeninfo-token-content'); + let copy = $('#info-copy'); _tokeninfo('introspect', function (res) { let token = res['token']; @@ -40,7 +40,7 @@ function getSessionTokenInfo(e) { msg.text(getErrorMessage(errRes)); msg.addClass('text-danger'); copy.removeClass('d-none'); - }) + }, $tokenInput.val()) return false; } @@ -53,7 +53,7 @@ function historyToHTML(events) { let tableEntries = []; events.forEach(function (event) { let comment = event['comment'] || ''; - let time = new Date(event['time']*1000).toLocaleString(); + let time = formatTime(event['time']); let agentIcons = userAgentToHTMLIcons(event['user_agent']); let entry = '<tr>' + '<td>'+event['event']+'</td>' + @@ -86,7 +86,7 @@ function _tokenTreeToHTML(tree, depth) { if (depth > 0) { name = arrowI.repeat(depth-1) + lastArrowI + name } - let time = new Date(token['created']*1000).toLocaleString(); + let time = formatTime(token['created']); let tableEntries = ['<tr>' + '<td>'+name+'</td>' + '<td>'+time+'</td>' + @@ -131,7 +131,7 @@ function getHistoryTokenInfo(e) { msg.text(getErrorMessage(errRes)); msg.addClass('text-danger'); copy.removeClass('d-none'); - }) + }, $tokenInput.val()) return false; } @@ -149,7 +149,7 @@ function getTreeTokenInfo(e) { msg.text(getErrorMessage(errRes)); msg.addClass('text-danger'); copy.removeClass('d-none'); - }) + }, $tokenInput.val()) return false; } @@ -182,11 +182,12 @@ function getListTokenInfo(e) { return false; } -$('#session-info-tab').on('shown.bs.tab', getSessionTokenInfo) -$('#session-reload').on('click', getSessionTokenInfo) +$('#info-tab').on('shown.bs.tab', getTokenInfo) +$('#info-reload').on('click', getTokenInfo) $('#history-tab').on('shown.bs.tab', getHistoryTokenInfo) $('#history-reload').on('click', getHistoryTokenInfo) $('#tree-tab').on('shown.bs.tab', getTreeTokenInfo) $('#tree-reload').on('click', getTreeTokenInfo) + $('#list-mts-tab').on('shown.bs.tab', getListTokenInfo) -$('#list-reload').on('click', getListTokenInfo) +$('#list-reload').on('click', getListTokenInfo) \ No newline at end of file diff --git a/internal/server/web/static/js/utils.js b/internal/server/web/static/js/utils.js index 6825a5f8..de9ac37e 100644 --- a/internal/server/web/static/js/utils.js +++ b/internal/server/web/static/js/utils.js @@ -96,4 +96,8 @@ function extractMaxScopesFromToken(token) { scopes.push(...s.split(' ')); } return scopes.filter(onlyUnique).join(" ") +} + +function formatTime(t) { + return new Date(t*1000).toLocaleString() } \ No newline at end of file -- GitLab