Best Practices for Securely Storing API Keys & Passwords in BTT Scripts

Hello BTT Community,

I frequently use BetterTouchTool’s scripting actions—Run Real JavaScript, Run JavaScript for Automation, and Run AppleScript—and they’ve been incredibly powerful. However, I haven’t found any information on securely handling sensitive credentials (API keys, secrets, passwords, etc.) within these scripts.

Specifically, I’m wondering:

  1. Is there a recommended approach for securely storing or retrieving credentials when writing BTT scripts?
  2. Does BTT offer any built-in functionality (or planned functionality) to manage secrets in a secure manner (e.g., encryption, keychain integration, environment variables, etc.)?
  3. How are others in the BTT community handling this? Are you storing credentials externally, using macOS Keychain, or employing another workaround?

I haven’t come across any official documentation or community discussions on this topic, so I’d love to hear any advice, best practices, or insights. Thank you!

Good idea, I'll add a JS function to save & retrieve from keychain

1 Like

4.206 alpha has added these:

await set_keychain_item("somename", "somepassword");
let result = await get_keychain_item("somename");

It's saved under "BetterTouchToolUser" in keychain

1 Like

Thanks! I need a little help understanding how to use the new interface. For example, I have this JavaScript script where I'm currently storing the accountId and password in plaintext right in the script. How would I modify it to now use the new interface?

(async () => {
  class Logger {
    constructor() {
      this.logLevels = {
        INFO: "INFO",
        ERROR: "ERROR",
      };
    }

    log(message, level = this.logLevels.INFO) {
      const now = new Date();
      const formattedDate = now.toLocaleDateString("en-GB"); // European format
      const formattedTime = now.toLocaleTimeString();
      const timestampStyle = "color: cyan;"; // Cyan
      const levelStyle =
        level === this.logLevels.ERROR ? "color: red;" : "color: green;"; // Red for ERROR, Green for INFO

      console.log(
        `%c${formattedDate} ${formattedTime} - %c${level} - %c${message}`,
        timestampStyle,
        levelStyle,
        "color: white;"
      );
    }
  }

  const logger = new Logger();
  const BASE_URL = "https://share2.dexcom.com";

  try {
    const loginHeaders = {
      "Content-Type": "application/json",
    };

    // How to use the new interface here?
    const loginPayload = JSON.stringify({
      accountId: "redacted",
      password: "redacted",
      applicationId: "d89443d2-327c-4a6f-89e5-496bbb0317db",
    });

    const loginRequestOptions = {
      method: "POST",
      headers: loginHeaders,
      body: loginPayload,
      redirect: "follow",
    };

    let sessionId = "";
    try {
      logger.log("Attempting to log in...");
      const loginResponse = await fetch(
        `${BASE_URL}/ShareWebServices/Services/General/LoginPublisherAccountById`,
        loginRequestOptions
      );
      const sessionResponse = await loginResponse.json();
      sessionId = sessionResponse ? sessionResponse : "";
      logger.log(`Login successful, sessionId: ${sessionId}`);
    } catch (loginError) {
      logger.log(
        "Login API call failed: " + loginError.message,
        logger.logLevels.ERROR
      );
      returnToBTT("\u26A0\uFE0F " + loginError.message);
      return;
    }

    const glucoseHeaders = {
      "Content-Length": "0",
      Accept: "application/json",
      "User-Agent": "Dexcom Share/3.0.2.11 CFNetwork/672.0.2 Darwin/14.0.0",
    };

    const glucoseRequestOptions = {
      method: "GET",
      headers: glucoseHeaders,
      body: "",
      redirect: "follow",
    };

    const glucoseParams = {
      sessionId: sessionId,
      minutes: 1440,
      maxCount: 2,
    };

    const glucoseQueryString = Object.entries(glucoseParams)
      .map(
        ([key, value]) =>
          `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
      )
      .join("&");

    let glucoseData = [];
    try {
      logger.log("Fetching glucose data...");
      const glucoseResponse = await fetch(
        `${BASE_URL}/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues?${glucoseQueryString}`,
        glucoseRequestOptions
      );
      glucoseData = await glucoseResponse.json();
      logger.log("glucoseData:");
      logger.log(glucoseData);

      logger.log("JSON.stringify(glucoseData):");
      logger.log(`\n${JSON.stringify(glucoseData, null, 2)}`);
    } catch (glucoseError) {
      logger.log(
        "Glucose API call failed: " + glucoseError.message,
        logger.logLevels.ERROR
      );
      returnToBTT("\u26A0\uFE0F " + glucoseError.message);
      return;
    }
    if (glucoseData.length === 0) {
      logger.log("No glucose data available.", logger.logLevels.WARNING);
      returnToBTT("\u26A0\uFE0F No data");
      return;
    }

    const currentGlucoseValue = glucoseData[0]?.Value ?? "N/A";
    const previousGlucoseValue = glucoseData[1]?.Value ?? "N/A";

    let glucoseDelta = "N/A";
    if (currentGlucoseValue !== "N/A" && previousGlucoseValue !== "N/A") {
      glucoseDelta = currentGlucoseValue - previousGlucoseValue;
    }

    const deltaString =
      glucoseDelta !== "N/A" && glucoseDelta >= 0
        ? `+${glucoseDelta}`
        : `${glucoseDelta}`;

    const currentTrend = glucoseData[0]?.Trend ?? "None";

    const trendSymbols = {
      None: "",
      DoubleUp: "\u21c8", // ⇈
      SingleUp: "\u2191", // ↑
      FortyFiveUp: "\u2197", // ↗
      Flat: "\u2192", // →
      FortyFiveDown: "\u2198", // ↘
      SingleDown: "\u2193", // ↓
      DoubleDown: "\u21ca", // ⇊
      NotComputable: "",
      RateOutOfRange: "",
    };

    const trendSymbol = trendSymbols[currentTrend];

    const WT = glucoseData[0]?.WT ?? NaN;
    // WT is a string of the form "Date(1736105141000)". We need to extract the timestamp using a regex.
    const timestampString = WT.match(/Date\((\d+)\)/)[1];

    // Get the difference between the current time and the WT timestamp
    const now = new Date();
    const timestamp = new Date(parseInt(timestampString));
    const difference = now - timestamp;
    const minutes = Math.floor(difference / 60000);

    let timeAgoText;
    if (minutes >= 60) {
      const hours = Math.floor(minutes / 60);
      const remainingMinutes = minutes % 60;
      timeAgoText = `${hours}h ${remainingMinutes}m`;
    } else {
      timeAgoText = `${minutes}m`;
    }

    const bttMenuItem = {
      text: `${currentGlucoseValue} ${trendSymbol} ${deltaString} ${timeAgoText}`,
    };

    logger.log("Returning data to BTT:");
    logger.log(`\n${JSON.stringify(bttMenuItem, null, 2)}`);

    returnToBTT(JSON.stringify(bttMenuItem));
  } catch (error) {
    logger.log("Unexpected error: " + error.message, logger.logLevels.ERROR);
    returnToBTT("\u26A0\uFE0F " + error.message);
  }
})();

let accountId = await get_keychain_item("SomeNameForYourID");
let password = await get_keychain_item("SomeNameForServiceXY")
 const loginPayload = JSON.stringify({
      accountId: accountId,
      password: password,
      applicationId: "d89443d2-327c-4a6f-89e5-496bbb0317db",
    });

However before being able to access these you'd first need to set them, you can either do that in a separate script which you delete later, or by manually adding an entry like this to keychain manually:

1 Like

Awesome – that works! Thanks Andreas :slight_smile:

This is so much better than saving credentials in plaintext in my BTT scripts!

The only downside is now I have passwords/credentials saved in 1Password, Passwords (Apple native app), and Keychain Access :sweat_smile:

Any idea if it would be possible to fetch credentials from the Passwords app? Or better yet, from 1Password? I've been experimenting using 1Passwords's CLI in combination with BTT's runShellScript. e.g.

let password = await runShellScript({script: "/opt/homebrew/bin/op read 'op://Private/Dexcom US/password'"});

But with this I'm constantly getting prompted to enter my master password :confused:

unfortunately I don't know what is causing the master password prompts, do they not happen when called from terminal?

//for Apple passwords, I could extend the keychain api a bit to allow specifying the name of the item, then it should be possible to access passwords too - afaik passwords is mostly just a new UI for keychain

1 Like

Maybe I'm doing something wrong with the 1Password CLI. I've been reading their docs and it's been tricky... I'll keep reading.

This would be fantastic!

For 1password possibly try:

let password = await runShellScript({launchPath : "/usr/bin/env",parameters: "op;;read;;'op://Private/Dexcom US/password'"});

Maybe it is depending on the environment

1 Like

I think this is my issue, the session duration lasts for 30 minutes.

1 Like