feat(js-sandbox): add extensive Web Crypto API support (#5791)
This commit is contained in:
parent
4e717d79a5
commit
a998d6c493
13 changed files with 3223 additions and 199 deletions
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"v": 10,
|
||||
"id": "cmi8s7e0b000alj0isau8jt3x",
|
||||
"v": 11,
|
||||
"name": "scripting-revamp-coll",
|
||||
"folders": [],
|
||||
"requests": [
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0oo0092qt0if5rvd2g4",
|
||||
"name": "json-response-test",
|
||||
"method": "POST",
|
||||
|
|
@ -31,10 +30,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz1_0e51a53b-8e08-4390-a2c8-bf4034623f78"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0093qt0ictgoxymy",
|
||||
"name": "html-response-test",
|
||||
"method": "GET",
|
||||
|
|
@ -60,10 +59,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz1_87685b90-47bb-4272-b9e3-78efc86ce298"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0094qt0ixbo9rqnw",
|
||||
"name": "environment-variables-test",
|
||||
"method": "GET",
|
||||
|
|
@ -82,10 +81,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz1_7e619d82-0e16-4a24-bc03-d070cd5f0621"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0095qt0ieogkxx1w",
|
||||
"name": "request-modification-test",
|
||||
"method": "GET",
|
||||
|
|
@ -118,10 +117,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_5d3ccef8-8ed9-45b4-b8da-a83127730147"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0096qt0i6wellfus",
|
||||
"name": "response-parsing-test",
|
||||
"method": "POST",
|
||||
|
|
@ -147,10 +146,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_d04535d4-ea26-40bf-be2b-e5fef0051b03"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0097qt0ia4wf0lej",
|
||||
"name": "request-variables-test",
|
||||
"method": "GET",
|
||||
|
|
@ -185,10 +184,10 @@
|
|||
}
|
||||
],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_29079e08-dc98-4332-87e6-12f86ca273a5"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0098qt0ii9fguj6e",
|
||||
"name": "info-context-test",
|
||||
"method": "GET",
|
||||
|
|
@ -207,10 +206,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_f95a19a0-fcaf-4aac-a0cc-80d103e0a500"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op0099qt0iamthw97r",
|
||||
"name": "pm-namespace-additional-test",
|
||||
"method": "GET",
|
||||
|
|
@ -229,10 +228,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_61a82bd3-0884-4b29-bb6e-0807c694e6dd"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op009aqt0inw3j6dq9",
|
||||
"name": "expectation-methods-test",
|
||||
"method": "POST",
|
||||
|
|
@ -251,10 +250,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_c7de9aae-5936-4fe7-9205-2823b560f8ad"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00chai1qt0inext01",
|
||||
"name": "chai-assertions-hopp-extended",
|
||||
"method": "POST",
|
||||
|
|
@ -273,10 +272,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz2_d4acd239-fd73-43e7-a96b-27f293b4f8ce"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00chai2qt0inext02",
|
||||
"name": "chai-assertions-pm-parity",
|
||||
"method": "POST",
|
||||
|
|
@ -295,10 +294,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_e33cf09a-d284-46ca-a394-c8033d5dde84"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00cookies01",
|
||||
"name": "cookie-assertions-test",
|
||||
"method": "GET",
|
||||
|
|
@ -324,10 +323,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_0bff5a56-b147-45f8-a8da-e5175eb940d9"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00schema01",
|
||||
"name": "json-schema-validation-test",
|
||||
"method": "POST",
|
||||
|
|
@ -346,10 +345,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_05e595b7-ff00-4ae8-b695-8957c1381387"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00charset01",
|
||||
"name": "charset-validation-test",
|
||||
"method": "GET",
|
||||
|
|
@ -368,10 +367,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_0e536fee-92a3-4131-8a67-a7fd69cd189f"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00jsonpath01",
|
||||
"name": "jsonpath-query-test",
|
||||
"method": "POST",
|
||||
|
|
@ -390,10 +389,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_db1401ea-7ef8-4838-a570-dc3782610050"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00envext01",
|
||||
"name": "environment-extensions-test",
|
||||
"method": "GET",
|
||||
|
|
@ -412,10 +411,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_b749ccf4-0efb-4543-b8c5-94a142d53876"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00respext01",
|
||||
"name": "response-extensions-test",
|
||||
"method": "POST",
|
||||
|
|
@ -434,10 +433,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_27dfe163-c152-46b4-b3ce-a90377b640f7"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00execext01",
|
||||
"name": "execution-context-test",
|
||||
"method": "GET",
|
||||
|
|
@ -456,10 +455,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_57859840-7e61-4114-b514-199ab51ba57e"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00bddassert01",
|
||||
"name": "bdd-response-assertions-test",
|
||||
"method": "POST",
|
||||
|
|
@ -485,10 +484,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz3_ec06bb6c-1857-4352-bdb5-24b349a51a09"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00includecontain01",
|
||||
"name": "include-contain-assertions-test",
|
||||
"method": "POST",
|
||||
|
|
@ -507,10 +506,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_11cc69ef-f13a-4d02-9c42-607bcd84054b"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00envunsetclear01",
|
||||
"name": "environment-unset-clear-test",
|
||||
"method": "GET",
|
||||
|
|
@ -529,10 +528,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_ab8af065-c323-4830-86b8-be5f8b8570a7"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00pmmutate01",
|
||||
"name": "pm-request-mutation-test",
|
||||
"method": "GET",
|
||||
|
|
@ -565,10 +564,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_f73dfa1a-8539-425b-a6af-0a4622aec733"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00urlmutate01",
|
||||
"name": "pm-url-property-mutations-test",
|
||||
"method": "GET",
|
||||
|
|
@ -587,10 +586,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_4ecd2c5d-acbe-4f77-987a-a6c257b7f825"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00unsupported01",
|
||||
"name": "unsupported-features-test",
|
||||
"method": "GET",
|
||||
|
|
@ -609,10 +608,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_d3265ddb-982b-4da6-9506-125b0657fa13"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00urlpropertylist01",
|
||||
"name": "url-propertylist-helpers-test",
|
||||
"method": "GET",
|
||||
|
|
@ -669,10 +668,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_d8fa5d58-1a76-420d-946e-5cb063fc65e3"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"name": "propertylist-advanced-methods-test",
|
||||
"method": "GET",
|
||||
"endpoint": "https://echo.hoppscotch.io/propertylist",
|
||||
|
|
@ -722,10 +721,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_8e31db5d-90ed-4bad-b92a-67976c476c35"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "advanced-response-methods-test",
|
||||
"name": "advanced-response-methods-test",
|
||||
"method": "POST",
|
||||
|
|
@ -751,10 +750,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_80b32834-2683-4a4a-a866-90a3a97c7471"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "advanced-chai-map-set-test",
|
||||
"name": "advanced-chai-map-set-test",
|
||||
"method": "GET",
|
||||
|
|
@ -773,10 +772,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz4_c10eecab-a890-4a1b-97bb-99ddaa9bca9c"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "cmfhzf0op00typecoer01",
|
||||
"name": "type-preservation-test",
|
||||
"method": "GET",
|
||||
|
|
@ -794,10 +793,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "type_preservation_ui_compat",
|
||||
"name": "type-preservation-ui-compatibility-test",
|
||||
"method": "POST",
|
||||
|
|
@ -816,10 +816,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s7dz5_94e03aa3-8d21-4bad-8d3f-e4a276e1667e"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-get-basic",
|
||||
"name": "hopp.fetch() - GET request basic",
|
||||
"method": "GET",
|
||||
|
|
@ -837,10 +837,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-post-json",
|
||||
"name": "hopp.fetch() - POST with JSON body",
|
||||
"method": "GET",
|
||||
|
|
@ -858,10 +859,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-404-error",
|
||||
"name": "hopp.fetch() - 404 error handling",
|
||||
"method": "GET",
|
||||
|
|
@ -879,10 +881,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-custom-headers",
|
||||
"name": "hopp.fetch() - Custom headers",
|
||||
"method": "GET",
|
||||
|
|
@ -900,10 +903,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-env-url",
|
||||
"name": "hopp.fetch() - Environment variable URL",
|
||||
"method": "GET",
|
||||
|
|
@ -921,10 +925,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-response-text",
|
||||
"name": "hopp.fetch() - Response text parsing",
|
||||
"method": "GET",
|
||||
|
|
@ -942,10 +947,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-http-methods",
|
||||
"name": "hopp.fetch() - HTTP methods (PUT, DELETE, PATCH)",
|
||||
"method": "GET",
|
||||
|
|
@ -963,10 +969,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-string-url",
|
||||
"name": "pm.sendRequest() - String URL format",
|
||||
"method": "GET",
|
||||
|
|
@ -984,10 +991,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-request-object",
|
||||
"name": "pm.sendRequest() - Request object format",
|
||||
"method": "GET",
|
||||
|
|
@ -1005,10 +1013,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-urlencoded",
|
||||
"name": "pm.sendRequest() - URL-encoded body",
|
||||
"method": "GET",
|
||||
|
|
@ -1026,10 +1035,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-response-format",
|
||||
"name": "pm.sendRequest() - Response format validation",
|
||||
"method": "GET",
|
||||
|
|
@ -1047,10 +1057,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-error-codes",
|
||||
"name": "pm.sendRequest() - HTTP error status codes",
|
||||
"method": "GET",
|
||||
|
|
@ -1069,10 +1080,10 @@
|
|||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"_ref_id": "req_mi8s89cl_01becae7-dca6-47ab-87e5-fb2df28fc393"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-env-integration",
|
||||
"name": "pm.sendRequest() - Environment variable integration",
|
||||
"method": "GET",
|
||||
|
|
@ -1090,10 +1101,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-store-response",
|
||||
"name": "pm.sendRequest() - Store response in environment",
|
||||
"method": "GET",
|
||||
|
|
@ -1111,10 +1123,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-rfc-object-headers",
|
||||
"name": "pm.sendRequest() - RFC pattern with object headers",
|
||||
"method": "GET",
|
||||
|
|
@ -1132,10 +1145,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-pm-interop",
|
||||
"name": "hopp.fetch() and pm.sendRequest() - Interoperability",
|
||||
"method": "GET",
|
||||
|
|
@ -1153,10 +1167,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-json-parsing",
|
||||
"name": "hopp.fetch() - JSON response parsing",
|
||||
"method": "GET",
|
||||
|
|
@ -1181,10 +1196,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-headers-access",
|
||||
"name": "hopp.fetch() - Response headers access",
|
||||
"method": "GET",
|
||||
|
|
@ -1209,10 +1225,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-formdata",
|
||||
"name": "pm.sendRequest() - FormData body mode",
|
||||
"method": "POST",
|
||||
|
|
@ -1230,10 +1247,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-json-parsing",
|
||||
"name": "pm.sendRequest() - JSON parsing method",
|
||||
"method": "POST",
|
||||
|
|
@ -1251,10 +1269,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-headers-extraction",
|
||||
"name": "pm.sendRequest() - Response headers extraction",
|
||||
"method": "GET",
|
||||
|
|
@ -1279,10 +1298,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-network-error",
|
||||
"name": "hopp.fetch() - Network error handling",
|
||||
"method": "GET",
|
||||
|
|
@ -1300,10 +1320,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-network-error",
|
||||
"name": "pm.sendRequest() - Network error callback",
|
||||
"method": "GET",
|
||||
|
|
@ -1321,10 +1342,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-sequential-requests",
|
||||
"name": "hopp.fetch() - Sequential requests chain",
|
||||
"method": "GET",
|
||||
|
|
@ -1342,10 +1364,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-nested",
|
||||
"name": "pm.sendRequest() - Nested requests",
|
||||
"method": "GET",
|
||||
|
|
@ -1363,10 +1386,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "fetch-binary-response",
|
||||
"name": "hopp.fetch() - Binary response (arrayBuffer)",
|
||||
"method": "GET",
|
||||
|
|
@ -1384,10 +1408,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "pm-sendrequest-empty-response",
|
||||
"name": "pm.sendRequest() - Empty response body (204)",
|
||||
"method": "DELETE",
|
||||
|
|
@ -1405,10 +1430,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "async_patterns_prereq",
|
||||
"name": "Async Patterns - Pre-Request",
|
||||
"method": "GET",
|
||||
|
|
@ -1426,10 +1452,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "async_patterns_test",
|
||||
"name": "Async Patterns - Test Script",
|
||||
"method": "GET",
|
||||
|
|
@ -1447,10 +1474,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "workflow_patterns",
|
||||
"name": "Workflow Patterns (Sequential, Parallel, Auth)",
|
||||
"method": "GET",
|
||||
|
|
@ -1468,10 +1496,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "error_handling_combined",
|
||||
"name": "Error Handling & Edge Cases",
|
||||
"method": "GET",
|
||||
|
|
@ -1489,10 +1518,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "large_payload_formdata",
|
||||
"name": "Large Payload & FormData",
|
||||
"method": "GET",
|
||||
|
|
@ -1510,10 +1540,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "get_methods_combined",
|
||||
"name": "GET Methods (Query, Headers, URL)",
|
||||
"method": "GET",
|
||||
|
|
@ -1531,10 +1562,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "post_methods_combined",
|
||||
"name": "POST Methods (JSON, URLEncoded, Binary)",
|
||||
"method": "GET",
|
||||
|
|
@ -1552,10 +1584,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "http_methods_combined",
|
||||
"name": "HTTP Methods (PUT, PATCH, DELETE)",
|
||||
"method": "GET",
|
||||
|
|
@ -1573,10 +1606,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "response_parsing_combined",
|
||||
"name": "Response Parsing (Headers, Status, Body)",
|
||||
"method": "GET",
|
||||
|
|
@ -1594,10 +1628,11 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "16",
|
||||
"v": "17",
|
||||
"id": "dynamic_url_construction",
|
||||
"name": "Dynamic URL Construction",
|
||||
"method": "GET",
|
||||
|
|
@ -1615,7 +1650,30 @@
|
|||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {}
|
||||
"responses": {},
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"v": "17",
|
||||
"id": "crypto_module_test",
|
||||
"name": "crypto-module-test",
|
||||
"method": "GET",
|
||||
"endpoint": "https://echo.hoppscotch.io",
|
||||
"params": [],
|
||||
"headers": [],
|
||||
"preRequestScript": "// Hoppscotch Sandbox Crypto Module Tests\n// NOTE: The sandbox crypto API accepts plain arrays instead of TypedArrays.\n// Results include both .length and .byteLength for compatibility.\n// @ts-expect-error comments suppress Monaco's Web Crypto type checking for inputs.\n\n// Test crypto.randomUUID()\nconst uuid = crypto.randomUUID()\npm.environment.set('test_uuid', uuid)\n\n// Test crypto.getRandomValues()\nconst randomBytes = new Array(16).fill(0)\n// @ts-expect-error - Sandbox accepts plain arrays, not just TypedArrays\ncrypto.getRandomValues(randomBytes)\npm.environment.set('random_bytes_length', randomBytes.length)\npm.environment.set('has_random_values', randomBytes.some(v => v !== 0))\n\n// Test crypto.subtle.digest() with SHA-256\nconst testData = [72, 101, 108, 108, 111] // 'Hello' as bytes\n// @ts-expect-error - Sandbox accepts plain arrays as data input\nconst hash = await crypto.subtle.digest('SHA-256', testData)\npm.environment.set('hash_length', hash.byteLength)\n\n// Test crypto.subtle.generateKey() and sign/verify with HMAC\nconst hmacKey = await crypto.subtle.generateKey(\n { name: 'HMAC', hash: 'SHA-256' },\n true,\n ['sign', 'verify']\n)\npm.environment.set('hmac_key_type', hmacKey.type)\n\n// @ts-expect-error - Sandbox accepts plain arrays as data input\nconst signature = await crypto.subtle.sign('HMAC', hmacKey, testData)\npm.environment.set('signature_length', signature.byteLength)\n\n// @ts-expect-error - Sandbox accepts plain arrays for signature and data\nconst isValid = await crypto.subtle.verify('HMAC', hmacKey, signature, testData)\npm.environment.set('signature_valid', isValid)\n\n// Test crypto.subtle.generateKey() with AES-GCM\nconst aesKey = await crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n true,\n ['encrypt', 'decrypt']\n)\npm.environment.set('aes_key_type', aesKey.type)\n\n// Test encrypt/decrypt with AES-GCM\nconst iv = new Array(12).fill(0).map((_, i) => i)\nconst plaintext = [83, 101, 99, 114, 101, 116] // 'Secret' as bytes\n\n// @ts-expect-error - Sandbox accepts plain arrays for iv and plaintext\nconst ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n aesKey,\n plaintext\n)\npm.environment.set('ciphertext_length', ciphertext.byteLength)\n\n// @ts-expect-error - Sandbox accepts plain arrays for iv and ciphertext\nconst decrypted = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: iv },\n aesKey,\n ciphertext\n)\n// Compare decrypted bytes to original plaintext\nlet decryptMatch = decrypted.length === plaintext.length\nif (decryptMatch) {\n for (let i = 0; i < plaintext.length; i++) {\n if (decrypted[i] !== plaintext[i]) { decryptMatch = false; break }\n }\n}\npm.environment.set('decrypted_matches', decryptMatch)\n\n// Test RSA-OAEP encryption/decryption\nconst rsaKeyPair = await crypto.subtle.generateKey(\n {\n name: 'RSA-OAEP',\n modulusLength: 2048,\n // @ts-expect-error - Sandbox accepts plain array for publicExponent\n publicExponent: [1, 0, 1],\n hash: 'SHA-256'\n },\n true,\n ['encrypt', 'decrypt']\n)\n\n// @ts-expect-error - Sandbox accepts plain arrays as data input\nconst rsaCiphertext = await crypto.subtle.encrypt(\n { name: 'RSA-OAEP' },\n rsaKeyPair.publicKey,\n testData\n)\npm.environment.set('rsa_ciphertext_length', rsaCiphertext.byteLength)\n\nconst rsaDecrypted = await crypto.subtle.decrypt(\n { name: 'RSA-OAEP' },\n rsaKeyPair.privateKey,\n rsaCiphertext\n)\n// Compare RSA decrypted bytes to original testData\nlet rsaMatch = rsaDecrypted.length === testData.length\nif (rsaMatch) {\n for (let i = 0; i < testData.length; i++) {\n if (rsaDecrypted[i] !== testData[i]) { rsaMatch = false; break }\n }\n}\npm.environment.set('rsa_decrypted_matches', rsaMatch)\n\n// Test PBKDF2 key derivation\n// @ts-expect-error - Sandbox accepts plain array as key data\nconst passwordKey = await crypto.subtle.importKey(\n 'raw',\n [112, 97, 115, 115, 119, 111, 114, 100], // 'password'\n { name: 'PBKDF2' },\n false,\n ['deriveKey']\n)\n\nconst derivedKey = await crypto.subtle.deriveKey(\n {\n name: 'PBKDF2',\n hash: 'SHA-256',\n // @ts-expect-error - Sandbox accepts plain array for salt\n salt: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],\n iterations: 1000\n },\n passwordKey,\n { name: 'AES-GCM', length: 256 },\n true,\n ['encrypt', 'decrypt']\n)\npm.environment.set('pbkdf2_key_type', derivedKey.type)\n",
|
||||
"testScript": "const getNum = (key) => {\n const v = pm.environment.get(key)\n return typeof v === 'number' ? v : parseInt(String(v), 10)\n}\n\nconst getBool = (key) => {\n const v = pm.environment.get(key)\n return String(v) === 'true'\n}\n\n// crypto.randomUUID() tests\npm.test('crypto.randomUUID() generates valid UUID v4 format', () => {\n const uuid = pm.environment.get('test_uuid')\n pm.expect(uuid).to.be.a('string')\n pm.expect(uuid.length).to.equal(36)\n \n // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\n const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n pm.expect(uuidPattern.test(uuid)).to.be.true\n})\n\n// crypto.getRandomValues() tests\npm.test('crypto.getRandomValues() fills array with random bytes', () => {\n pm.expect(getNum('random_bytes_length')).to.equal(16)\n pm.expect(getBool('has_random_values')).to.be.true\n})\n\n// crypto.subtle.digest() tests\npm.test('crypto.subtle.digest() produces SHA-256 hash', () => {\n pm.expect(getNum('hash_length')).to.equal(32) // SHA-256 = 32 bytes\n})\n\n// HMAC sign/verify tests\npm.test('crypto.subtle.generateKey() creates HMAC key', () => {\n pm.expect(pm.environment.get('hmac_key_type')).to.equal('secret')\n})\n\npm.test('crypto.subtle.sign() produces HMAC signature', () => {\n pm.expect(getNum('signature_length')).to.equal(32) // SHA-256 HMAC = 32 bytes\n})\n\npm.test('crypto.subtle.verify() validates HMAC signature', () => {\n pm.expect(getBool('signature_valid')).to.be.true\n})\n\n// AES-GCM encrypt/decrypt tests\npm.test('crypto.subtle.generateKey() creates AES-GCM key', () => {\n pm.expect(pm.environment.get('aes_key_type')).to.equal('secret')\n})\n\npm.test('crypto.subtle.encrypt() produces ciphertext', () => {\n pm.expect(getNum('ciphertext_length')).to.be.above(0)\n})\n\npm.test('crypto.subtle.decrypt() recovers original plaintext', () => {\n pm.expect(getBool('decrypted_matches')).to.be.true\n})\n\n// RSA-OAEP encrypt/decrypt tests\npm.test('crypto.subtle.encrypt() produces RSA ciphertext', () => {\n pm.expect(getNum('rsa_ciphertext_length')).to.be.above(0)\n})\n\npm.test('crypto.subtle.decrypt() recovers RSA plaintext', () => {\n pm.expect(getBool('rsa_decrypted_matches')).to.be.true\n})\n\n// PBKDF2 key derivation tests\npm.test('crypto.subtle.deriveKey() creates AES key from password', () => {\n pm.expect(pm.environment.get('pbkdf2_key_type')).to.equal('secret')\n})\n\n// Additional in-script crypto tests\npm.test('crypto.randomUUID() generates unique UUIDs', () => {\n const uuid1 = crypto.randomUUID()\n const uuid2 = crypto.randomUUID()\n pm.expect(uuid1).to.not.equal(uuid2)\n})\n\npm.test('crypto.getRandomValues() mutates array in place', () => {\n const arr = new Array(10).fill(0)\n // @ts-expect-error - Sandbox accepts plain arrays, not just TypedArrays\n const result = crypto.getRandomValues(arr)\n pm.expect(result).to.equal(arr)\n pm.expect(arr.some(v => v !== 0)).to.be.true\n})\n",
|
||||
"auth": {
|
||||
"authType": "inherit",
|
||||
"authActive": true
|
||||
},
|
||||
"body": {
|
||||
"contentType": null,
|
||||
"body": null
|
||||
},
|
||||
"requestVariables": [],
|
||||
"responses": {},
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
|
|
@ -1624,5 +1682,5 @@
|
|||
},
|
||||
"headers": [],
|
||||
"variables": [],
|
||||
"_ref_id": "coll_mi8sfgx8_4523effa-e775-4550-afb8-4ab5a4ef45ae"
|
||||
"description": null
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { FaradayCage } from "faraday-cage"
|
||||
import { customCryptoModule } from "../../../cage-modules"
|
||||
|
||||
const runCage = async (script: string) => {
|
||||
const cage = await FaradayCage.create()
|
||||
return cage.runCode(script, [
|
||||
customCryptoModule({
|
||||
cryptoImpl: globalThis.crypto,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
describe("crypto.subtle.encrypt/decrypt", () => {
|
||||
it("encrypts and decrypts with AES-GCM", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const data = [116, 101, 115, 116, 32, 100, 97, 116, 97] // "test data"
|
||||
const iv = [1,2,3,4,5,6,7,8,9,10,11,12]
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
if (!Array.isArray(encrypted) || encrypted.length === 0) {
|
||||
throw new Error('encrypted did not return a byte array')
|
||||
}
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
if (JSON.stringify(decrypted) !== JSON.stringify(data)) {
|
||||
throw new Error('decrypted bytes mismatch')
|
||||
}
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("encrypts and decrypts with AES-CBC", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-CBC', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const data = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] // "hello world"
|
||||
const iv = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-CBC', iv },
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv },
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
if (JSON.stringify(decrypted) !== JSON.stringify(data)) {
|
||||
throw new Error('decrypted bytes mismatch')
|
||||
}
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("handles empty data", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 128 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const data = []
|
||||
const iv = [1,2,3,4,5,6,7,8,9,10,11,12]
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
if (!Array.isArray(decrypted) || decrypted.length !== 0) {
|
||||
throw new Error('expected empty decrypted array')
|
||||
}
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("rejects decrypt with invalid key usage", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt'] // No decrypt usage
|
||||
)
|
||||
|
||||
const data = [116, 101, 115, 116]
|
||||
const iv = [1,2,3,4,5,6,7,8,9,10,11,12]
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted)
|
||||
} catch (_) {
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('expected decrypt to reject')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("rejects decrypt with wrong IV (AES-GCM)", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const data = [116, 101, 115, 116]
|
||||
const iv1 = [1,2,3,4,5,6,7,8,9,10,11,12]
|
||||
const iv2 = [12,11,10,9,8,7,6,5,4,3,2,1]
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: iv1 },
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
let rejected = false
|
||||
try {
|
||||
await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv2 }, key, encrypted)
|
||||
} catch (_) {
|
||||
rejected = true
|
||||
}
|
||||
if (!rejected) throw new Error('expected decrypt to reject with wrong IV')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("encrypts and decrypts with RSA-OAEP", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2048,
|
||||
publicExponent: [1, 0, 1],
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const data = [116, 101, 115, 116, 32, 100, 97, 116, 97] // "test data"
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
keyPair.publicKey,
|
||||
data
|
||||
)
|
||||
|
||||
if (!Array.isArray(encrypted) || encrypted.length === 0) {
|
||||
throw new Error('encrypted did not return a byte array')
|
||||
}
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
keyPair.privateKey,
|
||||
encrypted
|
||||
)
|
||||
|
||||
if (JSON.stringify(decrypted) !== JSON.stringify(data)) {
|
||||
throw new Error('decrypted bytes mismatch')
|
||||
}
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("encrypts and decrypts with AES-CTR", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'AES-CTR', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const data = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] // "hello world"
|
||||
const counter = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-CTR', counter: counter, length: 64 },
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-CTR', counter: counter, length: 64 },
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
if (JSON.stringify(decrypted) !== JSON.stringify(data)) {
|
||||
throw new Error('decrypted bytes mismatch')
|
||||
}
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,829 @@
|
|||
import { describe, expect, test } from "vitest"
|
||||
import { runTestWithEmptyEnv } from "~/utils/test-helpers"
|
||||
|
||||
/**
|
||||
* Tests for crypto.subtle key-based operations:
|
||||
* - generateKey
|
||||
* - importKey
|
||||
* - exportKey
|
||||
* - encrypt/decrypt
|
||||
* - sign/verify
|
||||
*/
|
||||
|
||||
describe("crypto.subtle.generateKey()", () => {
|
||||
test("should generate HMAC key for signing", () => {
|
||||
const script = `
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
hopp.test("HMAC key generated successfully", () => {
|
||||
hopp.expect(key).toBeType("object")
|
||||
hopp.expect(key.type).toBe("secret")
|
||||
hopp.expect(key.extractable).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "HMAC key generated successfully",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should generate AES-GCM key for encryption", () => {
|
||||
const script = `
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
hopp.test("AES-GCM key generated successfully", () => {
|
||||
hopp.expect(key).toBeType("object")
|
||||
hopp.expect(key.type).toBe("secret")
|
||||
hopp.expect(key.extractable).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "AES-GCM key generated successfully",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.subtle.importKey() and exportKey()", () => {
|
||||
test("should import raw HMAC key", () => {
|
||||
// Create a test key (32 bytes of data)
|
||||
const rawKeyBytes = Array.from({ length: 32 }, (_, i) => i)
|
||||
|
||||
const script = `
|
||||
const rawKeyData = ${JSON.stringify(rawKeyBytes)}
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
rawKeyData,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
hopp.test("Raw HMAC key imported successfully", () => {
|
||||
hopp.expect(key).toBeType("object")
|
||||
hopp.expect(key.type).toBe("secret")
|
||||
hopp.expect(key.extractable).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Raw HMAC key imported successfully",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should export key in raw format", () => {
|
||||
const rawKeyBytes = Array.from({ length: 32 }, (_, i) => i)
|
||||
|
||||
const script = `
|
||||
const rawKeyData = ${JSON.stringify(rawKeyBytes)}
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
rawKeyData,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
const exportedKey = await crypto.subtle.exportKey("raw", key)
|
||||
const exportedArray = Array.from(exportedKey)
|
||||
|
||||
hopp.test("Key exported successfully in raw format", () => {
|
||||
hopp.expect(exportedArray.length).toBe(32)
|
||||
hopp.expect(exportedArray[0]).toBe(0)
|
||||
hopp.expect(exportedArray[31]).toBe(31)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Key exported successfully in raw format",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should import and export key in JWK format", () => {
|
||||
const script = `
|
||||
// Generate a key first
|
||||
const generatedKey = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
// Export as JWK
|
||||
const jwk = await crypto.subtle.exportKey("jwk", generatedKey)
|
||||
|
||||
hopp.test("Key exported as JWK successfully", () => {
|
||||
hopp.expect(jwk).toBeType("object")
|
||||
hopp.expect(jwk.kty).toBe("oct")
|
||||
hopp.expect(jwk.k).toBeType("string")
|
||||
hopp.expect(jwk.alg).toBe("A256GCM")
|
||||
})
|
||||
|
||||
// Re-import the JWK
|
||||
const reimportedKey = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
jwk,
|
||||
{ name: "AES-GCM" },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
hopp.test("JWK reimported successfully", () => {
|
||||
hopp.expect(reimportedKey).toBeType("object")
|
||||
hopp.expect(reimportedKey.type).toBe("secret")
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Key exported as JWK successfully",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "JWK reimported successfully",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.subtle.encrypt() and decrypt()", () => {
|
||||
test("should encrypt and decrypt with AES-GCM", () => {
|
||||
const plainTextBytes = Array.from(new TextEncoder().encode("Hello, World!"))
|
||||
const ivBytes = Array.from({ length: 12 }, (_, i) => i) // 12-byte IV for AES-GCM
|
||||
|
||||
const script = `
|
||||
const plainText = ${JSON.stringify(plainTextBytes)}
|
||||
const iv = ${JSON.stringify(ivBytes)}
|
||||
|
||||
// Generate an AES-GCM key
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
// Encrypt the plaintext
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
key,
|
||||
plainText
|
||||
)
|
||||
|
||||
hopp.test("Encryption produces ciphertext", () => {
|
||||
hopp.expect(ciphertext.length).toBeType("number")
|
||||
hopp.expect(ciphertext.length > 0).toBe(true)
|
||||
})
|
||||
|
||||
// Decrypt the ciphertext
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
key,
|
||||
ciphertext
|
||||
)
|
||||
|
||||
const decryptedText = new TextDecoder().decode(new Uint8Array(decrypted))
|
||||
|
||||
hopp.test("Decryption recovers original plaintext", () => {
|
||||
hopp.expect(decryptedText).toBe("Hello, World!")
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Encryption produces ciphertext",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Decryption recovers original plaintext",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("encrypted data differs from plaintext", () => {
|
||||
const plainTextBytes = Array.from(
|
||||
new TextEncoder().encode("Secret message")
|
||||
)
|
||||
const ivBytes = Array.from({ length: 12 }, (_, i) => i + 100)
|
||||
|
||||
const script = `
|
||||
const plainText = ${JSON.stringify(plainTextBytes)}
|
||||
const iv = ${JSON.stringify(ivBytes)}
|
||||
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
key,
|
||||
plainText
|
||||
)
|
||||
|
||||
// Compare ciphertext to plaintext - they should be different
|
||||
const ciphertextArray = Array.from(ciphertext)
|
||||
const isDifferent = plainText.some((byte, i) => ciphertextArray[i] !== byte)
|
||||
|
||||
hopp.test("Ciphertext differs from plaintext", () => {
|
||||
hopp.expect(isDifferent).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Ciphertext differs from plaintext",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.subtle.sign() and verify()", () => {
|
||||
test("should sign and verify with HMAC", () => {
|
||||
const dataBytes = Array.from(new TextEncoder().encode("Data to sign"))
|
||||
|
||||
const script = `
|
||||
const data = ${JSON.stringify(dataBytes)}
|
||||
|
||||
// Generate HMAC key
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
// Sign the data
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key,
|
||||
data
|
||||
)
|
||||
|
||||
hopp.test("Signature is produced", () => {
|
||||
hopp.expect(signature.length).toBe(32) // SHA-256 produces 32-byte signatures
|
||||
hopp.expect(signature.byteLength).toBe(32)
|
||||
})
|
||||
|
||||
// Verify the signature
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"HMAC",
|
||||
key,
|
||||
signature,
|
||||
data
|
||||
)
|
||||
|
||||
hopp.test("Signature verification succeeds", () => {
|
||||
hopp.expect(isValid).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Signature is produced",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Signature verification succeeds",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("verification fails with tampered data", () => {
|
||||
const dataBytes = Array.from(new TextEncoder().encode("Original data"))
|
||||
const tamperedDataBytes = Array.from(
|
||||
new TextEncoder().encode("Tampered data")
|
||||
)
|
||||
|
||||
const script = `
|
||||
const originalData = ${JSON.stringify(dataBytes)}
|
||||
const tamperedData = ${JSON.stringify(tamperedDataBytes)}
|
||||
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
// Sign the original data
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key,
|
||||
originalData
|
||||
)
|
||||
|
||||
// Try to verify with tampered data
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"HMAC",
|
||||
key,
|
||||
signature,
|
||||
tamperedData
|
||||
)
|
||||
|
||||
hopp.test("Verification fails with tampered data", () => {
|
||||
hopp.expect(isValid).toBe(false)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Verification fails with tampered data",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("verification fails with wrong key", () => {
|
||||
const dataBytes = Array.from(new TextEncoder().encode("Protected data"))
|
||||
|
||||
const script = `
|
||||
const data = ${JSON.stringify(dataBytes)}
|
||||
|
||||
// Generate two different keys
|
||||
const key1 = await crypto.subtle.generateKey(
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
const key2 = await crypto.subtle.generateKey(
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
true,
|
||||
["sign", "verify"]
|
||||
)
|
||||
|
||||
// Sign with key1
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key1,
|
||||
data
|
||||
)
|
||||
|
||||
// Try to verify with key2
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"HMAC",
|
||||
key2,
|
||||
signature,
|
||||
data
|
||||
)
|
||||
|
||||
hopp.test("Verification fails with wrong key", () => {
|
||||
hopp.expect(isValid).toBe(false)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Verification fails with wrong key",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.getRandomValues() validation", () => {
|
||||
test("should reject arrays exceeding 65536 bytes", () => {
|
||||
const script = `
|
||||
let errorThrown = false
|
||||
let errorMessage = ""
|
||||
|
||||
try {
|
||||
const largeArray = new Array(65537).fill(0)
|
||||
crypto.getRandomValues(largeArray)
|
||||
} catch (e) {
|
||||
errorThrown = true
|
||||
errorMessage = e.message
|
||||
}
|
||||
|
||||
hopp.test("Throws error for oversized array", () => {
|
||||
hopp.expect(errorThrown).toBe(true)
|
||||
hopp.expect(errorMessage).toInclude("65536")
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Throws error for oversized array",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should accept array at exactly 65536 bytes", () => {
|
||||
const script = `
|
||||
const maxArray = new Array(65536).fill(0)
|
||||
const result = crypto.getRandomValues(maxArray)
|
||||
|
||||
hopp.test("Accepts 65536-byte array", () => {
|
||||
hopp.expect(result.length).toBe(65536)
|
||||
// Check that at least some values were filled
|
||||
const hasNonZero = result.some(v => v !== 0)
|
||||
hopp.expect(hasNonZero).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Accepts 65536-byte array",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.subtle.deriveBits() and deriveKey()", () => {
|
||||
test("should derive bits using PBKDF2", () => {
|
||||
const script = `
|
||||
// Import a password as key material
|
||||
const passwordKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
[112, 97, 115, 115, 119, 111, 114, 100], // "password"
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveBits"]
|
||||
)
|
||||
|
||||
// Derive 256 bits
|
||||
const derivedBits = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-256",
|
||||
salt: new Array(16).fill(1), // 16-byte salt
|
||||
iterations: 1000
|
||||
},
|
||||
passwordKey,
|
||||
256
|
||||
)
|
||||
|
||||
hopp.test("PBKDF2 deriveBits produces correct length", () => {
|
||||
hopp.expect(derivedBits.length).toBe(32) // 256 bits = 32 bytes
|
||||
// Check that we got non-zero data
|
||||
hopp.expect(derivedBits.some(b => b !== 0)).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "PBKDF2 deriveBits produces correct length",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should derive key using PBKDF2", () => {
|
||||
const script = `
|
||||
// Import password as key material
|
||||
const passwordKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
[109, 121, 112, 97, 115, 115], // "mypass"
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveKey"]
|
||||
)
|
||||
|
||||
// Derive an AES-GCM key
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-256",
|
||||
salt: new Array(16).fill(2),
|
||||
iterations: 1000
|
||||
},
|
||||
passwordKey,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
hopp.test("PBKDF2 deriveKey produces usable AES key", () => {
|
||||
hopp.expect(derivedKey).toBeType("object")
|
||||
hopp.expect(derivedKey.type).toBe("secret")
|
||||
hopp.expect(derivedKey.extractable).toBe(true)
|
||||
})
|
||||
|
||||
// Test that the derived key actually works
|
||||
const testData = [116, 101, 115, 116] // "test"
|
||||
const iv = new Array(12).fill(0)
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
derivedKey,
|
||||
testData
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
derivedKey,
|
||||
encrypted
|
||||
)
|
||||
|
||||
hopp.test("Derived key can encrypt and decrypt", () => {
|
||||
hopp.expect(JSON.stringify(decrypted)).toBe(JSON.stringify(testData))
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "PBKDF2 deriveKey produces usable AES key",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Derived key can encrypt and decrypt",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.subtle.wrapKey() and unwrapKey()", () => {
|
||||
test("should wrap and unwrap AES key with AES-KW", () => {
|
||||
const script = `
|
||||
// Generate keys
|
||||
const aesKey = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
const wrappingKey = await crypto.subtle.generateKey(
|
||||
{ name: "AES-KW", length: 256 },
|
||||
true,
|
||||
["wrapKey", "unwrapKey"]
|
||||
)
|
||||
|
||||
// Wrap the AES key
|
||||
const wrappedKey = await crypto.subtle.wrapKey(
|
||||
"raw",
|
||||
aesKey,
|
||||
wrappingKey,
|
||||
"AES-KW"
|
||||
)
|
||||
|
||||
hopp.test("Key wrapping produces wrapped data", () => {
|
||||
hopp.expect(wrappedKey).toBeType("object")
|
||||
hopp.expect(wrappedKey.length).toBe(40) // AES-256 key (32) + padding (8)
|
||||
})
|
||||
|
||||
// Unwrap the key
|
||||
const unwrappedKey = await crypto.subtle.unwrapKey(
|
||||
"raw",
|
||||
wrappedKey,
|
||||
wrappingKey,
|
||||
"AES-KW",
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
hopp.test("Unwrapped key is valid", () => {
|
||||
hopp.expect(unwrappedKey).toBeType("object")
|
||||
hopp.expect(unwrappedKey.type).toBe("secret")
|
||||
hopp.expect(unwrappedKey.extractable).toBe(true)
|
||||
})
|
||||
|
||||
// Test that unwrapped key works
|
||||
const testData = [117, 110, 119, 114, 97, 112] // "unwrap"
|
||||
const iv = new Array(12).fill(3)
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
unwrappedKey,
|
||||
testData
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
unwrappedKey,
|
||||
encrypted
|
||||
)
|
||||
|
||||
hopp.test("Unwrapped key functions correctly", () => {
|
||||
hopp.expect(JSON.stringify(decrypted)).toBe(JSON.stringify(testData))
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Key wrapping produces wrapped data",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Unwrapped key is valid",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Unwrapped key functions correctly",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should wrap and unwrap key with JWK format", () => {
|
||||
const script = `
|
||||
// Generate an AES key
|
||||
const aesKey = await crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 128 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
// Export to JWK for comparison
|
||||
const originalJwk = await crypto.subtle.exportKey("jwk", aesKey)
|
||||
|
||||
// Generate wrapping key
|
||||
const wrappingKey = await crypto.subtle.generateKey(
|
||||
{ name: "AES-KW", length: 128 },
|
||||
true,
|
||||
["wrapKey", "unwrapKey"]
|
||||
)
|
||||
|
||||
// Wrap the key as JWK
|
||||
const wrappedKey = await crypto.subtle.wrapKey(
|
||||
"jwk",
|
||||
aesKey,
|
||||
wrappingKey,
|
||||
"AES-KW"
|
||||
)
|
||||
|
||||
hopp.test("JWK wrapping produces data", () => {
|
||||
hopp.expect(wrappedKey).toBeType("object")
|
||||
hopp.expect(wrappedKey.length > 0).toBe(true)
|
||||
})
|
||||
|
||||
// Unwrap the JWK
|
||||
const unwrappedKey = await crypto.subtle.unwrapKey(
|
||||
"jwk",
|
||||
wrappedKey,
|
||||
wrappingKey,
|
||||
"AES-KW",
|
||||
{ name: "AES-GCM" },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
hopp.test("Unwrapped JWK key is valid", () => {
|
||||
hopp.expect(unwrappedKey).toBeType("object")
|
||||
hopp.expect(unwrappedKey.type).toBe("secret")
|
||||
})
|
||||
|
||||
// Export unwrapped key and compare
|
||||
const unwrappedJwk = await crypto.subtle.exportKey("jwk", unwrappedKey)
|
||||
|
||||
hopp.test("Unwrapped key matches original", () => {
|
||||
hopp.expect(unwrappedJwk.kty).toBe(originalJwk.kty)
|
||||
hopp.expect(unwrappedJwk.alg).toBe(originalJwk.alg)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "JWK wrapping produces data",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Unwrapped JWK key is valid",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
descriptor: "Unwrapped key matches original",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import { describe, expect, test } from "vitest"
|
||||
import { FaradayCage } from "faraday-cage"
|
||||
import { customCryptoModule } from "~/cage-modules"
|
||||
import { runTestWithEmptyEnv } from "~/utils/test-helpers"
|
||||
|
||||
describe("crypto.getRandomValues()", () => {
|
||||
test("should generate random values for array", () => {
|
||||
const script = `
|
||||
const array = new Array(10).fill(0)
|
||||
const result = crypto.getRandomValues(array)
|
||||
|
||||
// Check that values were modified
|
||||
const hasNonZero = result.some(v => v !== 0)
|
||||
|
||||
hopp.test("Random values generated", () => {
|
||||
hopp.expect(result.length).toBe(10)
|
||||
hopp.expect(hasNonZero).toBe(true)
|
||||
hopp.expect(result).toBe(array) // Should mutate in place
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Random values generated",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should generate different values on multiple calls", () => {
|
||||
const script = `
|
||||
const array1 = new Array(32).fill(0)
|
||||
const array2 = new Array(32).fill(0)
|
||||
|
||||
crypto.getRandomValues(array1)
|
||||
crypto.getRandomValues(array2)
|
||||
|
||||
// Arrays should be different
|
||||
const isDifferent = array1.some((v, i) => v !== array2[i])
|
||||
|
||||
hopp.test("Random values are different", () => {
|
||||
hopp.expect(isDifferent).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Random values are different",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should handle different array sizes", () => {
|
||||
const script = `
|
||||
const sizes = [1, 16, 256]
|
||||
const results = []
|
||||
|
||||
for (const size of sizes) {
|
||||
const array = new Array(size).fill(0)
|
||||
crypto.getRandomValues(array)
|
||||
results.push({
|
||||
size,
|
||||
hasRandomValues: array.some(v => v !== 0),
|
||||
length: array.length
|
||||
})
|
||||
}
|
||||
|
||||
hopp.test("Handles various array sizes", () => {
|
||||
for (const result of results) {
|
||||
hopp.expect(result.length).toBe(result.size)
|
||||
hopp.expect(result.hasRandomValues).toBe(true)
|
||||
}
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Handles various array sizes",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should return values in valid byte range (0-255)", () => {
|
||||
const script = `
|
||||
const array = new Array(100).fill(0)
|
||||
crypto.getRandomValues(array)
|
||||
|
||||
const allInRange = array.every(v => v >= 0 && v <= 255)
|
||||
const hasVariety = new Set(array).size > 1
|
||||
|
||||
hopp.test("Values are valid bytes", () => {
|
||||
hopp.expect(allInRange).toBe(true)
|
||||
hopp.expect(hasVariety).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Values are valid bytes",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("crypto.randomUUID()", () => {
|
||||
test("should generate valid UUID v4 format", () => {
|
||||
const script = `
|
||||
const uuid = crypto.randomUUID()
|
||||
|
||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
|
||||
hopp.test("UUID format is valid", () => {
|
||||
hopp.expect(typeof uuid).toBe("string")
|
||||
hopp.expect(uuid.length).toBe(36)
|
||||
hopp.expect(uuidPattern.test(uuid)).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "UUID format is valid",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should generate unique UUIDs", () => {
|
||||
const script = `
|
||||
const uuids = []
|
||||
for (let i = 0; i < 100; i++) {
|
||||
uuids.push(crypto.randomUUID())
|
||||
}
|
||||
|
||||
const uniqueUuids = new Set(uuids)
|
||||
|
||||
hopp.test("UUIDs are unique", () => {
|
||||
hopp.expect(uniqueUuids.size).toBe(100)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "UUIDs are unique",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should generate UUIDs with correct version and variant", () => {
|
||||
const script = `
|
||||
const uuid = crypto.randomUUID()
|
||||
const parts = uuid.split('-')
|
||||
|
||||
// Version should be 4 (random)
|
||||
const version = parts[2][0]
|
||||
|
||||
// Variant should be 8, 9, a, or b (RFC 4122)
|
||||
const variant = parts[3][0].toLowerCase()
|
||||
|
||||
hopp.test("UUID version and variant are correct", () => {
|
||||
hopp.expect(version).toBe('4')
|
||||
hopp.expect(['8', '9', 'a', 'b'].includes(variant)).toBe(true)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "UUID version and variant are correct",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should still work when cryptoImpl.randomUUID is missing (polyfill)", async () => {
|
||||
const cage = await FaradayCage.create()
|
||||
|
||||
const cryptoImplWithoutRandomUUID = {
|
||||
getRandomValues: globalThis.crypto.getRandomValues.bind(
|
||||
globalThis.crypto
|
||||
),
|
||||
subtle: globalThis.crypto.subtle,
|
||||
} as unknown as Crypto
|
||||
|
||||
const script = `
|
||||
(async () => {
|
||||
const uuid = crypto.randomUUID()
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
if (typeof uuid !== 'string' || uuid.length !== 36) throw new Error('uuid shape invalid')
|
||||
if (!uuidPattern.test(uuid)) throw new Error('uuid format invalid')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await cage.runCode(script, [
|
||||
customCryptoModule({
|
||||
cryptoImpl: cryptoImplWithoutRandomUUID,
|
||||
}),
|
||||
])
|
||||
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { FaradayCage } from "faraday-cage"
|
||||
import { customCryptoModule } from "../../../cage-modules"
|
||||
|
||||
const runCage = async (script: string) => {
|
||||
const cage = await FaradayCage.create()
|
||||
return cage.runCode(script, [
|
||||
customCryptoModule({
|
||||
cryptoImpl: globalThis.crypto,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
describe("crypto.subtle.sign/verify", () => {
|
||||
it("signs and verifies with HMAC", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data = [109, 101, 115, 115, 97, 103, 101] // "message"
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, data)
|
||||
if (!Array.isArray(signature) || signature.length === 0) throw new Error('signature shape mismatch')
|
||||
|
||||
const isValid = await crypto.subtle.verify('HMAC', key, signature, data)
|
||||
if (isValid !== true) throw new Error('expected signature to verify')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("signs and verifies with ECDSA", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data = [116, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101] // "test message"
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
keyPair.privateKey,
|
||||
data
|
||||
)
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
keyPair.publicKey,
|
||||
signature,
|
||||
data
|
||||
)
|
||||
|
||||
if (isValid !== true) throw new Error('expected ECDSA signature to verify')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("signs and verifies with RSA-PSS", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'RSA-PSS', modulusLength: 2048, publicExponent: [1, 0, 1], hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data = [114, 115, 97, 32, 116, 101, 115, 116] // "rsa test"
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
{ name: 'RSA-PSS', saltLength: 32 },
|
||||
keyPair.privateKey,
|
||||
data
|
||||
)
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
{ name: 'RSA-PSS', saltLength: 32 },
|
||||
keyPair.publicKey,
|
||||
signature,
|
||||
data
|
||||
)
|
||||
|
||||
if (isValid !== true) throw new Error('expected RSA-PSS signature to verify')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("returns false for verification with different data", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data1 = [100, 97, 116, 97, 49] // "data1"
|
||||
const data2 = [100, 97, 116, 97, 50] // "data2"
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, data1)
|
||||
const isValid = await crypto.subtle.verify('HMAC', key, signature, data2)
|
||||
if (isValid !== false) throw new Error('expected verification to be false with different data')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("returns false for verification with wrong key", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key1 = await crypto.subtle.generateKey(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const key2 = await crypto.subtle.generateKey(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data = [116, 101, 115, 116]
|
||||
const signature = await crypto.subtle.sign('HMAC', key1, data)
|
||||
const isValid = await crypto.subtle.verify('HMAC', key2, signature, data)
|
||||
|
||||
if (isValid !== false) throw new Error('expected verification to be false with wrong key')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("handles empty data", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const key = await crypto.subtle.generateKey(
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data = []
|
||||
const signature = await crypto.subtle.sign('HMAC', key, data)
|
||||
const isValid = await crypto.subtle.verify('HMAC', key, signature, data)
|
||||
if (isValid !== true) throw new Error('expected verification to succeed for empty data')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
|
||||
it("signs and verifies with RSASSA-PKCS1-v1_5", async () => {
|
||||
const script = `
|
||||
(async () => {
|
||||
const keyPair = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
modulusLength: 2048,
|
||||
publicExponent: [1, 0, 1],
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['sign', 'verify']
|
||||
)
|
||||
|
||||
const data = [114, 115, 97, 32, 116, 101, 115, 116] // "rsa test"
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
{ name: 'RSASSA-PKCS1-v1_5' },
|
||||
keyPair.privateKey,
|
||||
data
|
||||
)
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
{ name: 'RSASSA-PKCS1-v1_5' },
|
||||
keyPair.publicKey,
|
||||
signature,
|
||||
data
|
||||
)
|
||||
|
||||
if (isValid !== true) throw new Error('expected RSASSA-PKCS1-v1_5 signature to verify')
|
||||
})()
|
||||
`
|
||||
|
||||
const result = await runCage(script)
|
||||
expect(result.type).toBe("ok")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import { describe, expect, test } from "vitest"
|
||||
import { runTestWithEmptyEnv } from "~/utils/test-helpers"
|
||||
|
||||
// Pre-compute test data outside the sandbox
|
||||
const helloWorldBytes = Array.from(new TextEncoder().encode("Hello, World!"))
|
||||
const testBytes = Array.from(new TextEncoder().encode("test"))
|
||||
const emptyBytes: number[] = []
|
||||
const consistentDataBytes = Array.from(
|
||||
new TextEncoder().encode("consistent data")
|
||||
)
|
||||
const data1Bytes = Array.from(new TextEncoder().encode("data1"))
|
||||
const data2Bytes = Array.from(new TextEncoder().encode("data2"))
|
||||
|
||||
describe("crypto.subtle.digest()", () => {
|
||||
test("should compute SHA-256 hash of string data", () => {
|
||||
const script = `
|
||||
const dataArray = ${JSON.stringify(helloWorldBytes)}
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", dataArray)
|
||||
|
||||
// Convert to hex string for verification
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
// Expected SHA-256 hash of "Hello, World!"
|
||||
const expectedHash = "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"
|
||||
|
||||
hopp.test("SHA-256 digest is correct", () => {
|
||||
hopp.expect(hashHex).toBe(expectedHash)
|
||||
hopp.expect(hashBuffer.byteLength).toBe(32) // SHA-256 produces 32 bytes
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "SHA-256 digest is correct",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should compute SHA-1 hash", () => {
|
||||
const script = `
|
||||
const dataArray = ${JSON.stringify(testBytes)}
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-1", dataArray)
|
||||
|
||||
// Expected SHA-1 hash of "test"
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const expectedHash = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
|
||||
|
||||
hopp.test("SHA-1 digest is correct", () => {
|
||||
hopp.expect(hashHex).toBe(expectedHash)
|
||||
hopp.expect(hashBuffer.byteLength).toBe(20) // SHA-1 produces 20 bytes
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "SHA-1 digest is correct",
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should compute SHA-384 hash", () => {
|
||||
const script = `
|
||||
const dataArray = ${JSON.stringify(testBytes)}
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-384", dataArray)
|
||||
|
||||
hopp.test("SHA-384 produces correct byte length", () => {
|
||||
hopp.expect(hashBuffer.byteLength).toBe(48) // SHA-384 produces 48 bytes
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "SHA-384 produces correct byte length",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should compute SHA-512 hash", () => {
|
||||
const script = `
|
||||
const dataArray = ${JSON.stringify(testBytes)}
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-512", dataArray)
|
||||
|
||||
hopp.test("SHA-512 produces correct byte length", () => {
|
||||
hopp.expect(hashBuffer.byteLength).toBe(64) // SHA-512 produces 64 bytes
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "SHA-512 produces correct byte length",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should handle empty string", () => {
|
||||
const script = `
|
||||
const dataArray = ${JSON.stringify(emptyBytes)}
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", dataArray)
|
||||
|
||||
// SHA-256 of empty string
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const expectedHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
|
||||
hopp.test("Empty string digest is correct", () => {
|
||||
hopp.expect(hashHex).toBe(expectedHash)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Empty string digest is correct",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should produce consistent hashes for same input", () => {
|
||||
const script = `
|
||||
const dataArray = ${JSON.stringify(consistentDataBytes)}
|
||||
|
||||
const hash1 = await crypto.subtle.digest("SHA-256", dataArray)
|
||||
const hash2 = await crypto.subtle.digest("SHA-256", dataArray)
|
||||
|
||||
const hash1Hex = Array.from(new Uint8Array(hash1)).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const hash2Hex = Array.from(new Uint8Array(hash2)).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
hopp.test("Same input produces same hash", () => {
|
||||
hopp.expect(hash1Hex).toBe(hash2Hex)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Same input produces same hash",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("should produce different hashes for different inputs", () => {
|
||||
const script = `
|
||||
const data1Array = ${JSON.stringify(data1Bytes)}
|
||||
const data2Array = ${JSON.stringify(data2Bytes)}
|
||||
|
||||
const hash1 = await crypto.subtle.digest("SHA-256", data1Array)
|
||||
const hash2 = await crypto.subtle.digest("SHA-256", data2Array)
|
||||
|
||||
const hash1Hex = Array.from(new Uint8Array(hash1)).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
const hash2Hex = Array.from(new Uint8Array(hash2)).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
|
||||
hopp.test("Different inputs produce different hashes", () => {
|
||||
hopp.expect(hash1Hex).not.toBe(hash2Hex)
|
||||
})
|
||||
`
|
||||
|
||||
return expect(runTestWithEmptyEnv(script)()).resolves.toEqualRight([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
descriptor: "Different inputs produce different hashes",
|
||||
expectResults: [expect.objectContaining({ status: "pass" })],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -947,7 +947,6 @@ describe("Serialization Edge Cases - Assertion Chaining", () => {
|
|||
pm.test("Circular array limitation", function() {
|
||||
const arr = [1, 2, 3]
|
||||
arr.push(arr) // Creates circular reference
|
||||
|
||||
pm.expect(arr).to.have.lengthOf(4)
|
||||
})
|
||||
`)()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { describe, expect, test } from "vitest"
|
||||
import { runTest, fakeResponse } from "~/utils/test-helpers"
|
||||
|
||||
// Skipped: These tests are comprehensive but cause timeout issues in CI/CD environments
|
||||
// due to the large number of iterations (100+ per test). The toBeLevelxxx matchers are
|
||||
// adequately covered by other test suites and E2E tests. Re-enable if timeout issues are resolved.
|
||||
describe("toBeLevelxxx", { timeout: 100000 }, () => {
|
||||
describe("toBeLevel2xx", () => {
|
||||
test("assertion passes for 200 series with no negation", async () => {
|
||||
|
|
|
|||
1031
packages/hoppscotch-js-sandbox/src/cage-modules/crypto.ts
Normal file
1031
packages/hoppscotch-js-sandbox/src/cage-modules/crypto.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,13 +2,13 @@ import {
|
|||
blobPolyfill,
|
||||
ConsoleEntry,
|
||||
console as ConsoleModule,
|
||||
crypto,
|
||||
encoding,
|
||||
esmModuleLoader,
|
||||
timers,
|
||||
urlPolyfill,
|
||||
} from "faraday-cage/modules"
|
||||
import type { HoppFetchHook } from "~/types"
|
||||
import { customCryptoModule } from "./crypto"
|
||||
import { customFetchModule } from "./fetch"
|
||||
|
||||
type DefaultModulesConfig = {
|
||||
|
|
@ -60,7 +60,7 @@ export const defaultModules = (config?: DefaultModulesConfig) => {
|
|||
console.table(...args)
|
||||
},
|
||||
}),
|
||||
crypto({
|
||||
customCryptoModule({
|
||||
cryptoImpl: globalThis.crypto,
|
||||
}),
|
||||
|
||||
|
|
|
|||
|
|
@ -3,53 +3,27 @@ import {
|
|||
defineSandboxFunctionRaw,
|
||||
} from "faraday-cage/modules"
|
||||
import type { HoppFetchHook } from "~/types"
|
||||
import { marshalValue as sharedMarshalValue } from "./utils/vm-marshal"
|
||||
|
||||
/**
|
||||
* Type augmentation for Headers to include iterator methods
|
||||
* These methods exist in modern Headers implementations but may not be in all type definitions
|
||||
*/
|
||||
// Type augmentation for some Headers iterator methods.
|
||||
interface HeadersWithIterators extends Headers {
|
||||
entries(): IterableIterator<[string, string]>
|
||||
keys(): IterableIterator<string>
|
||||
values(): IterableIterator<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended Response type with internal properties for serialization
|
||||
* These properties are added by HoppFetchHook implementations
|
||||
*/
|
||||
// Response shape used for VM serialization.
|
||||
type SerializableResponse = Response & {
|
||||
/**
|
||||
* Raw body bytes for efficient transfer across VM boundary
|
||||
*/
|
||||
_bodyBytes: number[]
|
||||
/**
|
||||
* Plain object containing header key-value pairs (no methods)
|
||||
* Used for efficient iteration in the VM without native Headers methods
|
||||
*/
|
||||
_headersData?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for async script execution hooks
|
||||
* Although typed as (() => void) in faraday-cage, the runtime supports async functions
|
||||
*/
|
||||
type AsyncScriptExecutionHook = () => Promise<void>
|
||||
|
||||
/**
|
||||
* Interface for configuring the custom fetch module
|
||||
*/
|
||||
export type CustomFetchModuleConfig = {
|
||||
/**
|
||||
* Custom fetch implementation to use (HoppFetchHook)
|
||||
*/
|
||||
fetchImpl?: HoppFetchHook
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom fetch module that uses HoppFetchHook
|
||||
* This module wraps the HoppFetchHook and provides proper async handling
|
||||
*/
|
||||
export const customFetchModule = (config: CustomFetchModuleConfig = {}) =>
|
||||
defineCageModule((ctx) => {
|
||||
const fetchImpl = config.fetchImpl || globalThis.fetch
|
||||
|
|
@ -98,33 +72,8 @@ export const customFetchModule = (config: CustomFetchModuleConfig = {}) =>
|
|||
})
|
||||
}
|
||||
|
||||
// Helper to marshal values to VM
|
||||
const marshalValue = (value: any): any => {
|
||||
if (value === null) return ctx.vm.null
|
||||
if (value === undefined) return ctx.vm.undefined
|
||||
if (value === true) return ctx.vm.true
|
||||
if (value === false) return ctx.vm.false
|
||||
if (typeof value === "string")
|
||||
return ctx.scope.manage(ctx.vm.newString(value))
|
||||
if (typeof value === "number")
|
||||
return ctx.scope.manage(ctx.vm.newNumber(value))
|
||||
if (typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
const arr = ctx.scope.manage(ctx.vm.newArray())
|
||||
value.forEach((item, i) => {
|
||||
ctx.vm.setProp(arr, i, marshalValue(item))
|
||||
})
|
||||
return arr
|
||||
} else {
|
||||
const obj = ctx.scope.manage(ctx.vm.newObject())
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
ctx.vm.setProp(obj, k, marshalValue(v))
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return ctx.vm.undefined
|
||||
}
|
||||
// Helper to marshal values to VM (using shared utility)
|
||||
const marshalValue = (value: any): any => sharedMarshalValue(ctx, value)
|
||||
|
||||
// Define fetch function in the sandbox
|
||||
const fetchFn = defineSandboxFunctionRaw(ctx, "fetch", (...args) => {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
export { defaultModules } from "./default"
|
||||
export { postRequestModule, preRequestModule } from "./scripting-modules"
|
||||
export { customCryptoModule } from "./crypto"
|
||||
export type { CustomCryptoModuleConfig } from "./crypto"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
// Internal faraday-cage ctx type (kept as any).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type CageModuleContext = any
|
||||
|
||||
export const marshalValue = (ctx: CageModuleContext, value: any): any => {
|
||||
if (value === null) return ctx.vm.null
|
||||
if (value === undefined) return ctx.vm.undefined
|
||||
if (value === true) return ctx.vm.true
|
||||
if (value === false) return ctx.vm.false
|
||||
if (typeof value === "string")
|
||||
return ctx.scope.manage(ctx.vm.newString(value))
|
||||
if (typeof value === "number")
|
||||
return ctx.scope.manage(ctx.vm.newNumber(value))
|
||||
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
||||
// Convert typed arrays to plain arrays for QuickJS
|
||||
// QuickJS doesn't support native TypedArrays/ArrayBuffer
|
||||
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value)
|
||||
const arr = ctx.scope.manage(ctx.vm.newArray())
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
ctx.vm.setProp(arr, i, ctx.scope.manage(ctx.vm.newNumber(bytes[i])))
|
||||
}
|
||||
// Add byteLength property for ArrayBuffer compatibility
|
||||
ctx.vm.setProp(
|
||||
arr,
|
||||
"byteLength",
|
||||
ctx.scope.manage(ctx.vm.newNumber(bytes.length))
|
||||
)
|
||||
return arr
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
const arr = ctx.scope.manage(ctx.vm.newArray())
|
||||
value.forEach((item, i) => {
|
||||
ctx.vm.setProp(arr, i, marshalValue(ctx, item))
|
||||
})
|
||||
return arr
|
||||
} else {
|
||||
const obj = ctx.scope.manage(ctx.vm.newObject())
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
ctx.vm.setProp(obj, k, marshalValue(ctx, v))
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return ctx.vm.undefined
|
||||
}
|
||||
|
||||
export const vmArrayToUint8Array = (
|
||||
ctx: CageModuleContext,
|
||||
vmArray: any
|
||||
): Uint8Array => {
|
||||
const length = ctx.vm.getProp(vmArray, "length")
|
||||
const lengthNum = ctx.vm.getNumber(length)
|
||||
length.dispose()
|
||||
|
||||
const bytes = new Uint8Array(lengthNum)
|
||||
for (let i = 0; i < lengthNum; i++) {
|
||||
const item = ctx.vm.getProp(vmArray, i)
|
||||
bytes[i] = ctx.vm.getNumber(item)
|
||||
item.dispose()
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
export const uint8ArrayToVmArray = (
|
||||
ctx: CageModuleContext,
|
||||
bytes: Uint8Array
|
||||
): any => {
|
||||
const vmArray = ctx.scope.manage(ctx.vm.newArray())
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
ctx.vm.setProp(vmArray, i, ctx.scope.manage(ctx.vm.newNumber(bytes[i])))
|
||||
}
|
||||
// Add byteLength property for ArrayBuffer compatibility
|
||||
ctx.vm.setProp(
|
||||
vmArray,
|
||||
"byteLength",
|
||||
ctx.scope.manage(ctx.vm.newNumber(bytes.length))
|
||||
)
|
||||
return vmArray
|
||||
}
|
||||
|
||||
interface KeyEntry {
|
||||
ref: WeakRef<CryptoKey | CryptoKeyPair> | CryptoKey | CryptoKeyPair
|
||||
strongRef: CryptoKey | CryptoKeyPair
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
const KEY_EXPIRY_MS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export class CryptoKeyRegistry {
|
||||
private readonly supportsWeakRef = typeof WeakRef === "function"
|
||||
private readonly supportsFinalizer =
|
||||
typeof FinalizationRegistry === "function"
|
||||
private keys = new Map<string, KeyEntry>()
|
||||
private finalizer?: FinalizationRegistry<string>
|
||||
private cleanupTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
constructor() {
|
||||
if (this.supportsFinalizer) {
|
||||
this.finalizer = new FinalizationRegistry((id: string) => {
|
||||
this.keys.delete(id)
|
||||
})
|
||||
}
|
||||
this.scheduleCleanup()
|
||||
}
|
||||
|
||||
store(key: CryptoKey | CryptoKeyPair, ttl: number = KEY_EXPIRY_MS): string {
|
||||
const id =
|
||||
typeof globalThis.crypto?.randomUUID === "function"
|
||||
? globalThis.crypto.randomUUID()
|
||||
: (() => {
|
||||
// Fallback if randomUUID is unavailable - identifier does not need to be cryptographically secure
|
||||
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
||||
const bytes = new Uint8Array(16)
|
||||
globalThis.crypto.getRandomValues(bytes)
|
||||
// RFC 4122 v4 bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
const hex = Array.from(bytes, (b) =>
|
||||
b.toString(16).padStart(2, "0")
|
||||
).join("")
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
||||
}
|
||||
|
||||
return `${Date.now()}-${Math.random()}-${Math.random()}`
|
||||
})()
|
||||
const expiresAt = Date.now() + ttl
|
||||
|
||||
this.keys.set(id, {
|
||||
ref: this.supportsWeakRef ? new WeakRef(key) : key,
|
||||
strongRef: key,
|
||||
expiresAt,
|
||||
})
|
||||
|
||||
if (this.finalizer) {
|
||||
this.finalizer.register(key, id, key)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
get(id: string): CryptoKey | CryptoKeyPair | undefined {
|
||||
const entry = this.keys.get(id)
|
||||
if (!entry) return undefined
|
||||
|
||||
const now = Date.now()
|
||||
if (entry.expiresAt <= now) {
|
||||
this.keys.delete(id)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const key =
|
||||
this.supportsWeakRef && "deref" in entry.ref
|
||||
? ((entry.ref as WeakRef<CryptoKey | CryptoKeyPair>).deref() ??
|
||||
entry.strongRef)
|
||||
: entry.strongRef
|
||||
|
||||
if (!key) {
|
||||
this.keys.delete(id)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Reset TTL on access
|
||||
entry.expiresAt = Date.now() + KEY_EXPIRY_MS
|
||||
return key
|
||||
}
|
||||
|
||||
has(id: string): boolean {
|
||||
return this.get(id) !== undefined
|
||||
}
|
||||
|
||||
delete(id: string): boolean {
|
||||
const entry = this.keys.get(id)
|
||||
if (!entry) return false
|
||||
|
||||
if (this.finalizer) {
|
||||
const key = entry.strongRef
|
||||
|
||||
if (key) {
|
||||
this.finalizer.unregister(key)
|
||||
}
|
||||
}
|
||||
|
||||
return this.keys.delete(id)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const [_id, entry] of this.keys.entries()) {
|
||||
if (this.finalizer) {
|
||||
const key = entry.strongRef
|
||||
|
||||
this.finalizer.unregister(key)
|
||||
}
|
||||
}
|
||||
this.keys.clear()
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
this.cleanup()
|
||||
return this.keys.size
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearTimeout(this.cleanupTimer)
|
||||
this.cleanupTimer = null
|
||||
}
|
||||
this.clear()
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now()
|
||||
for (const [id, entry] of this.keys.entries()) {
|
||||
const key =
|
||||
this.supportsWeakRef && "deref" in entry.ref
|
||||
? ((entry.ref as WeakRef<CryptoKey | CryptoKeyPair>).deref() ??
|
||||
entry.strongRef)
|
||||
: entry.strongRef
|
||||
|
||||
if (!key || entry.expiresAt <= now) {
|
||||
this.keys.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleCleanup(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearTimeout(this.cleanupTimer)
|
||||
}
|
||||
|
||||
this.cleanupTimer = setTimeout(() => {
|
||||
this.cleanup()
|
||||
this.scheduleCleanup()
|
||||
}, KEY_EXPIRY_MS) // Cleanup interval based on KEY_EXPIRY_MS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum byte size for getRandomValues() per Web Crypto spec
|
||||
* https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues
|
||||
*/
|
||||
export const MAX_GET_RANDOM_VALUES_SIZE = 65536
|
||||
Loading…
Reference in a new issue