我正在尝试使用 SubtleCrypto(Web Crypto API 的接口)生成 RSA 私钥。 但在验证步骤中,手动计算的公共指数在 50% 的情况下与 JWK.e 不同。 Chrome、Edge 和 Firefox 上的行为相同。
🔎谁能解释一下bug藏在哪里?🕵🏼
最重要的代码部分(完整代码在最后):
// 1. Key
crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 1024,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 in little-endian
hash: "SHA-256",
}
true,
["encrypt", "decrypt"]
)
// 2. Export
crypto.subtle.exportKey("jwk", keyPair.privateKey)
// 3. Converting JWK (d, p, q, e, n) to BigInt
// 4. Validation RSA params
const pubExp = modInverse(jwk.d, (jwk.p - 1n) * (jwk.q - 1n));
const isExpValid = jwk.e === pubExp;
const isModulusValid = jwk.n === jwk.p * jwk.q;
console.log('public exp', isExpValid, jwk.e, pubExp);
console.log('n == p * q', isModulusValid);
目前,如果“错误”的 e 出现,那么我将重新运行生成。
完整代码:
运行后,调用generateRSAPrivateKeyObject(),您将在控制台中看到一些日志
function generateRSAPrivateKeyObject() {
const r16 = 16;
const onError = {
modulus: BigInt(0).toString(r16),
privateExponent: BigInt(0).toString(r16),
p: BigInt(0).toString(r16),
q: BigInt(0).toString(r16),
};
try {
function modInverse(a, m) {
let m0 = m;
let x0 = 0n;
let x1 = 1n;
while (a > 1n) {
const q = a / m;
let t = m;
m = a % m;
a = t;
t = x0;
x0 = x1 - q * x0;
x1 = t;
}
if (x1 < 0n) {
x1 += m0;
}
return x1;
}
function b64ToBn(b64) {
const bin = atob(b64);
const hex = [];
bin.split('').forEach(function (ch) {
let h = ch.charCodeAt(0).toString(r16);
if (h.length % 2) { h = '0' + h; }
hex.push(h);
});
return BigInt('0x' + hex.join(''));
}
function urlBase64ToBase64(str) {
const r = str % 4;
if (2 === r) {
str += '==';
} else if (3 === r) {
str += '=';
}
return str.replace(/-/g, '+').replace(/_/g, '/');
}
function base64urlDecode(base64url) {
return b64ToBn(urlBase64ToBase64(base64url));
}
function generateJWK() {
return crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 1024,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 in little-endian
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
)
.then((keyPair) => {
return crypto.subtle.exportKey("jwk", keyPair.privateKey);
})
.then((privateKeyJWK) => {
return {
n: base64urlDecode(privateKeyJWK.n),
p: base64urlDecode(privateKeyJWK.p),
q: base64urlDecode(privateKeyJWK.q),
d: base64urlDecode(privateKeyJWK.d),
e: base64urlDecode(privateKeyJWK.e)
};
})
}
let retries = 20;
function jwkValidator(jwk) {
const pubExp = modInverse(jwk.d, (jwk.p - 1n) * (jwk.q - 1n));
const isExpValid = jwk.e === pubExp;
const isModulusValid = jwk.n === jwk.p * jwk.q;
console.log('public exp', isExpValid, jwk.e, pubExp);
console.log('n == p * q', isModulusValid);
if (isExpValid && isModulusValid) {
return {
modulus: jwk.n.toString(r16),
p: jwk.p.toString(r16),
q: jwk.q.toString(r16),
privateExponent: jwk.d.toString(r16)
};
} else {
retries--;
if (retries > 0) {
console.log('Retrying generation...');
return generateJWK().then(jwkValidator);
} else {
return onError;
}
}
}
return generateJWK().then(jwkValidator).catch((e) => {
console.error("Error generating RSA private key:", e);
return onError;
});
} catch (e) {
console.error("Error generating RSA private key:", e);
return Promise.resolve(onError)
}
}
window.generateRSAPrivateKeyObject = generateRSAPrivateKeyObject;
大多数现代实现(显然像 WebCrypto API)使用 Carmichael 而不是 Euler totient 函数。
如果应用 Carmichael 的 totient 函数
lcm(p - 1, q - 1)
而不是 Euler 的 totient 函数 (p - 1, q - 1)
,脚本中的公共指数会匹配,请参阅 here 了解有关两个函数之间差异的更多详细信息。
以下脚本基于您的脚本,但使用 Carmichael 函数,最多运行测试 10 次,如果计算的指数与 WebCrypto 指数不同则终止。
每次执行时测试都会运行到最后,证明指数始终匹配。
(async () => {
function generateRSAPrivateKeyObject() {
const r16 = 16;
const onError = {
modulus: BigInt(0).toString(r16),
privateExponent: BigInt(0).toString(r16),
p: BigInt(0).toString(r16),
q: BigInt(0).toString(r16),
};
try {
function modInverse(a, m) {
let m0 = m;
let x0 = 0n;
let x1 = 1n;
while (a > 1n) {
const q = a / m;
let t = m;
m = a % m;
a = t;
t = x0;
x0 = x1 - q * x0;
x1 = t;
}
if (x1 < 0n) {
x1 += m0;
}
return x1;
}
function b64ToBn(b64) {
const bin = atob(b64);
const hex = [];
bin.split('').forEach(function (ch) {
let h = ch.charCodeAt(0).toString(r16);
if (h.length % 2) { h = '0' + h; }
hex.push(h);
});
return BigInt('0x' + hex.join(''));
}
function urlBase64ToBase64(str) {
const r = str % 4;
if (2 === r) {
str += '==';
} else if (3 === r) {
str += '=';
}
return str.replace(/-/g, '+').replace(/_/g, '/');
}
function base64urlDecode(base64url) {
return b64ToBn(urlBase64ToBase64(base64url));
}
function generateJWK() {
return crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 1024,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 in little-endian
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
)
.then((keyPair) => {
return crypto.subtle.exportKey("jwk", keyPair.privateKey);
})
.then((privateKeyJWK) => {
return {
n: base64urlDecode(privateKeyJWK.n),
p: base64urlDecode(privateKeyJWK.p),
q: base64urlDecode(privateKeyJWK.q),
d: base64urlDecode(privateKeyJWK.d),
e: base64urlDecode(privateKeyJWK.e)
};
})
}
let retries = 10;
function gcd(a, b) {
return b == 0 ? a : gcd(b, a % b);
}
function jwkValidator(jwk) {
const lambda = (jwk.p - 1n) * (jwk.q - 1n) / gcd((jwk.p - 1n), (jwk.q - 1n)); // Carmichael, lcm(a, b) = a * b / gcd(a, b), see https://stackoverflow.com/a/48462473/9014097
const phi = (jwk.p - 1n) * (jwk.q - 1n); // Euler
const pubExp = modInverse(jwk.d, lambda); // match with WebCrypto result
//const pubExp = modInverse(jwk.d, phi); // doesn't match with WebCrypto result
const isExpValid = jwk.e === pubExp;
const isModulusValid = jwk.n === jwk.p * jwk.q;
console.log('public exp', isExpValid, jwk.e, pubExp);
console.log('n == p * q', isModulusValid);
if (!(isExpValid && isModulusValid)) {
return {
modulus: jwk.n.toString(r16),
p: jwk.p.toString(r16),
q: jwk.q.toString(r16),
privateExponent: jwk.d.toString(r16)
};
} else {
retries--;
if (retries > 0) {
console.log('\nRetrying generation...');
return generateJWK().then(jwkValidator);
} else {
return onError;
}
}
}
return generateJWK().then(jwkValidator).catch((e) => {
console.error("Error generating RSA private key:", e);
return onError;
});
} catch (e) {
console.error("Error generating RSA private key:", e);
return Promise.resolve(onError)
}
}
generateRSAPrivateKeyObject = generateRSAPrivateKeyObject();
})();