Skip to content

Commit

Permalink
Remove unneeded crypto methods
Browse files Browse the repository at this point in the history
VAPID is authenticated on mozilla side, assuming we push up a vapid key on subscription create (which we require).

Old RFC implementations aren't wired up and not currently needed.
  • Loading branch information
MGibson1 committed Aug 6, 2024
1 parent 7364c02 commit b515e07
Show file tree
Hide file tree
Showing 2 changed files with 0 additions and 254 deletions.
47 changes: 0 additions & 47 deletions src/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
aesGcmDecrypt,
ecdhDeriveSharedKey,
generateEcKeys,
randomBytes,
parsePrivateJwk,
verifyVapidAuth,
webPushDecryptPrep,
extractPrivateJwk,
} from "./crypto";
Expand Down Expand Up @@ -89,51 +87,6 @@ describe("parsePrivateJwk", () => {
});
});

describe("VerifyVapidAuth", () => {
it("verifies a valid VAPID auth header", async () => {
// https://datatracker.ietf.org/doc/html/rfc8292#section-2.4
const header =
"vapid t=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0.i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA, k=BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs";
const publicKey =
"BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs";
await expect(verifyVapidAuth(header, publicKey)).resolves.toEqual(true);
});
});

describe("ecdhDeriveSharedKey", () => {
it("derives a shared key", async () => {
const publicKey = new Uint8Array([
4, 212, 7, 72, 118, 252, 190, 220, 245, 154, 52, 177, 252, 15, 23, 133, 156, 239, 180, 143,
238, 35, 90, 17, 113, 37, 51, 202, 227, 65, 216, 90, 65, 164, 147, 8, 238, 157, 148, 51, 109,
61, 222, 177, 105, 70, 150, 45, 212, 238, 129, 62, 121, 29, 29, 181, 81, 11, 242, 181, 219,
56, 159, 236, 125,
]);
const localKeys = await parsePrivateJwk({
key_ops: ["deriveKey", "deriveBits"],
ext: true,
kty: "EC",
x: "84p2j3B4ulNBhJmjcrIsJl0pax3MaYYk6eqk1HYsN_Y",
y: "cZCKmCjy4grDsBrGXkpUikHv2VZmen8SRmclj244OtY",
crv: "P-256",
d: "EZdq8BiFjHbl6U6F0iK0yF8nXvw8-6mGjto9E_2fpwo",
});
if (localKeys === null) {
fail("localKeys is null");
}
const secret = new Uint8Array(16);
const senderKey = fromBufferToUrlB64(publicKey.buffer);
const salt = fromBufferToUrlB64(secret.buffer); // In practice this is a random value, not linked to secret

const sharedKeys = await ecdhDeriveSharedKey(localKeys, secret, senderKey, salt);
expect(sharedKeys.contentEncryptionKey).toEqualBuffer(
new Uint8Array([48, 0, 223, 95, 172, 79, 172, 31, 184, 11, 61, 5, 68, 120, 86, 62]),
);
expect(sharedKeys.nonce).toEqualBuffer(
new Uint8Array([201, 196, 98, 239, 12, 215, 67, 233, 119, 119, 11, 191]),
);
});
});

describe("webPushDecryptPrep", () => {
// https://datatracker.ietf.org/doc/html/rfc8291#section-5
it("recreates the RFC example", async () => {
Expand Down
207 changes: 0 additions & 207 deletions src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as crypto from "crypto";
import type { Jsonify } from "type-fest";

import { CsprngArray, ECKeyPair, UncompressedPublicKey } from "./crypto-types";
import { fromUrlB64ToBuffer, fromUtf8ToBuffer } from "./string-manipulation";

const subtle = crypto.webcrypto.subtle;

Expand Down Expand Up @@ -119,81 +118,6 @@ export async function parsePrivateJwk(jwk: Jsonify<JsonWebKey> | null): Promise<
};
}

/**
* Verifies a VAPID auth header according to RFC-8292
* https://datatracker.ietf.org/doc/html/rfc8292
* Currently only supports EC P-256 signatures in the form of
* `vapid t=..., k=...`
*
* @todo handle earlier drafts of the RFC ()
*
* @param authHeader The value of the Authorization header
* @param vapidPublicKey the public key to verify the signature with
* @returns true if the signature is valid, false otherwise
*/
export async function verifyVapidAuth(
authHeader: string,
vapidPublicKey: string,
): Promise<boolean> {
const parts = authHeader.split(" ");
if (parts.length !== 3 || parts[0] !== "vapid") {
return false;
}

const t = parts
.find((p) => p.startsWith("t="))
?.split(",")[0]
.substring(2);
const k = parts
.find((p) => p.startsWith("k="))
?.split(",")[0]
.substring(2);

if (!t || !k || t.length === 0 || k.length === 0 || k !== vapidPublicKey) {
return false;
}

const tParts = t.split(".");

if (tParts.length !== 3) {
return false;
}

const [header, body, signature] = tParts;
const unsigned = `${header}.${body}`;

const result = await ecVerify(unsigned, signature, vapidPublicKey);
return result;
}

/**
* Verifies a signature was signed with the partner private key of the given public key.
*
* Uses the ECDSA algorithm with the P-256 curve and SHA-256 hash.
*
* @param data data that was signed
* @param signature signature to verify
* @param publicKey public key to verify the signature with
* @returns
*/
async function ecVerify(data: string, signature: string, publicKey: string): Promise<boolean> {
const publicKeyBuffer = fromUrlB64ToBuffer(publicKey);
const key = await subtle.importKey(
"raw",
publicKeyBuffer,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["verify"],
);
return await subtle.verify(
{ name: "ECDSA", hash: { name: "SHA-256" } },
key,
fromUrlB64ToBuffer(signature),
fromUtf8ToBuffer(data),
);
// }
}

/**
* Derives a shared secret following RFC-8291 and RFC-8188
* https://datatracker.ietf.org/doc/html/rfc8291
Expand Down Expand Up @@ -284,137 +208,6 @@ export async function webPushDecryptPrep(
};
}

/**
* Derives a shared secret following older versions of RFC-8291 and RFC-8188 (v4 and V3)
* https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-04
* https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-encryption-encoding-03
*
* @remarks @see {@link webPushDecryptPrep} for the up-to-date implementation
*
* @param ecKeys Local EC key pair
* @param secret Secret shared with the remote server, used as a salt in deriving a shared prk, which is then expanded into a content encryption key and nonce
* @param otherPublicKey The remote server's public key
* @param salt The salt used in the HKDF expansion of the content encryption key and nonce
* @returns
*/
export async function ecdhDeriveSharedKey(
ecKeys: ECKeyPair,
secret: ArrayBuffer,
otherPublicKey: string,
salt: string,
): Promise<{
contentEncryptionKey: ArrayBuffer;
nonce: ArrayBuffer;
}> {
const recipientPublicKey = ecKeys.uncompressedPublicKey;
const senderPublicKey = await subtle.importKey(
"raw",
fromUrlB64ToBuffer(otherPublicKey),
{ name: "ECDH", namedCurve: "P-256" },
true,
[],
);

const ecdhSecret = await subtle.deriveBits(
{
name: "ECDH",
public: senderPublicKey,
},
ecKeys.privateKey as CryptoKey,
256,
);

const ecdhSecretKey = await subtle.importKey("raw", ecdhSecret, { name: "HKDF" }, false, [
"deriveBits",
"deriveKey",
]);

const prk = await subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
salt: secret,
info: fromUtf8ToBuffer("Content-Encoding: auth\0"),
},
ecdhSecretKey,
256,
);

const prkKey = await subtle.importKey("raw", prk, { name: "HKDF" }, false, ["deriveBits"]);

const contentEncryptionKey = await subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
info: createInfoV3("aesgcm", recipientPublicKey, fromUrlB64ToBuffer(otherPublicKey)),
salt: fromUrlB64ToBuffer(salt),
},
prkKey,
// hkdfDerivedSecret,
128,
);

const nonce = await subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
info: createInfoV3("nonce", recipientPublicKey, fromUrlB64ToBuffer(otherPublicKey)),
salt: fromUrlB64ToBuffer(salt),
},
prkKey,
// hkdfDerivedSecret,
96,
);

return {
contentEncryptionKey,
nonce,
};
}

/**
* Creates key info for the HKDF key derivation function following RFC-8291 v4, not the final version
* @param type
* @param clientPublicKey
* @param serverPublicKey
* @returns
*/
function createInfoV3(
type: string,
clientPublicKey: ArrayBuffer,
serverPublicKey: ArrayBuffer,
): Uint8Array {
const len = type.length;
const clientPublicKeyBuffer = new Uint8Array(clientPublicKey);
const serverPublicKeyBuffer = new Uint8Array(serverPublicKey);

const info = new Uint8Array(18 + len + 1 + 5 + 1 + 2 + 65 + 2 + 65);

const encoder = new TextEncoder();
const typeBuffer = encoder.encode(type);

// The string 'Content-Encoding: ', as utf-8
info.set(encoder.encode("Content-Encoding: "));
// The 'type' of the record, a utf-8 string
info.set(typeBuffer, 18);
// A single null-byte
info.set(encoder.encode("\0"), 18 + len);
// The string 'P-256', declaring the elliptic curve being used
info.set(encoder.encode("P-256"), 19 + len);
// A single null-byte
info.set(encoder.encode("\0"), 24 + len);
// The length of the client's public key as a 16-bit integer
info.set(new Uint16Array([clientPublicKeyBuffer.length]), 25 + len);
// Now the actual client public key
info.set(clientPublicKeyBuffer, 27 + len);
// Length of our public key
info.set(new Uint16Array([serverPublicKeyBuffer.length]), 92 + len);
// The key itself
info.set(serverPublicKeyBuffer, 94 + len);

return info;
}

/**
* Creates key info for the HKDF key derivation function following RFC-8291
* @param type
Expand Down

0 comments on commit b515e07

Please sign in to comment.