JWT私钥/公钥混淆

时间:2020-03-05 04:06:11

标签: jwt

在尝试对从客户端(在本例中为React Native App)发送到我的服务器的数据进行签名时,我想了解使用带有私钥/公钥(RS512)的JSON Web令牌的逻辑。

我认为私钥/公钥的全部目的是将 private 私钥保留在我的服务器上,并将 public 密钥交给成功登录的人进入应用程序。

我认为,对于服务器的每个API请求,经过身份验证的应用程序用户将使用 public 密钥创建JWT(在客户端),而服务器将使用专用密钥以验证API请求中的签名/有效载荷。

我似乎倒退了,因为在我阅读的所有地方,都应该使用 private 密钥对JWT进行签名-但这与我对谁拥有密钥的理解背道而驰。 / p>

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

加密从何而来?如果应用程序的用户正在使用API​​发送敏感数据,我是否应该加密有效负载并在客户端使用JWT对其进行签名,然后让服务器验证JWT签名并解密数据?

本教程对https://medium.com/@siddharthac6/json-web-token-jwt-the-right-way-of-implementing-with-node-js-65b8915d550e很有帮助,但似乎倒退了。

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

谢谢。

2 个答案:

答案 0 :(得分:2)

在JWT中,密钥材料的拥有和使用与发生密码操作的任何其他情况完全相同。

用于签名:

  • 私钥由发行者拥有。
  • 公钥可以与需要验证签名的所有各方共享。

对于加密:

  • 私钥归接收者所有
  • 可以将公共密钥共享给希望向接收者发送敏感数据的任何一方。

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

如果您有不应该透露给第三方的敏感数据(电话号码,个人地址...),那么强烈建议您使用加密令牌(JWE)。 在这种情况下,每个客户(即令牌的接收者)都应拥有一个私钥,令牌的发行者必须使用每个接收者的公钥对令牌进行加密。这意味着令牌的发行者可以为给定的客户选择适当的密钥。

答案 1 :(得分:1)

React Native和Node Backend中的JWT和JWE解决方案

最困难的部分是找到一种在RN和Node中都可以使用的方法,因为我不能只在RN中使用任何Node库,因此我在App和服务器上安装的crypto和jwt软件包必须协同工作。 / p>

我正在通过HTTPS传输所有API调用,因此,正如Florent所述,无论如何加密部分都可能是过分的。

方法1更干净,但我更喜欢方法2,因为它使我可以更好地控制加密内容和JWT的创建方式。这使我可以在尝试解密有效负载之前之前验证JWT。

方法#1

创建一个JWE以同时加密令牌和有效负载。

优点:

  1. 符合公认的加密/安全标准。
  2. 更少的代码。
  3. 不需要跟踪秘密密码,因为我们仅使用服务器和应用程序仅在登录时创建的公钥/私钥对。

缺点:

  1. 我看不到自定义JWT的方法。制作简单的JWT时,我可以将明确的到期时间设置为60s或更短。我看不到用JWE做到这一点的方法。
  2. 解密JWE时抛出的错误没有帮助。它会工作或失败。使用JWT解密,如果发生某些故障或无效,则会收到良好的错误消息。

反应本机应用代码

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);
  }

}

方法#2

创建一个JWT并自己加密有效负载。

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

当用户登录时,我将传回JWT的密钥和用于加密的单独密钥(如有必要),该密钥仅在会话期间有效。注销后,密钥将被销毁。

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

优点:

在设置到期时间方面,您可以更好地控制JWT。

缺点:

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

React Native App

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});

}