Skip to content

Commit

Permalink
add createRequestDecryptor.getJWKMetadata method (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bamieh authored Mar 1, 2021
1 parent d504613 commit cfe17ac
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
coverage
.nyc_output
test
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ jwksManager.getPublicJWKS();
jwksManager.getPrivateJWKS();
```

### Getting JWK metadata from request

The method `getJWKMetadata` returns the metadata of the JWK used to encrypt the request body.

The metadata is an object including the following:
- `key`: JWK details (`kid`, `length`, `kty`, `use`, `alg`)
- `protected` an array of the member names from the "protected" member.
- `header`: an object of "protected" member key values.

```js
import { createRequestDecryptor } from '@elastic/request-crypto';
import privateJWKS from './privateJWKS';

async function handler (event, context, callback) {
const requestDecryptor = await createRequestDecryptor(privateJWKS);
const jwkMetadata = await requestDecryptor.getJWKMetadata(event.body);

// ... use metadata
}
```

If the key is not in the provided JWKS the function will throw an error `Error: no key found`.

### RFCs followed for implementation details

- JWK RFC: https://tools.ietf.org/html/rfc7517
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@elastic/request-crypto",
"version": "1.1.4",
"version": "1.2.0",
"description": "Request Cryptography",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
25 changes: 21 additions & 4 deletions src/jwks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface PrivateJWK extends PublicJWK {
qi: string;
}

export type JWKMetadata = Pick<jose.JWK.Key, 'length' | 'kty' | 'kid' | 'use' | 'alg'>;

export type KeyUse = 'enc' | 'desc';
export type PublicJWKS = JWKS<PublicJWK>;

Expand All @@ -43,6 +45,21 @@ export type UnsignedJWK = PrivateJWK | PublicJWK;

export const RSA_ALGORITHM = 'RSA-OAEP';

export interface JWKDecryptResult extends jose.JWE.DecryptResult {
/**
* JWK metadata
*/
key: JWKMetadata;
/**
* an object of "protected" member key values.
*/
header: Record<string, string>;
/**
* payload Buffer
*/
payload: Buffer;
}

export class JWKSManager {
public store: any;
public JWK: typeof jose.JWK;
Expand Down Expand Up @@ -93,12 +110,12 @@ export class JWKSManager {
.update(input)
.final();
}
public async decrypt(payload: any, jwks = this.store): Promise<Buffer> {
public async decrypt(payload: any, jwks = this.store): Promise<JWKDecryptResult> {
const decrypter = this.JWE.createDecrypt(jwks);
const decryptedPayload = await decrypter.decrypt(payload);

return (decryptedPayload as any).payload;
const decryptedPayload = (await decrypter.decrypt(payload)) as JWKDecryptResult;
return decryptedPayload;
}

protected getKey(kid?: string): any {
return this.store.get(kid);
}
Expand Down
12 changes: 10 additions & 2 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import makeAESCryptoWith, { EncryptOutput } from '@elastic/node-crypto';
import { util } from 'node-jose';

import { createJWKManager } from './jwk';
import { PrivateJWKS, PublicJWK, PublicJWKS } from './jwks';
import { JWKDecryptResult, PrivateJWKS, PublicJWK, PublicJWKS } from './jwks';
import { generatePassphrase } from './random-bytes';

export interface Encryptor {
Expand All @@ -13,6 +13,9 @@ export interface Decryptor {
getPublicComponent(kid: string): PublicJWK | null;
getWellKnowns(): PublicJWKS;
decrypt(encryptedBody: string): Promise<EncryptOutput | EncryptOutput[]>;
getJWKMetadata(
encryptedBody: string
): Promise<Pick<JWKDecryptResult, 'key' | 'protected' | 'header'>>;
}

export async function createRequestEncryptor(publicJWKS: PublicJWKS): Promise<Encryptor> {
Expand All @@ -39,10 +42,15 @@ export async function createRequestDecryptor(privateJWKS: PrivateJWKS): Promise<
},
async decrypt(encryptedBody: string) {
const { encryptedAESKey, encryptedPayload } = unpackBody(encryptedBody);
const encryptionKeyBuffer = await jwkManager.decrypt(encryptedAESKey);
const { payload: encryptionKeyBuffer } = await jwkManager.decrypt(encryptedAESKey);
const AES = makeAESCryptoWith({ encryptionKey: encryptionKeyBuffer });
return AES.decrypt(encryptedPayload);
},
async getJWKMetadata(encryptedBody: string) {
const { encryptedAESKey } = unpackBody(encryptedBody);
const { key, protected: protectedFields, header } = await jwkManager.decrypt(encryptedAESKey);
return { key, protected: protectedFields, header };
},
};
}

Expand Down
7 changes: 7 additions & 0 deletions test/fixture/encrypted_jwk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const encryptedRequest = {
kidUsed: 'KIBANA_7.0',
encryptedKey:
'eyJ6aXAiOiJERUYiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBLU9BRVAiLCJraWQiOiJLSUJBTkFfNy4wIn0.c6kmW2---aZsZcGA4_fnAbNWwIPOJIXR86H8Sr9N0v7qQ4TPV-T0XswkpGvVSashO5zik_1zipTAC5OGBhsSAnlgxs9ilFKXwdAMG7qf8tWKPWu37HhUQx1sgBv-vEM_Wm8_TxPpJGfhi3JOiTEawqQ4UBQKM8Fj8tcnK9dv1SU.rMvA-6-VJZeer70yAjnDmw.EdnaGJOOMaDmUDvE7ujW0A.Bp6KvXjNTtFRJnNawKFvSA',
encryptedPayload:
'eyJlbmNyeXB0ZWRBRVNLZXkiOiJleUo2YVhBaU9pSkVSVVlpTENKbGJtTWlPaUpCTVRJNFEwSkRMVWhUTWpVMklpd2lZV3huSWpvaVVsTkJMVTlCUlZBaUxDSnJhV1FpT2lKTFNVSkJUa0ZmTnk0d0luMC5ZUWxFUWFacmFuS2NLakZYNUE2eUphVGVILVFyeGdpTzdfZ3lfbm1zMWM0WFV0ZTBZdUFTYmpOb0xaYlduQnRBNnRwV0FKbU5tTEl1NUdrM1FaYnVZWFVFSlpTeEw5bVZSejI0b1JUZURMQ2RNNG5QWlZGZEQwRXdIbGVPY1p4NURPNHQ5WVVzNkNyQUFkQWtSVTNhcnZqQ2NXcWZMdDJNRFM0Q05IaHZ1Zm8udGJEcjFXY3NZSERMdERkMzNqQXJiQS5HQjBTUTZpbThsXzZ1a3FWem55N1pKel9WM0dpUkRsdDVHSGdHRW9oZmN6dXM3OHVXeGdqTFdyLUhZRHE1QzlrLl9tR2ZZNEdtM2VsLWtQcUU4U3k3cXciLCJlbmNyeXB0ZWRQYXlsb2FkIjoiZWcvVVFkMk1KRUZrTmdNWkVROG9EUmpPRXNNb3ZXZGVDMWZOT2lFYXVZbjNqNFhTTEI3aVk4Z0ZBTWt1RENEcm9SYXdHcWM2dXgyeXVJVGZxaS9DWjZFa0FDUG53eWcvM0lOU2tROUFOMGlsSXFxUjEyc2lWdnFYM2VvNUtCcEJSZTFMWUxmRTdMNDE4OE1PRzhoWmoraW00NitqN3NhVnZUST0ifQ',
};
26 changes: 24 additions & 2 deletions test/jwk.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as jose from 'node-jose';
import { createJWKManager, JWKManager } from '../src/jwk';
import { createJWKManager } from '../src/jwk';

import { privateJWKS } from './fixture/private_jwks';
import { publicJWKS } from './fixture/public_jwks';
Expand Down Expand Up @@ -59,10 +59,32 @@ describe('JSON Web Keys Manager', () => {
});
it('decrypts messages using private key set', async () => {
const manager = await createJWKManager(privateJWKS, mockJWK);
const messageBuffer = await manager.decrypt(encryptedMessage);
const { payload: messageBuffer } = await manager.decrypt(encryptedMessage);
const messageObject = messageBuffer.toString();
expect(messageObject).to.equal(originalInput);
});
it('returns JWKDecryptResult contract', async () => {
const manager = await createJWKManager(privateJWKS, mockJWK);
const jwkDecryptResult = await manager.decrypt(encryptedMessage);
expect(jwkDecryptResult.header).to.eql({
zip: 'DEF',
enc: 'A128CBC-HS256',
alg: 'RSA-OAEP',
kid: 'KIBANA',
});
expect(jwkDecryptResult.protected).to.eql(['zip', 'enc', 'alg', 'kid']);
expect(Buffer.isBuffer(jwkDecryptResult.plaintext)).to.equal(true);
expect(Buffer.isBuffer(jwkDecryptResult.payload)).to.equal(true);
const { kty, kid, use, alg } = manager.getPublicJWK('KIBANA');
expect(jwkDecryptResult.key).to.eql({
kty,
kid,
use,
alg,
length: 1024,
keystore: {},
});
});
it('cannot decrypt messages not encrypted with matching keys', async () => {
const unworldlyManager = await createJWKManager(undefined, mockJWK);
await unworldlyManager.addKey('KIBANA_7.0');
Expand Down
30 changes: 24 additions & 6 deletions test/request.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { readFile } from 'fs';
import * as path from 'path';
import { promisify } from 'util';

import { createRequestDecryptor, createRequestEncryptor } from '../src/request';

import { privateJWKS } from './fixture/private_jwks';
import { publicJWKS } from './fixture/public_jwks';

import { JWKS, PublicJWK } from '../src';
import { PublicJWK } from '../src';
import { publicComponents } from './helpers';

import { encryptedRequest } from './fixture/encrypted_jwk';
import * as largePayload from './fixture/large_payload.json';
import * as smallPayload from './fixture/small_payload.json';

Expand Down Expand Up @@ -53,7 +50,7 @@ describe('Request Crypto', () => {
});
});

describe('Request Decryption', async () => {
describe('Request Decryption', () => {
it('decrypts small payload with private key', async () => {
const decryptor = await createRequestDecryptor(privateJWKS);
const decryptedPayload = await decryptor.decrypt(encryptedBodyWithSmallPayload);
Expand All @@ -65,4 +62,25 @@ describe('Request Crypto', () => {
expect(decryptedPayload).to.eql(largePayload);
});
});

describe('Request getJWKMetadata', () => {
it('returns jwk metadata from request payload', async () => {
const decryptor = await createRequestDecryptor(privateJWKS);
const jwkMetadata = await decryptor.getJWKMetadata(encryptedBodyWithLargePayload);
expect(jwkMetadata.protected).to.eql(['zip', 'enc', 'alg', 'kid']);
expect(Object.keys(jwkMetadata.header)).to.eql(jwkMetadata.protected);
expect(jwkMetadata.key.kid).to.eql('KIBANA');
});

it('fails to grab metadata of unknown JWK', async () => {
const decryptor = await createRequestDecryptor(privateJWKS);
let errorMessage = '';
try {
await decryptor.getJWKMetadata(encryptedRequest.encryptedPayload);
} catch (err) {
errorMessage = err.toString();
}
expect(errorMessage).to.equal('Error: no key found');
});
});
});

0 comments on commit cfe17ac

Please sign in to comment.