当前位置: 首页 > 知识库问答 >
问题:

JWT私钥/公钥混淆

杨飞飙
2023-03-14

我试图理解使用带有私钥/公钥(RS512)的JSON Web令牌的逻辑,当将从客户端(在本例中是React Native App)发送到服务器的数据有效载荷签名时。

我认为私钥/公钥的全部意义在于将私钥保持为私钥(在我的服务器上),并将公钥交给成功登录应用程序的人。

我认为,对于我的服务器的每个API请求,经过身份验证的应用程序用户将使用公钥创建JWT(在客户端),服务器将使用私钥验证来自API请求的签名/有效负载。

我似乎把它倒过来了,因为我读到的每一个地方,你都应该用私钥签署JWT——但这违背了我对谁拥有这些密钥的理解。

根据密钥的创建方式,一些私钥可以具有应该是秘密的密码!因此,如果私钥和秘密公开(在客户端代码中),那会有多安全?

加密是从哪里来的?如果应用程序的用户在API中发送敏感数据,我是否应该加密负载并在客户端使用JWT签名,然后让服务器验证JWT签名并解密数据?

本教程很有帮助https://medium.com/@Siddhartac6/json-web-token-jwt-the-right-way-of-implementation-with-node-js-65b8915d550e,但它似乎是倒退的。

任何解释肯定会有帮助,因为所有的在线教程都没有意义。

非常感谢。

共有2个答案

谭曦
2023-03-14

最难的部分是找到一种同时适用于RN和节点的方法,因为我不能只在RN中使用任何节点库,所以我在应用程序和服务器上安装的加密和jwt包必须一起工作。

我正在通过HTTPS传输所有的应用编程接口调用,所以加密部分可能是过度杀伤性的,正如弗洛伦特所提到的。

方法#1更干净,但我更喜欢方法#2,因为它让我可以更好地控制什么是加密的,以及如何创建JWT。这让我在尝试解密有效载荷之前验证JWT。

赞成的意见:

  1. 符合公认的加密/安全标准

欺骗:

  1. 我看不到定制JWT的方法。在制作一个简单的JWT时,我可以将显式过期时间设置为60秒或更短。我看不出用JWE能做到这一点
import {JWK, JWE} from 'react-native-jose';

/**
 * Create JWE encrypted web token
 *
 * @param payload
 * @returns {Promise<string>}
 */
async function createJWEToken(payload = {}) {

  // This is the Public Key created at login. It is stored in the App.  
  // I'm hard-coding the key here just for convenience but normally it 
  // would be kept in a Keychain, a flat file on the mobile device, or 
  // in React state to refer to before making the API call.

  const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApl9FLYsLnP10T98mT70e
qdAeHA8qDU5rmY8YFFlcOcy2q1dijpgfop8WyHu1ULufJJXm0PV20/J9BD2HqTAK
DZ+/qTv4glDJjyIlo/PIhehQJqSrdIim4fjuwkax9FOCuFQ9nesv32hZ6rbFjETe
QSxUPjNzsYGOuULWSR3cI8FuV9InlSZQ7q6dEunLPRf/rZujxiAxGzY8zrMehjM5
LNdl7qDEOsc109Yy3HBbOwUdJyyTg/GRPwklLogw9kkldz5+wMvwOT38IlkO2rCr
qJpqqt1KmxdOQNbeGwNzZiGiuYIdiQWjilq5a5K9e75z+Uivx+G3LfTxSAnebPlE
LwIDAQAB
-----END PUBLIC KEY-----`;

  try {

    const makeKey = pem => JWK.asKey(pem, 'pem');
    const key = await makeKey(publicKey);

    // This returns the encrypted JWE string

    return await JWE.createEncrypt({
      zip:    true,
      format: 'compact',
    }, key).update(JSON.stringify(payload)).final();

  } catch (err) {
    throw new Error(err.message);
  }

}
const keygen = require('generate-rsa-keypair');
const {JWK, JWE} = require('node-jose');

/**
 * Create private/public keys for JWE encrypt/decrypt
 *
 * @returns {Promise<object>}
 *
 */
async function createKeys() {

  // When user logs in, create a standard RSA key-pair.
  // The public key is returned to the user when he logs in.
  // The private key stays on the server to decrypt the message with each API call.
  // Keys are destroyed when the user logs out.

  const keys = keygen();
  const publicKey = keys.public;
  const privateKey = keys.private;

  return {
    publicKey,
    privateKey
  };

}

/**
 * Decrypt JWE Web Token
 *
 * @param input
 * @returns {Promise<object>}
 */
async function decryptJWEToken(input) {

  // This is the Private Key kept on the server.  This was
  // the key created along with the Public Key after login.
  // The public key was sent to the App and the Private Key
  // stays on the server.
  // I'm hard-coding the key here just for convenience but 
  // normally it would be held in a database to 
  // refer during the API call.

  const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApl9FLYsLnP10T98mT70eqdAeHA8qDU5rmY8YFFlcOcy2q1di
jpgfop8WyHu1ULufJJXm0PV20/J9BD2HqTAKDZ+/qTv4glDJjyIlo/PIhehQJqSr
dIim4fjuwkax9FOCuFQ9nesv32hZ6rbFjETeQSxUPjNzsYGOuULWSR3cI8FuV9In
lSZQ7q6dEunLPRf/rZujxiAxGzY8zrMehjM5LNdl7qDEOsc109Yy3HBbOwUdJyyT
g/GRPwklLogw9kkldz5+wMvwOT38IlkO2rCrqJpqqt1KmxdOQNbeGwNzZiGiuYId
iQWjilq5a5K9e75z+Uivx+G3LfTxSAnebPlELwIDAQABAoIBAQCmJ2FkMYhAmhOO
LRMK8ZntB876QN7DeT0WmAT5VaE4jE0mY1gnhp+Zfn53bKzQ2v/9vsNMjsjEtVjL
YlPY0QRJRPBZqG3wX5RcoUKsMaxip3dckHo3IL5h0YVJeucAVmKnimIbE6W03Xdn
ZG94PdMljYr4r9PsQ7JxLOHrFaoj/c7Dc7rd6M5cNtmcozqZsz6zVtqO1PGaNa4p
5mAj9UHtumIb49e3tHxr//JUwZv2Gqik0RKkjkrnUmFpHX4N+f81RLDnKsY4+wyI
bM5Gwq/2t8suZbwfHNFufytaRnRFjk+P6crPIpcfe05Xc+Y+Wq4yL62VY3wSS13C
EeUZ2FXpAoGBANPtw8De96TXsxdHcbmameWv4uepHUrYKq+7H+pJEGIfJf/1wsJ0
Gc6w2AE69WJVvCtTzP9XZmfiIze2sMR/ynhbUl9wOzakFpEh0+AmJUG+lUHOy4k2
Mdmu6GmeIM9azz6EXyfXuSZ39LHowS0Es1xaWRuu5kta73B5efz/hz2tAoGBAMj4
QR87z14tF6dPG+/OVM/hh9H5laKMaKCbesoXjvcRVkvi7sW8MbfxVlaRCpLbsSOs
cvAkc4oPY+iQt8fJWSJ1nwGJ0g7iuObLJh9w6P5C3udCGLcvqNbmQ9r+edy1IDBr
t7pdrFKiPFvaEEqYl06gVSsPCg041N6bRTJ1nEzLAoGAajSOVDqo6lA6bOEd6gDD
PSr+0E+c4WQhSD3Dibqh3jpz5aj4uFBMmptfNIaicGw8x43QfuoC5O6b7ZC9V0wf
YF+LkU6CLijfMk48iuky5Jao3/jNYW7qXofb6woWsTN2BoN52FKwc8nLs9jL7k6b
wB166Hem636f3cLS0moQEWUCgYABWjJN/IALuS/0j0K33WKSt4jLb+uC2YEGu6Ua
4Qe0P+idwBwtNnP7MeOL15QDovjRLaLkXMpuPmZEtVyXOpKf+bylLQE92ma2Ht3V
zlOzCk4nrjkuWmK/d3MzcQzu4EUkLkVhOqojMDZJw/DiH569B7UrAgHmTuCX0uGn
UkVH+wKBgQCJ+z527LXiV1l9C0wQ6q8lrq7iVE1dqeCY1sOFLmg/NlYooO1t5oYM
bNDYOkFMzHTOeTUwbuEbCO5CEAj4psfcorTQijMVy3gSDJUuf+gKMzVubzzmfQkV
syUSjC+swH6T0SiEFYlU1FTqTGKsOM68huorD/HEX64Bt9mMBFiVyA==
-----END RSA PRIVATE KEY-----`;

  try {

    const makeKey = pem => JWK.asKey(pem, 'pem');
    const key = await makeKey(privateKey);

    // This returns the decrypted data

    return await JWE.createDecrypt(key).decrypt(input);

  } catch (err) {
    throw new Error(err.message);
  }

}

下面的JS展示了我如何创建JWT并在React Native中加密数据,然后验证令牌并在节点后端解密数据。

当用户登录时,我将为JWT传回一个密钥,并为加密(如果需要)传回一个单独的密钥,该密钥仅在会话期间有效。他们注销后,密钥被销毁。

为了安全起见,我还将JWT的过期时间保持在60秒左右。

赞成的意见:

在设置过期时间方面,您可以对JWT进行更多的控制。

欺骗:

可能会更慢,因为您必须在单独的步骤中创建JWT并加密有效负载。

import jwt from 'react-native-pure-jwt';
import CryptoJS from 'react-native-crypto-js';

/**
 * Create JWT Signature from payload for API call
 *
 * @param payload {object|string} - payload to add to JWT
 * @param encrypt {boolean} - encrypt the payload
 *
 */
async function createJWT(payload, encrypt = false) {

  try {

    const signature = await jwt.sign({

        // REQUIRED: Payload
        // Any data you want to pass in the JWT
        // Two options:
        // 1. Send the payload without encryption.
        //      No need to stringify payload. Just send it as-is.
        // 2. Send the payload as an encrypted string.
        //      Use the CryptoJS library with a 'another-secret-key' that the App and the server share.
        //      Payload needs to be stringified.

        data: encrypt ? CryptoJS.AES.encrypt(JSON.stringify(payload), 'another-secret-key').toString() : payload,

        // REQUIRED: Expires Time
        // Milliseconds since 1970 when the token can no longer be decoded.
        // Here, I've set the value to however many seconds I want the token to last.
        // The clock starts ticking from the moment this token was created.  Afer the time has expired,
        // the token can't be decoded by anyone.

        exp: new Date().getTime() + 60 * 1000,

        // OPTIONAL: Issuer Claim
        // String that identifies whoever issued this JWT.
        // The JWT is coming from my App so I'll use the App name here.

        iss: 'APP ID',

        // OPTIONAL: Subject Claim
        // String that uniquely identifies the subject of the JWT.
        // The I'll probably use the Username or User ID of the person using the App.

        sub: 'UNIQUE USER ID',

      },

      // REQUIRED: Secret Key
      // This is the string that is used to encode/decode the JWT.
      // Here, I'll probably use something that's unique to the user's account on the App such
      // as a password or strong token that's stored in the user's Keychain and that's
      // also in a database the server can retrieve.

      'my-secret-key',

      {

        // REQUIRED: Algorithm
        // What algorithm is being used to encode the JWT
        // The supported algorithms by react-native-pure-jwt are HS256, HS384, HS512.
        // The higher the number, the better the security but it will also take longer to encode/decode the token.
        // I would prefer to use RS256 or RS512 but those algorithms aren't supported
        // by the react-native-pure-jwt package as far as I can tell.

        alg: 'HS256',

      },
    );

    return signature;

  } catch (err) {

    throw new Error(err.message);

  }

}
const jwt = require('jsonwebtoken');
const CryptoJS = require('crypto-js');

/**
 * Decode JWT Signature from API call
 *
 * @param token {string}
 * @param decrypt {boolean} - decrypt the payload
 *
 */
async function decodeJWT(token, decrypt = false) {

  // Use the same options to decode the JWT that were used to encode the JWT.

  const options = {
    issuer:  'APP ID',
    subject: 'UNIQUE USER ID',
  };

  // Use the same Secret Key that was used to encode the JWT.
  // This should be a password or some hidden value that is only
  // known by the App and the Server such as the user's password or previously
  // agreed upon strong token shared after login and discarded after logout.

  const secretJWTKey = 'my-secret-key';

  // If decrypting an encrypted payload, you'll also need the encryption secret
  // which should ideally not be the same as the secretJWTKey.
  // It can also be some hidden value that is only known by the App and the Server such
  // an agreed upon strong token shared after login and discarded after logout.

  const secretDecryptKey = 'another-secret-key';

  return new Promise((resolve, reject) => {

    try {

      // First check if the token is valid. Otherwise, it will throw an error.

      const verified = jwt.verify(token, secretJWTKey, options);

      let data = verified.data;

      if (decrypt) {

        // If the payload is encrypted, unwind it into an object or string.

        const bytes = CryptoJS.AES.decrypt(data, secretDecryptKey);
        data = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));

      }

      resolve(data);

    } catch (err) {

      // If the token is not valid or the decryption failed.

      reject(err.message);

    }

  });

}
async function test() {

  // Create the JWT and encrypt the payload (optional) in React Native

  const tokenSignature = await createJWT({
    name: 'Marc',
    password: 'P@55w0rd',
    cc: 'Visa',
    cc_num: '4400-0000-0000-0000'
    phone: '704-000-0000'
    info: {
      birthdate: '1970-00-00',
      ssn: '000-00-0000',
    },
  }, true).catch(err => {

    console.log('Error creating signature', err);

  });

  // Use the JWT signature to make the https api call.

  // On the server...
  // Decode the JWT and decrypt the payload (optional)

  const data = await decodeJWT(tokenSignature, true).catch(err => {

    console.log('Error retrieving data', err);

  });

  // Use the data to update the database, etc.

  console.log({data});

}
朱皓
2023-03-14

使用JWT,关键材料的拥有和使用与发生密码操作的任何其他上下文完全相同。

用于签署:

  • 私钥由颁发者拥有

用于加密:

  • 私钥归收件人所有
  • 公钥可以共享给希望向收件人发送敏感数据的任何一方

加密很少用于JWT。大多数时候,HTTPS层已经足够了,令牌本身只包含一些不敏感的信息(数据、标识...)。令牌的发行者(身份验证服务器)拥有用于生成签名令牌(JWS)的私钥。这些令牌被发送到客户端(API服务器、Web/本地应用程序...)。客户端可以使用公钥验证令牌。

如果您有不应向第三方披露的敏感数据(电话号码、个人地址……),然后强烈建议使用加密令牌(JWE)。在这种情况下,每个客户端(即令牌的接收者)应具有私钥,令牌的发行者必须使用每个接收者的公钥加密令牌。这意味着令牌的颁发者可以为给定的客户端选择适当的密钥。

 类似资料:
  • Auth0提供了两个JWT库,一个用于Node:Node jsonwebtoken,另一个用于Java:Java JWT。 我创建了私有/公钥对,并在Node中成功地使用了node-jsonwebToken: 但是我发现没有办法在java-jwtJava做同样的事情。 有人有一个如何在Java中为JWT使用私钥/公钥的工作示例吗?

  • 我明白JWT规范没有涵盖这一点:但我很想知道在实际的JWT使用中有哪些常见的做法。 当然,这个问题与HTTPS中涉及的证书无关:我只是在讨论JWT签名和加密中使用的密钥。

  • 我正在实施类似于:https://login.microsoftonline.com/common/discovery/v2.0/keys Spring boot JWT应用程序,用于生成和验证JWT令牌。我将生成许多公钥/私钥(我不想用一个密钥生成所有令牌。一个密钥对将生成生命周期较短的令牌,第二个密钥对将生成更长的令牌…)我将使用公钥创建endpoint/密钥。问题是:如何在我的应用程序中连接

  • 我和JWT有一些安全问题。事情是这样的: 首先是创建一个只有一个密钥的令牌,并使用它来注册和验证令牌-(这是许多开发使用的规则)。 第二种是使用私钥和带有私钥的公钥进行注册,创建新的令牌,并且只使用公钥进行令牌验证。点击这里查看第二条规则中的图像 所以我的问题是哪两种方式更安全?第二条安全规则真的必要吗?谢谢

  • Auth0提供了两个JWT库,一个用于Node:Node jsonwebtoken,另一个用于Java:Java JWT。事实证明,JavaJWT不支持公钥/私钥对。 然而,另一个java库jjwt库声称支持该特性。但是,该文档没有显示如何在jjwt中使用自己的公钥/私钥对。 我创建了私有/公钥对,并在Node中成功地使用了node-jsonwebToken: 但是我发现在Java中用JWT无法做

  • 我使用secp192r1曲线生成了ECC公钥和私钥。公共密钥数组长度为75,私有密钥数组长度125。为什么私钥比公钥长?为什么私钥的长度不是公钥的两倍?为什么由于secp192r1私钥不是192位=24字节?