Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ida auth sdk #49

Merged
merged 21 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This package ensures a secure and a robust integration between OpenCRVS and MOSI
## Development

```sh
# copy demo certs to gitignored location
cp docs/example-certs/* certs/

# start the mosip-api and all the mocked servers
yarn install
yarn dev
Expand All @@ -16,6 +19,9 @@ yarn dev
cd packages/*
yarn install
yarn dev

# bump package.json versions
yarn set-version 1.7.0-alpha.16
```

## Country configuration
Expand Down
1 change: 1 addition & 0 deletions certs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
19 changes: 19 additions & 0 deletions docs/example-certs/ida-partner.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfkCFEuJlYaoJWlqAhGqqnpRCIujbooeMA0GCSqGSIb3DQEBCwUAMEUx
CzAJBgNVBAYTAkJHMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwMTI4MDc0NzQ4WhcNMjYwMTI4MDc0
NzQ4WjBFMQswCQYDVQQGEwJCRzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA0Ku1bkM+xqpNempivJEvu2upOoaeHtq9l/1d98MWOQ5AfiQQ
lGtBcWruq3wGY0bWBgH1GjfhLM16zEdPMBXzKJCQC0Wsqc6R64cizgaqyi9qunk3
XTIhrF7/Vf3XNZrdzsKjbJXiLfPLANawVVgHTrQVfSe6mB6m1fl+bPXpNuW6wUVo
3L8UTbrUyKNlwXre2+repD4EKUtApiFQl3qiqfeDjQw4OxkQqQ75SS7kPvfwD4vz
US5C/nmmv9WVF98qBPVVCOUu/0cOACzs4II8Wd+pFgttiUMG2x094N3h2nk5+F4U
c3sp6oGYOL7QZf8y9yUOVTd/x7F9nYC58qAatQIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQCSWitTrls0diOtMZilODZhF7RF5m+7IHlNAETQqoqhiWbjmL/poO/up4np
MIZMM6Ofd6ZtUJJLhNQYH4+Ac/xnt5rePuVQuVVmMLAekKu+uJXEI8ORzR1lK7RW
CFo+Ugk+qJRvjNg0vR6WQkOaaL0MzDQh1ZcSlkXkAs+OzmLd7tqtEfhfAoTxI1Qr
csctaFaNG7OtYpXozIgm3je9GemoJrYrQ84EsgFiJcVpaYly9mKDadCMERYyo66w
OsFQJJVW7EWaGOhqGvimp/ueBVcjNCDXArSOVJnq0iou/FXCxDIN0roYUtBaGNwN
fMWV+rji9hHU1TAoJDop/oAUZkNk
-----END CERTIFICATE-----
Binary file added docs/example-certs/keystore.p12
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Installation

This document describes how to setup the integration between OpenCRVS and MOSIP. In this example, we will deploy the [mosip-mock](./packages/mosip-mock), [esigneet-mock](./packages/esigneet-mock) & [mosip-api](./packages/mosip-api). In a real-world scenario, MOSIP would provide the details we're mocking.
This document describes how to setup the integration between OpenCRVS and MOSIP. In this example, we will deploy the [mosip-mock](./packages/mosip-mock), [esignet-mock](./packages/esignet-mock) & [mosip-api](./packages/mosip-api). In a real-world scenario, MOSIP would provide the details we're mocking.
98 changes: 70 additions & 28 deletions docs/playground.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,55 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# Import birth bundle from .json\n",
"\n",
"import json\n",
"\n",
"with open('incoming-birth-bundle.json') as f:\n",
" event = json.load(f)\n"
" event = json.load(f)\n",
" record_id = event[\"entry\"][0][\"resource\"][\"id\"]\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"202\n"
]
}
],
"outputs": [],
"source": [
"# Get a record-specific token\n",
"# https://is-my-opencrvs-up.netlify.app/ token generator for the subject_token\n",
"\n",
"import requests\n",
"\n",
"url = \"http://localhost:4040/token\"\n",
"querystring = {\"subject_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWNvcmQuZGVjbGFyZS1iaXJ0aCIsInJlY29yZC5kZWNsYXJlLWRlYXRoIiwicmVjb3JkLmRlY2xhcmUtbWFycmlhZ2UiLCJyZWNvcmQuZGVjbGFyYXRpb24tZWRpdCIsInJlY29yZC5kZWNsYXJhdGlvbi1zdWJtaXQtZm9yLXVwZGF0ZXMiLCJyZWNvcmQucmV2aWV3LWR1cGxpY2F0ZXMiLCJyZWNvcmQuZGVjbGFyYXRpb24tYXJjaGl2ZSIsInJlY29yZC5kZWNsYXJhdGlvbi1yZWluc3RhdGUiLCJyZWNvcmQucmVnaXN0ZXIiLCJyZWNvcmQucmVnaXN0cmF0aW9uLWNvcnJlY3QiLCJyZWNvcmQuZGVjbGFyYXRpb24tcHJpbnQtc3VwcG9ydGluZy1kb2N1bWVudHMiLCJyZWNvcmQuZXhwb3J0LXJlY29yZHMiLCJyZWNvcmQudW5hc3NpZ24tb3RoZXJzIiwicmVjb3JkLnJlZ2lzdHJhdGlvbi1wcmludCZpc3N1ZS1jZXJ0aWZpZWQtY29waWVzIiwicmVjb3JkLmNvbmZpcm0tcmVnaXN0cmF0aW9uIiwicmVjb3JkLnJlamVjdC1yZWdpc3RyYXRpb24iLCJwZXJmb3JtYW5jZS5yZWFkIiwicGVyZm9ybWFuY2UucmVhZC1kYXNoYm9hcmRzIiwicHJvZmlsZS5lbGVjdHJvbmljLXNpZ25hdHVyZSIsIm9yZ2FuaXNhdGlvbi5yZWFkLWxvY2F0aW9uczpteS1vZmZpY2UiLCJzZWFyY2guYmlydGgiLCJzZWFyY2guZGVhdGgiLCJzZWFyY2gubWFycmlhZ2UiLCJkZW1vIl0sImlhdCI6MTczODc1OTE1MywiZXhwIjoxNzM5MzYzOTUzLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciIsIm9wZW5jcnZzOnNlYXJjaC11c2VyIiwib3BlbmNydnM6bWV0cmljcy11c2VyIiwib3BlbmNydnM6Y291bnRyeWNvbmZpZy11c2VyIiwib3BlbmNydnM6d2ViaG9va3MtdXNlciIsIm9wZW5jcnZzOmNvbmZpZy11c2VyIiwib3BlbmNydnM6ZG9jdW1lbnRzLXVzZXIiXSwiaXNzIjoib3BlbmNydnM6YXV0aC1zZXJ2aWNlIiwic3ViIjoiNjdhMzViZTM5Njg3ZTg4OTA0NTM4ZTJhIn0.Cig_aJgGuWArtcVhMUTY6oXJZTeUQ9jIOqiop6rPjzXUxbPiBiD_mnRAxUelY04FFtu8E2EHKQX0HLurD5kjcA32-wzag8nbuCRdKQcx-AB3PSdQcrqnrEXx4wtW6pfesupsKBqX1KiQC0g3hk8kcA71j_oqUzhhj-8M4M7bO5d8Ql4L-41-gsSWdiIYwNvFcSPTkV7_ysALUDQckvWg2qoZIsiRyvEsDZvuv9oworkm95Tv1xiZX5FaHNF1K7HOjt7mOorREW6tJB90LDry0FhUf-Cp6TAU3LxsNz8icaPpomysf45lbzNfJYcOSrJe2yVvBC8qGOGjhg4m32aGnQ\",\n",
" \"grant_type\":\"urn:opencrvs:oauth:grant-type:token-exchange\",\n",
" \"subject_token_type\":\"urn:ietf:params:oauth:token-type:access_token\",\n",
" \"requested_token_type\":\"urn:opencrvs:oauth:token-type:single_record_token\",\n",
" \"record_id\": record_id}\n",
"headers = {\n",
" \"Content-Type\": \"application/json\",\n",
"}\n",
"\n",
"response = requests.request(\"POST\", url, headers=headers, params=querystring)\n",
"token = response.json()[\"access_token\"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Register the event\n",
"\n",
"import requests\n",
"\n",
"url = \"http://localhost:2024/events/registration\"\n",
"token = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsInBlcmZvcm1hbmNlIiwiY2VydGlmeSIsImRlbW8iXSwiaWF0IjoxNzM3NDcwNjI1LCJleHAiOjE3MzgwNzU0MjUsImF1ZCI6WyJvcGVuY3J2czphdXRoLXVzZXIiLCJvcGVuY3J2czp1c2VyLW1nbnQtdXNlciIsIm9wZW5jcnZzOmhlYXJ0aC11c2VyIiwib3BlbmNydnM6Z2F0ZXdheS11c2VyIiwib3BlbmNydnM6bm90aWZpY2F0aW9uLXVzZXIiLCJvcGVuY3J2czp3b3JrZmxvdy11c2VyIiwib3BlbmNydnM6c2VhcmNoLXVzZXIiLCJvcGVuY3J2czptZXRyaWNzLXVzZXIiLCJvcGVuY3J2czpjb3VudHJ5Y29uZmlnLXVzZXIiLCJvcGVuY3J2czp3ZWJob29rcy11c2VyIiwib3BlbmNydnM6Y29uZmlnLXVzZXIiLCJvcGVuY3J2czpkb2N1bWVudHMtdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI2NzRkZTAzMGY4YzBhMWMxMmVmODBjODcifQ.BViXNILaE8aEKEXdb46gWGuuIarwxAMCY1hKM7lO6X3p7vcM7VfarPu36usM3Ca0AygOVIYwxZ5wEsJwAng1F10FSYBnu1G8vlk1nB99vqZa5_9Q0p-2lyfHkjFEOsusFjU1z7uTZ53VYJ_EsLwv6ClSF9slr4SxUL5486xC8mG9MuJpvKyGCPt9yPvfUyEX41PImrReMHJLgnE4S74bQW-B8CH2gi_CnZBGmYewljXF1Wf8AQgHqXfpTMO8M7mP947x3CMgdZVaRkd9mycsoPQCKVyH_P8kCjobwZxgPmmMAr9yfXfWGCVJvxQSJVNlpzcPpR9uygdl14IGn_eiQA\"\n",
"headers = {\"Authorization\": f\"Bearer {token}\"}\n",
"response = requests.post(url, json=event, headers=headers)\n",
"print(response.status_code)\n"
Expand All @@ -46,7 +67,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### Send a death bundle to the mosip-api webhook without having to create a record in UI"
"### Verification"
]
},
{
Expand All @@ -55,30 +76,51 @@
"metadata": {},
"outputs": [],
"source": [
"# Import birth bundle from .json\n",
"\n",
"import json\n",
"\n",
"with open('incoming-death-bundle.json') as f:\n",
" event = json.load(f)"
"with open('incoming-birth-bundle.json') as f:\n",
" event = json.load(f)\n",
" record_id = event[\"entry\"][0][\"resource\"][\"id\"]"
]
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"202\n"
]
}
],
"outputs": [],
"source": [
"# Get a record-specific token\n",
"# https://is-my-opencrvs-up.netlify.app/ token generator for the subject_token\n",
"\n",
"import requests\n",
"\n",
"url = \"http://localhost:2024/events/registration\"\n",
"token = \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsInBlcmZvcm1hbmNlIiwiY2VydGlmeSIsImRlbW8iXSwiaWF0IjoxNzM3NDcwNjI1LCJleHAiOjE3MzgwNzU0MjUsImF1ZCI6WyJvcGVuY3J2czphdXRoLXVzZXIiLCJvcGVuY3J2czp1c2VyLW1nbnQtdXNlciIsIm9wZW5jcnZzOmhlYXJ0aC11c2VyIiwib3BlbmNydnM6Z2F0ZXdheS11c2VyIiwib3BlbmNydnM6bm90aWZpY2F0aW9uLXVzZXIiLCJvcGVuY3J2czp3b3JrZmxvdy11c2VyIiwib3BlbmNydnM6c2VhcmNoLXVzZXIiLCJvcGVuY3J2czptZXRyaWNzLXVzZXIiLCJvcGVuY3J2czpjb3VudHJ5Y29uZmlnLXVzZXIiLCJvcGVuY3J2czp3ZWJob29rcy11c2VyIiwib3BlbmNydnM6Y29uZmlnLXVzZXIiLCJvcGVuY3J2czpkb2N1bWVudHMtdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI2NzRkZTAzMGY4YzBhMWMxMmVmODBjODcifQ.BViXNILaE8aEKEXdb46gWGuuIarwxAMCY1hKM7lO6X3p7vcM7VfarPu36usM3Ca0AygOVIYwxZ5wEsJwAng1F10FSYBnu1G8vlk1nB99vqZa5_9Q0p-2lyfHkjFEOsusFjU1z7uTZ53VYJ_EsLwv6ClSF9slr4SxUL5486xC8mG9MuJpvKyGCPt9yPvfUyEX41PImrReMHJLgnE4S74bQW-B8CH2gi_CnZBGmYewljXF1Wf8AQgHqXfpTMO8M7mP947x3CMgdZVaRkd9mycsoPQCKVyH_P8kCjobwZxgPmmMAr9yfXfWGCVJvxQSJVNlpzcPpR9uygdl14IGn_eiQA\"\n",
"url = \"http://localhost:4040/token\"\n",
"querystring = {\"subject_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWNvcmQuZGVjbGFyZS1iaXJ0aCIsInJlY29yZC5kZWNsYXJlLWRlYXRoIiwicmVjb3JkLmRlY2xhcmUtbWFycmlhZ2UiLCJyZWNvcmQuZGVjbGFyYXRpb24tZWRpdCIsInJlY29yZC5kZWNsYXJhdGlvbi1zdWJtaXQtZm9yLXVwZGF0ZXMiLCJyZWNvcmQucmV2aWV3LWR1cGxpY2F0ZXMiLCJyZWNvcmQuZGVjbGFyYXRpb24tYXJjaGl2ZSIsInJlY29yZC5kZWNsYXJhdGlvbi1yZWluc3RhdGUiLCJyZWNvcmQucmVnaXN0ZXIiLCJyZWNvcmQucmVnaXN0cmF0aW9uLWNvcnJlY3QiLCJyZWNvcmQuZGVjbGFyYXRpb24tcHJpbnQtc3VwcG9ydGluZy1kb2N1bWVudHMiLCJyZWNvcmQuZXhwb3J0LXJlY29yZHMiLCJyZWNvcmQudW5hc3NpZ24tb3RoZXJzIiwicmVjb3JkLnJlZ2lzdHJhdGlvbi1wcmludCZpc3N1ZS1jZXJ0aWZpZWQtY29waWVzIiwicmVjb3JkLmNvbmZpcm0tcmVnaXN0cmF0aW9uIiwicmVjb3JkLnJlamVjdC1yZWdpc3RyYXRpb24iLCJwZXJmb3JtYW5jZS5yZWFkIiwicGVyZm9ybWFuY2UucmVhZC1kYXNoYm9hcmRzIiwicHJvZmlsZS5lbGVjdHJvbmljLXNpZ25hdHVyZSIsIm9yZ2FuaXNhdGlvbi5yZWFkLWxvY2F0aW9uczpteS1vZmZpY2UiLCJzZWFyY2guYmlydGgiLCJzZWFyY2guZGVhdGgiLCJzZWFyY2gubWFycmlhZ2UiLCJkZW1vIl0sImlhdCI6MTczODc1OTE1MywiZXhwIjoxNzM5MzYzOTUzLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciIsIm9wZW5jcnZzOnNlYXJjaC11c2VyIiwib3BlbmNydnM6bWV0cmljcy11c2VyIiwib3BlbmNydnM6Y291bnRyeWNvbmZpZy11c2VyIiwib3BlbmNydnM6d2ViaG9va3MtdXNlciIsIm9wZW5jcnZzOmNvbmZpZy11c2VyIiwib3BlbmNydnM6ZG9jdW1lbnRzLXVzZXIiXSwiaXNzIjoib3BlbmNydnM6YXV0aC1zZXJ2aWNlIiwic3ViIjoiNjdhMzViZTM5Njg3ZTg4OTA0NTM4ZTJhIn0.Cig_aJgGuWArtcVhMUTY6oXJZTeUQ9jIOqiop6rPjzXUxbPiBiD_mnRAxUelY04FFtu8E2EHKQX0HLurD5kjcA32-wzag8nbuCRdKQcx-AB3PSdQcrqnrEXx4wtW6pfesupsKBqX1KiQC0g3hk8kcA71j_oqUzhhj-8M4M7bO5d8Ql4L-41-gsSWdiIYwNvFcSPTkV7_ysALUDQckvWg2qoZIsiRyvEsDZvuv9oworkm95Tv1xiZX5FaHNF1K7HOjt7mOorREW6tJB90LDry0FhUf-Cp6TAU3LxsNz8icaPpomysf45lbzNfJYcOSrJe2yVvBC8qGOGjhg4m32aGnQ\",\n",
" \"grant_type\":\"urn:opencrvs:oauth:grant-type:token-exchange\",\n",
" \"subject_token_type\":\"urn:ietf:params:oauth:token-type:access_token\",\n",
" \"requested_token_type\":\"urn:opencrvs:oauth:token-type:single_record_token\",\n",
" \"record_id\": record_id}\n",
"headers = {\n",
" \"Content-Type\": \"application/json\",\n",
"}\n",
"\n",
"response = requests.request(\"POST\", url, headers=headers, params=querystring)\n",
"token = response.json()[\"access_token\"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Review event\n",
"\n",
"import requests\n",
"\n",
"url = \"http://localhost:2024/events/review\"\n",
"headers = {\"Authorization\": f\"Bearer {token}\"}\n",
"response = requests.post(url, json=event, headers=headers)\n",
"print(response.status_code)"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"scripts": {
"dev": "turbo run dev",
"format": "prettier --write ."
"format": "prettier --write .",
"set-version": "node -e \"const fs=require('fs'),v=process.argv[1];['package.json',...fs.readdirSync('packages').map(d=>'packages/'+d+'/package.json')].forEach(f=>fs.writeFileSync(f,JSON.stringify({...require('./'+f),version:v},null,2)+'\\n'))\""
naftis marked this conversation as resolved.
Show resolved Hide resolved
}
}
3 changes: 3 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# MOSIP API Crypto

Helpers for encryption and decryption of the communication with MOSIP. The helpers are moved here for clarity, as it can be complex at parts. Basically this should abstract most `jose`, `node-forge`, `node:crypto`, etc. library calls away from the other packages.
12 changes: 12 additions & 0 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@opencrvs/mosip-crypto",
"license": "MPL-2.0",
"version": "1.7.0-alpha.16",
"main": "src/index.ts",
"dependencies": {
"@types/node-forge": "^1.3.11",
"jose": "^5.9.6",
"node-forge": "^1.3.1",
"typescript": "^5.6.3"
}
}
120 changes: 120 additions & 0 deletions packages/crypto/src/encrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as crypto from "node:crypto";
import * as jose from "jose";
import { base64Encode, padBase64 } from "./utils";
import forge from "node-forge";

export const getPemCertificateThumbprint = (pemCertificate: string) => {
const fingerprint = new crypto.X509Certificate(pemCertificate).fingerprint256; // In "node:crypto", this gives the SHA-256 fingerprint as a hexadecimal string
return Buffer.from(fingerprint.replace(/:/g, ""), "hex");
};

export const urlSafeCertificateThumbprint = (pemCertificate: string) =>
padBase64(getPemCertificateThumbprint(pemCertificate).toString("base64url"));

const SYMMETRIC_NONCE_SIZE = 128 / 8;
const SYMMETRIC_KEY_LENGTH = 256;

/** Symmetrically (allows to be decrypted by the same key) encrypts the data */
const symmetricEncrypt = (data: Buffer, key: Buffer) => {
const nonce = crypto.randomBytes(SYMMETRIC_NONCE_SIZE);
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce, {
authTagLength: 16,
});

const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
const tag = cipher.getAuthTag();

return Buffer.concat([encrypted, tag, nonce]);
};

/**
* Asymmetrically (allowed to be decrypted by the partner certificate) encrypts the data
*/
const asymmetricEncrypt = (
aesKey: Buffer,
encryptPemCertificate: string,
): Buffer => {
const cert = forge.pki.certificateFromPem(encryptPemCertificate);
const publicKey = cert.publicKey as forge.pki.rsa.PublicKey; // Explicitly cast to RSA public key

const encryptedKey = publicKey.encrypt(
aesKey.toString("binary"),
"RSA-OAEP",
{
md: forge.md.sha256.create(),
mgf1: forge.mgf.mgf1.create(forge.md.sha256.create()),
label: Buffer.alloc(0), // Explicitly set an empty label
},
);

return Buffer.from(encryptedKey, "binary");
};

export const encryptAuthData = (
data: string,
encryptPemCertificate: string,
) => {
// Generate a random AES Key and encrypt Auth Request Data using the generated random key.
const aesKey = crypto.randomBytes(SYMMETRIC_KEY_LENGTH / 8);

const encryptedData = symmetricEncrypt(Buffer.from(data, "utf-8"), aesKey);
const encryptedAuthB64Data = padBase64(encryptedData.toString("base64url"));

// Encrypt the randomly generated key using the IDA partner certificate
const encryptedAesKey = asymmetricEncrypt(aesKey, encryptPemCertificate);
const encryptedAesKeyB64 = encryptedAesKey.toString("base64url");

// Generate SHA256 hash for the Auth Request Data
const sha256Hash = crypto
.createHash("sha256")
.update(data)
.digest("hex")
.toUpperCase();
const authDataHashBuffer = Buffer.from(sha256Hash, "utf-8");
const encryptedAuthDataHash = symmetricEncrypt(authDataHashBuffer, aesKey);
const encryptedAuthDataHashBase64 = padBase64(
encryptedAuthDataHash.toString("base64url"),
);

return {
encryptedAuthB64Data,
encryptedAesKeyB64,
encryptedAuthDataHashBase64,
};
};

export async function signAuthRequestData(
authRequestData: string,
encryptPemCertificate: string,
signPemPrivateKey: string,
signPemCertificate: string,
algorithm = "RS256",
) {
const protectedHeader = {
alg: algorithm,
x5c: [base64Encode(signPemCertificate)],
};

const unprotectedHeader = {
kid: crypto
.createHash("sha256")
.update(encryptPemCertificate)
.digest("base64url"),
};

const privateKey = await jose.importPKCS8(signPemPrivateKey, algorithm);

const flattenedSign = await new jose.FlattenedSign(
Buffer.from(authRequestData, "utf-8"),
)
.setProtectedHeader(protectedHeader)
.setUnprotectedHeader(unprotectedHeader)
.sign(privateKey);

const parts = [
flattenedSign.protected,
"", // No payload in this case
flattenedSign.signature,
];
return `${parts[0]}..${parts[2]}`;
}
44 changes: 44 additions & 0 deletions packages/crypto/src/extract-pkcs12.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { asn1, pkcs12, pki } from "node-forge";

/**
* Reads and extracts private key and certificate from a PKCS#12 file.
* @param filePath - The path to the PKCS#12 (.p12) file.
* @param password - The password for decrypting the PKCS#12 file.
* @returns An object containing the private key and certificate in PEM format.
*/
export const extractKeysFromPkcs12 = (
fileContents: string,
password: string,
) => {
const p12Asn1 = asn1.fromDer(fileContents);
const p12Object = pkcs12.pkcs12FromAsn1(p12Asn1, password);

let privateKeyPkcs8: pki.PEM | null = null;
let certificate: pki.PEM | null = null;

// Extract private key and certificate
p12Object.safeContents.forEach((safeContent) => {
safeContent.safeBags.forEach((safeBag) => {
if (safeBag.type === pki.oids.pkcs8ShroudedKeyBag && safeBag.key) {
// To return PKCS#1:
// privateKeyPkcs1 = pki.privateKeyToPem(safeBag.key);
const privateKeyAsn1 = pki.privateKeyToAsn1(safeBag.key);
const privateKeyPkcs8Asn1 = pki.wrapRsaPrivateKey(privateKeyAsn1);
privateKeyPkcs8 = pki.privateKeyInfoToPem(privateKeyPkcs8Asn1);
} else if (safeBag.type === pki.oids.certBag && safeBag.cert) {
certificate = pki.certificateToPem(safeBag.cert);
}
});
});

if (privateKeyPkcs8 === null)
throw new Error("PEM private key not available in keystore");

if (certificate === null)
throw new Error("PEM certificate not available in keystore");

return { privateKeyPkcs8, certificate } as {
privateKeyPkcs8: pki.PEM;
certificate: pki.PEM;
};
};
2 changes: 2 additions & 0 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./encrypt";
export * from "./extract-pkcs12";
8 changes: 8 additions & 0 deletions packages/crypto/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// The receiving end expects a URL safe Base64 encoding in many places
// In addition to that, it also expects it to be padded with ='s to the nearest 4 character chunk, which base64url in Node.js doesn't do by default

export const padBase64 = (str: string) =>
str + "=".repeat((4 - (str.length % 4)) % 4);

export const base64Encode = (input: string) =>
Buffer.from(input, "utf8").toString("base64url");
naftis marked this conversation as resolved.
Show resolved Hide resolved
Loading