diff --git a/bun.lock b/bun.lock index ac0652b..0ce52ec 100644 --- a/bun.lock +++ b/bun.lock @@ -76,11 +76,13 @@ "name": "@furtherverse/crypto", "version": "1.0.0", "dependencies": { + "node-forge": "^1.3.3", "openpgp": "catalog:", }, "devDependencies": { "@furtherverse/tsconfig": "workspace:*", "@types/bun": "catalog:", + "@types/node-forge": "^1.3.14", }, }, "packages/tsconfig": { @@ -562,6 +564,8 @@ "@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -1142,6 +1146,8 @@ "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], + "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 6018329..05d28bd 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -7,10 +7,12 @@ ".": "./src/index.ts" }, "dependencies": { + "node-forge": "^1.3.3", "openpgp": "catalog:" }, "devDependencies": { "@furtherverse/tsconfig": "workspace:*", - "@types/bun": "catalog:" + "@types/bun": "catalog:", + "@types/node-forge": "^1.3.14" } } diff --git a/packages/crypto/src/rsa-oaep.ts b/packages/crypto/src/rsa-oaep.ts index 1de1583..5304bed 100644 --- a/packages/crypto/src/rsa-oaep.ts +++ b/packages/crypto/src/rsa-oaep.ts @@ -1,34 +1,32 @@ -import { constants, createPublicKey, publicEncrypt } from 'node:crypto' +import forge from 'node-forge' /** * RSA-OAEP encrypt with platform public key. * - * Algorithm: RSA/ECB/OAEPWithSHA-256AndMGF1Padding - * - OAEP hash: SHA-256 - * - MGF1 hash: SHA-256 + * Matches Java's {@code Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")} + * with **default SunJCE parameters**: + * + * | Parameter | Value | + * |-----------|--------| + * | OAEP hash | SHA-256| + * | MGF1 hash | SHA-1 | + * + * Node.js `crypto.publicEncrypt({ oaepHash })` ties both hashes together, + * so we use `node-forge` which allows independent configuration. * * @param plaintext - UTF-8 string to encrypt - * @param publicKeyBase64 - Platform public key (X.509 DER, Base64 encoded) + * @param publicKeyBase64 - Platform RSA public key (X.509 / SPKI DER, Base64) * @returns Base64-encoded ciphertext */ export const rsaOaepEncrypt = (plaintext: string, publicKeyBase64: string): string => { - // Load public key from Base64-encoded DER (X.509 / SubjectPublicKeyInfo) - const publicKeyDer = Buffer.from(publicKeyBase64, 'base64') - const publicKey = createPublicKey({ - key: publicKeyDer, - format: 'der', - type: 'spki', + const derBytes = forge.util.decode64(publicKeyBase64) + const asn1 = forge.asn1.fromDer(derBytes) + const publicKey = forge.pki.publicKeyFromAsn1(asn1) as forge.pki.rsa.PublicKey + + const encrypted = publicKey.encrypt(plaintext, 'RSA-OAEP', { + md: forge.md.sha256.create(), + mgf1: { md: forge.md.sha1.create() }, }) - // Encrypt with RSA-OAEP (SHA-256 for both OAEP hash and MGF1) - const encrypted = publicEncrypt( - { - key: publicKey, - padding: constants.RSA_PKCS1_OAEP_PADDING, - oaepHash: 'sha256', - }, - Buffer.from(plaintext, 'utf-8'), - ) - - return encrypted.toString('base64') + return forge.util.encode64(encrypted) }