codemonument / deno_audio_logbook

A deno fresh server, with a telegram bot to receive audio messages and a website to show all messages of a user in a calendar
2 stars 0 forks source link

Verify Telegram Auth signature #22

Closed bjesuiter closed 1 year ago

bjesuiter commented 1 year ago

When receiving a telegram auth token, we have to verify it in /auth/callback.tsx.

Docs

Related

Bloodiko commented 1 year ago

Gosh this is Hard. I tried to figure it out, but nobody seems to have done it with WebCrypt publicly yet.

Should Work

    const secrets = await secretsPromise;
    const telegramToken = z.string().parse(secrets.get("TELEGRAM_TOKEN"));

    const encoder = new TextEncoder();

    // hash telegram token with sha256

    const telegramTokenHashed = await crypto.subtle.digest(
      "SHA-256",
      encoder.encode(telegramToken),
    ).then((hash) => new Uint8Array(hash));

    //debug, skip this. 
    const tokenShaHex = Array.from(new Uint8Array(signature))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");
    console.log(tokenShaHex)

    // Telegram Token Hash is correct, confirmed with sha256sum on terminal

tokenShaHex is the same as echo -n "telegramToken"|sha256sum

unclear Part

// generate hmac from hash and authDataCheckString
    const hmac = await crypto.subtle.importKey(
      "raw",
      telegramTokenHashed,
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"],
    );

    const signature = await crypto.subtle.sign(
      "HMAC",
      hmac,
      encoder.encode(authDataCheckString),
    );

    const signatureHex = Array.from(new Uint8Array(signature))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    if (signatureHex !== payload.data.hash) {
      console.log("signatureHex ", signatureHex);
      console.log("payload.data.hash ", payload.data.hash);
      const msg = `Problem while verifying oauth callback payload`;

      return new Response(msg, { status: 403 });
    }

Parsing Usersession to match telegram data for verification.

function buildAuthString(payload: UserSession) {
  const authStringArray = [];

  authStringArray.push(`auth_date=${payload.unixAuthDate}`);
  payload.firstName && authStringArray.push(`first_name=${payload.firstName}`);
  authStringArray.push(`id=${payload.userId}`);
  payload.lastName && authStringArray.push(`last_name=${payload.lastName}`);
  payload.photoUrl && authStringArray.push(`photo_url=${payload.photoUrl}`);
  payload.username && authStringArray.push(`username=${payload.username}`);

  return authStringArray.join("\n");
}

Either

Trying to Verify the UserSession.Hash via echo -n "{parsedUserSessionString with \n between each line, but not at the end}"|openssl sha256 -hmac "sha256sum(TelegramToken)" did not yield any useful results. Could not verify my own callback URL after succesfull login.

bjesuiter commented 1 year ago

The research looks good. Think we have to go over the algortihm together again to find the problem. I also think the problem is with encoding the Params in the thing which gets hashed. Probably our zod validation adds more properties into the object than should be there or otherwise messes with the hashing, so that our comparisons do not match.

Let's see...

bjesuiter commented 1 year ago

Telegram Auth Verification Algorithm excalidraw

bjesuiter commented 1 year ago

Example code for "how to use web crypto for HMAC signing)

Goto TS Playground

let responseString = "";
const encoder = new TextEncoder();

crypto.subtle.importKey(
  "raw",
  encoder.encode("secret"),
  { name: "HMAC", hash: "SHA-256" },
  false,
  ["sign"],
).then((hmac) => {
  responseString = `
    Has crypto: ${crypto !== undefined}, ${crypto}
    Has crypto.subtle: ${crypto.subtle !== undefined}, ${crypto.subtle}
    `;

  return crypto.subtle.sign(
    "HMAC",
    hmac,
    encoder.encode("Hello World!"),
  );
}).then((payloadSignature) => {
  responseString += `
        payloadSignature: ${payloadSignature}
    `;

  const signatureHex = Array.from(new Uint8Array(payloadSignature))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");

  responseString += `
        payloadSignature as Hex: ${signatureHex}
    `;
}).then(() => {
  console.log(responseString);
});

Result hash: 6fa7b4dea28ee348df10f9bb595ad985ff150a4adfd6131cca677d9acee07dc6

Verify with openssl

$ echo -n "Hello World!" | openssl sha256 -hmac "secret"
SHA256(stdin)= 6fa7b4dea28ee348df10f9bb595ad985ff150a4adfd6131cca677d9acee07dc6
bjesuiter commented 1 year ago

Extended Example Code with hashing the secret before using as an hmac key:

Goto TS Playground

let responseString = "";
const encoder = new TextEncoder();

crypto.subtle.digest("SHA-256", encoder.encode("secret"))
  .then((secretHash) => {
    return crypto.subtle.importKey(
      "raw",
      secretHash,
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"],
    );
  })
  .then((hmac) => {
    responseString = `
    Has crypto: ${crypto !== undefined}, ${crypto}
    Has crypto.subtle: ${crypto.subtle !== undefined}, ${crypto.subtle}
    `;

    return crypto.subtle.sign(
      "HMAC",
      hmac,
      encoder.encode("Hello World!"),
    );
  }).then((payloadSignature) => {
    responseString += `
        payloadSignature: ${payloadSignature}
    `;

    const signatureHex = Array.from(new Uint8Array(payloadSignature))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    responseString += `
        payloadSignature as Hex: ${signatureHex}
    `;
  }).then(() => {
    console.log(responseString);
  });

Result Hash: 69a49567d6b9d862eb301f392557ce60bedf0dff3b1589b88c3051812373af7c

Verify with openssl

$ echo -n "Hello World!" | openssl sha256 -mac hmac -macopt hexkey:$(echo -n "secret"|sha256sum)
SHA256(stdin)= 69a49567d6b9d862eb301f392557ce60bedf0dff3b1589b88c3051812373af7c
bjesuiter commented 1 year ago

Fixed by #31