在尝试对从客户端(在本例中为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很有帮助,但似乎倒退了。
任何解释肯定会有所帮助,因为所有的在线教程都没有意义。
谢谢。
答案 0 :(得分:2)
在JWT中,密钥材料的拥有和使用与发生密码操作的任何其他情况完全相同。
用于签名:
对于加密:
JWT很少使用加密。大多数情况下,HTTPS层已足够,令牌本身仅包含一些不敏感的信息(数据时间,ID ...)。 令牌的发行者(认证服务器)具有用于生成签名令牌(JWS)的私钥。这些令牌被发送到客户端(API服务器,Web /本地应用程序...)。 客户可以使用公钥验证令牌。
如果您有不应该透露给第三方的敏感数据(电话号码,个人地址...),那么强烈建议您使用加密令牌(JWE)。 在这种情况下,每个客户(即令牌的接收者)都应拥有一个私钥,令牌的发行者必须使用每个接收者的公钥对令牌进行加密。这意味着令牌的发行者可以为给定的客户选择适当的密钥。
答案 1 :(得分:1)
最困难的部分是找到一种在RN和Node中都可以使用的方法,因为我不能只在RN中使用任何Node库,因此我在App和服务器上安装的crypto和jwt软件包必须协同工作。 / p>
我正在通过HTTPS传输所有API调用,因此,正如Florent所述,无论如何加密部分都可能是过分的。
方法1更干净,但我更喜欢方法2,因为它使我可以更好地控制加密内容和JWT的创建方式。这使我可以在尝试解密有效负载之前之前验证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);
}
}
下面的JS显示了我如何创建JWT并在React Native中加密数据,然后验证令牌并在Node后端解密数据。
当用户登录时,我将传回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});
}