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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# certs
certs

# dependencies
node_modules
package-lock.json
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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
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'))\""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GPT created helper for...

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

}
}
8 changes: 8 additions & 0 deletions packages/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# MOSIP API Crypto

Helpers for encryption and decryption of the communication with MOSIP. It's separated here for clarity, as it can be complex at parts.

These functions take in the following files, which the other packages must supply to these functions.

- `keystore.p12`
- `ida-partner.crt`
20 changes: 20 additions & 0 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@opencrvs/mosip-crypto",
"license": "MPL-2.0",
"version": "1.7.0-alpha.16",
"main": "index.js",
"dependencies": {
"@fastify/formbody": "^8.0.1",
"@fastify/static": "^8.0.3",
"@types/node": "^22.4.1",
"@types/node-forge": "^1.3.11",
"casual": "^1.6.2",
"envalid": "^8.0.0",
"fastify": "^5.0.0",
"jose": "^5.9.6",
"jsonwebtoken": "^9.0.2",
"node-forge": "^1.3.1",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
Comment on lines +7 to +18
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likely most are unneeded

}
}
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";
5 changes: 5 additions & 0 deletions packages/crypto/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const padBase64 = (str: string) =>
str + "=".repeat((4 - (str.length % 4)) % 4);

export const base64Encode = (input: string) =>
Buffer.from(input, "utf8").toString("base64url");
Comment on lines +1 to +5
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment what and why

20 changes: 20 additions & 0 deletions packages/ida-auth-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@opencrvs/mosip-ida-auth-sdk",
"license": "MPL-2.0",
"version": "1.7.0-alpha.16",
"main": "index.js",
"dependencies": {
"@fastify/formbody": "^8.0.1",
"@fastify/static": "^8.0.3",
"@types/node": "^22.4.1",
"@types/node-forge": "^1.3.11",
"casual": "^1.6.2",
"envalid": "^8.0.0",
"fastify": "^5.0.0",
"jose": "^5.9.6",
"jsonwebtoken": "^9.0.2",
"node-forge": "^1.3.1",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}
}
40 changes: 40 additions & 0 deletions packages/ida-auth-sdk/src/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import MOSIPAuthenticator from "./mosip-authenticator";

const main = async () => {
const authenticator = new MOSIPAuthenticator({
// MOSIP Auth
partnerApiKey: "",
partnerMispLk: "",
partnerId: "",

// MOSIP Auth Server
idaAuthDomainUri: "https://api-internal.collab.mosip.net",
idaAuthUrl: "https://api.collab.mosip.net/idauthentication/v1/auth",

// Crypto encrypt
encryptCertPath: "../../certs/ida-partner.crt",
decryptP12FilePath: "../../certs/keystore.p12",
decryptP12FilePassword: "",

// Crypto signature
signP12FilePath: "../../certs/keystore.p12",
signP12FilePassword: "",
});

const response = await authenticator.auth({
individualId: "6580954839",
individualIdType: "UIN",
demographicData: {
dob: "1992/04/29",
},
consent: true,
});

if (!response.ok) {
throw new Error(`Error in MOSIP Authenticator: ${await response.text()}`);
}

console.log(await response.json());
};

main();
Loading