fix(crypto): 修复 RSA-OAEP 加密与 Java SunJCE 的 MGF1 哈希不兼容问题
Node.js publicEncrypt({ oaepHash }) 会将 OAEP hash 和 MGF1 hash
绑定为同一算法,而 Java OAEPWithSHA-256AndMGF1Padding 默认使用
SHA-256(OAEP) + SHA-1(MGF1)。改用 node-forge 独立配置两个哈希,
确保密文可被管理平台正确解密。
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -76,11 +76,13 @@
|
|||||||
"name": "@furtherverse/crypto",
|
"name": "@furtherverse/crypto",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"node-forge": "^1.3.3",
|
||||||
"openpgp": "catalog:",
|
"openpgp": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
"@furtherverse/tsconfig": "workspace:*",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/tsconfig": {
|
"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": ["@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/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=="],
|
"@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-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-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=="],
|
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"node-forge": "^1.3.3",
|
||||||
"openpgp": "catalog:"
|
"openpgp": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
"@furtherverse/tsconfig": "workspace:*",
|
||||||
"@types/bun": "catalog:"
|
"@types/bun": "catalog:",
|
||||||
|
"@types/node-forge": "^1.3.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
import { constants, createPublicKey, publicEncrypt } from 'node:crypto'
|
import forge from 'node-forge'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RSA-OAEP encrypt with platform public key.
|
* RSA-OAEP encrypt with platform public key.
|
||||||
*
|
*
|
||||||
* Algorithm: RSA/ECB/OAEPWithSHA-256AndMGF1Padding
|
* Matches Java's {@code Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")}
|
||||||
* - OAEP hash: SHA-256
|
* with **default SunJCE parameters**:
|
||||||
* - MGF1 hash: SHA-256
|
*
|
||||||
|
* | 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 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
|
* @returns Base64-encoded ciphertext
|
||||||
*/
|
*/
|
||||||
export const rsaOaepEncrypt = (plaintext: string, publicKeyBase64: string): string => {
|
export const rsaOaepEncrypt = (plaintext: string, publicKeyBase64: string): string => {
|
||||||
// Load public key from Base64-encoded DER (X.509 / SubjectPublicKeyInfo)
|
const derBytes = forge.util.decode64(publicKeyBase64)
|
||||||
const publicKeyDer = Buffer.from(publicKeyBase64, 'base64')
|
const asn1 = forge.asn1.fromDer(derBytes)
|
||||||
const publicKey = createPublicKey({
|
const publicKey = forge.pki.publicKeyFromAsn1(asn1) as forge.pki.rsa.PublicKey
|
||||||
key: publicKeyDer,
|
|
||||||
format: 'der',
|
const encrypted = publicKey.encrypt(plaintext, 'RSA-OAEP', {
|
||||||
type: 'spki',
|
md: forge.md.sha256.create(),
|
||||||
|
mgf1: { md: forge.md.sha1.create() },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Encrypt with RSA-OAEP (SHA-256 for both OAEP hash and MGF1)
|
return forge.util.encode64(encrypted)
|
||||||
const encrypted = publicEncrypt(
|
|
||||||
{
|
|
||||||
key: publicKey,
|
|
||||||
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
||||||
oaepHash: 'sha256',
|
|
||||||
},
|
|
||||||
Buffer.from(plaintext, 'utf-8'),
|
|
||||||
)
|
|
||||||
|
|
||||||
return encrypted.toString('base64')
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user