API exempel dokumentation

Kompletta, fungerande exempel för de vanligaste API-uppgifterna. Alla exempel använder ren JavaScript och den inbyggda Web Crypto API:n, så inga externa bibliotek behövs. Se API-dokumentation för den fullständiga slutpunktsreferensen.

Skapa hemlighet

POST /api/secrets Privat API-nyckel

Ett exempel på att kryptera en hemlighet och skicka den till password.link-API:et med JavaScript. Spara det som en HTML-fil, ange API-nyckeln och öppna det i en webbläsare.

Ett skript som detta kan användas på en lokal dator eller i ett lokalt nätverk för att smidigt skapa hemligheter i vår tjänst, men lägg aldrig ut det på en publik plats utan ordentlig autentisering, eftersom det innehåller din privata API-nyckel.

HTML
<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
    <meta content="utf-8" http-equiv="encoding">

    <script>
      // Set the base URL for the generated link
      const LINK_BASE_URL = "https://some.site/secret.html";

      // Set the API key here - a private API key is required
      const PRIVATE_API_KEY = "private_key_abcd...";

      // ----------------------------------- //

      // A function for sending the secret to password.link API
      async function sendToPasswordlinkApi(secret) {
        // Create the public and private password parts
        const passwordPartPublic = generateString(18);
        const passwordPartPrivate = generateString(18);

        // Create a Web Crypto compatible Base64 encoded ciphertext
        const ciphertext = await encryptSecret(passwordPartPublic, passwordPartPrivate, secret);

        // Send a request to the password.link API and process the result
        const response = await fetch("https://password.link/api/secrets", {
          method: "POST",
          headers: {
            "Authorization": "ApiKey " + PRIVATE_API_KEY,
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            "ciphertext": ciphertext,
            "password_part_private": btoa(passwordPartPrivate)
          })
        });

        const data = await response.json();

        if (!response.ok) {
          throw new Error(data.error.message);
        }

        const secretUrl = LINK_BASE_URL + "?" + data.data.id + "#" + btoa(passwordPartPublic);
        const meta = data.metadata;

        document.getElementById("secret").textContent = "URL to secret: " + secretUrl;
        document.getElementById("secret-meta").textContent =
          "Total secrets: " + meta.secrets_total +
          " | Usage: " + meta.secrets_usage +
          " | Allowance: " + meta.secrets_allowance;
      }

      // A function for encrypting a secret with the Web Crypto API (AES-GCM),
      // returns a Base64 encoded ciphertext JSON string
      async function encryptSecret(passwordPartPublic, passwordPartPrivate, secret) {
        const iterations = 10000;
        const salt = crypto.getRandomValues(new Uint8Array(16));
        const iv = crypto.getRandomValues(new Uint8Array(12));

        // The encryption key is derived from the concatenated password parts
        const passwordKey = await crypto.subtle.importKey(
          "raw",
          new TextEncoder().encode(passwordPartPrivate + passwordPartPublic),
          "PBKDF2",
          false,
          ["deriveKey"]
        );

        const key = await crypto.subtle.deriveKey(
          { name: "PBKDF2", salt: salt, iterations: iterations, hash: "SHA-256" },
          passwordKey,
          { name: "AES-GCM", length: 256 },
          false,
          ["encrypt"]
        );

        const cipher = await crypto.subtle.encrypt(
          { name: "AES-GCM", iv: iv },
          key,
          new TextEncoder().encode(secret)
        );

        return btoa(JSON.stringify({
          "cipher": bufferToBase64(cipher),
          "iv": bufferToBase64(iv),
          "salt": bufferToBase64(salt),
          "iter": iterations
        }));
      }

      // Base64 encode the bytes of an ArrayBuffer
      function bufferToBase64(buffer) {
        const bytes = new Uint8Array(buffer);
        let binary = "";

        for (let i = 0; i < bytes.length; i++) {
          binary += String.fromCharCode(bytes[i]);
        }

        return btoa(binary);
      }

      // A function for generating a random string. Rejection sampling keeps
      // the character distribution unbiased.
      function generateString(length) {
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$|/!_+,.-?()[]{}<>&#^*=@";
        const maxAcceptable = Math.floor(256 / chars.length) * chars.length;

        let str = "";

        while (str.length < length) {
          const randomValues = new Uint8Array(length - str.length);
          crypto.getRandomValues(randomValues);

          for (let i = 0; i < randomValues.length && str.length < length; i++) {
            if (randomValues[i] < maxAcceptable) {
              str += chars.charAt(randomValues[i] % chars.length);
            }
          }
        }

        return str;
      }

      // Execute the send function when the button is clicked
      document.addEventListener("DOMContentLoaded", function() {
        document.getElementById("secret-button").addEventListener("click", function(e) {
          e.preventDefault();

          const secret = document.getElementById("secret-input").value;

          sendToPasswordlinkApi(secret).catch(function(error) {
            document.getElementById("secret").textContent = "Error: " + error.message;
          });
        });
      });
    </script>
    <style>
      body {
        font: 12px Arial;
      }
      .container {
        max-width: 960px;
        margin: 0 auto;
        text-align: center;
        margin-top: 60px;
      }
      #secret {
        margin-top: 30px;
        font-size: 18px;
      }
      #secret-meta {
        margin-top: 40px;
      }
      .secret-form {
        display: flex;
        flex-flow: row wrap;
        align-items: center;
        flex-direction: vertical;
        justify-content: center;
      }
      .secret-form input {
        vertical-align: middle;
        margin: 5px 10px 5px 0;
        padding: 10px;
        border: 1px solid #ddd;
        width: 200px;
      }
      .secret-form button {
        padding: 10px 20px;
        border: 1px solid #ddd;
      }
      .secret-form button:hover {
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h2>Encrypt and create a secret on password.link</h2>
      <form class="secret-form">
        <input id="secret-input" type="text" name="secret">
        <button id="secret-button">Encrypt and create link</button>
      </form>

      <!-- This div will contain the link to the secret (or error) -->
      <div id="secret"></div>
      <div id="secret-meta"></div>
    </div>
  </body>
</html>

Skapa hemlighet med bilaga

POST /api/secrets Privat API-nyckel

Det här exemplet visar de bilagespecifika delarna. Det förutsätter att du redan har skapat hemlighetens chiffertext och lösenordsdelarna enligt exemplet Skapa hemlighet ovan.

JavaScript
// Set these first
const PASSWORDLINK_BASE_URL = "https://password.link";
const PRIVATE_API_KEY = "private_key_abcd...";

// These values come from the Create Secret example above
const ciphertext = "...";
const passwordPartPrivate = "...";
const passwordPartPublic = "...";
const passwordPartPrivateBase64 = btoa(passwordPartPrivate);

async function createSecretWithAttachment(file) {
  const createResponse = await fetch(`${PASSWORDLINK_BASE_URL}/api/secrets`, {
    method: "POST",
    headers: {
      "Authorization": "ApiKey " + PRIVATE_API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      ciphertext: ciphertext,
      password_part_private: passwordPartPrivateBase64,
      attachment: {
        file_name: file.name,
        file_type: file.type || "application/octet-stream",
        file_size: file.size
      }
    })
  });

  const createData = await createResponse.json();

  if (!createResponse.ok) {
    throw new Error(createData.error?.message || "Failed to create secret");
  }

  const attachmentUpload = createData.data.attachment.upload;
  const fileDataUrl = await fileToDataUrl(file);
  const encryptedFile = await encryptAttachment(
    fileDataUrl,
    passwordPartPrivate + passwordPartPublic
  );

  const formData = new FormData();
  appendFormFields(formData, attachmentUpload.metadata);
  appendFormFields(formData, attachmentUpload.fields);
  formData.append("file", new Blob([encryptedFile], { type: "text/plain" }), "file.txt");

  const uploadUrl = new URL(attachmentUpload.url, PASSWORDLINK_BASE_URL);
  const uploadOptions = {
    method: "POST",
    body: formData
  };

  if (uploadUrl.origin === new URL(PASSWORDLINK_BASE_URL).origin) {
    uploadOptions.headers = {
      "Authorization": "ApiKey " + PRIVATE_API_KEY
    };
  } else {
    uploadOptions.mode = "no-cors";
  }

  const uploadResponse = await fetch(uploadUrl.toString(), uploadOptions);

  if (uploadResponse.status !== 0 && !uploadResponse.ok) {
    throw new Error("Failed to upload attachment");
  }

  return createData.data.id;
}

function appendFormFields(formData, fields) {
  if (!fields) {
    return;
  }

  Object.entries(fields).forEach(([key, value]) => {
    formData.append(key, value);
  });
}

function fileToDataUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

async function encryptAttachment(plaintext, password) {
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const passwordKey = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveKey"]
  );

  const key = await crypto.subtle.deriveKey(
    { name: "PBKDF2", salt: salt, iterations: 10000, hash: "SHA-256" },
    passwordKey,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt"]
  );

  const cipher = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv: iv },
    key,
    new TextEncoder().encode(plaintext)
  );

  return btoa(JSON.stringify({
    cipher: bufferToString(cipher),
    iv: bufferToString(iv),
    salt: bufferToString(salt)
  }));
}

function bufferToString(buffer) {
  let binary = "";
  const bytes = new Uint8Array(buffer);

  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }

  return binary;
}

Visa hemlighet

GET /api/secrets/<id> Publik API-nyckel

Ett exempel på att hämta, dekryptera och visa en hemlighet med JavaScript. Ett skript som detta kan användas för att skapa en självhostad sida för att visa hemligheter.

Chiffertexten kan vara Web Crypto eller äldre SJCL. Exemplet nedan hanterar båda: SJCL-nyttolaster har en ct-nyckel. För SJCL-dekryptering, inkludera sjcl.js på sidan.

HTML
<html>
  <head>
    <style>
      body {
        font: 12px Arial;
      }
      .container {
        max-width: 960px;
        margin: 0 auto;
        text-align: center;
        margin-top: 60px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h2>Here's the secret</h2>

      <!-- This div will contain the decrypted secret (or error) -->
      <div id="secret"></div>
    </div>

    <script>
      (function() {
        // Set the API key here - a public API key is required
        const PUBLIC_API_KEY = "public_key_abcd...";

        // ----------------------------------- //

        // Get the secret ID from the query string part of the URL
        // E.g. https://some.site/secret.html?secret_id
        const secretId = location.search.substr(1);

        // Get the public password (encryption key) part from the hash part of the URL, in Base64 format
        // E.g. https://some.site/secret.html?secret_id#password_part_public
        const passwordPartPublic = location.hash.substr(1);

        // Fetch the secret from the password.link API and decrypt it
        async function fetchAndDecryptSecret() {
          const response = await fetch("https://password.link/api/secrets/" + secretId, {
            headers: {
              "Authorization": "ApiKey " + PUBLIC_API_KEY
            }
          });

          const data = await response.json();

          if (!response.ok) {
            throw new Error(data.error.message);
          }

          const secret = data.data;
          const decryptedSecret = await decryptSecret(
            passwordPartPublic,
            secret.password_part_private,
            secret.ciphertext
          );

          // Set the content of the element with id "secret" to the decrypted secret
          // Use .textContent to avoid XSS
          document.getElementById("secret").textContent = decryptedSecret;
        }

        // Decrypts a ciphertext received from the password.link API.
        // All parameters are in Base64 format.
        //
        // Legacy SJCL payloads contain a "ct" key and need sjcl.js:
        // https://github.com/bitwiseshiftleft/sjcl
        async function decryptSecret(passwordPartPublic, passwordPartPrivate, ciphertext) {
          const payload = JSON.parse(atob(ciphertext));
          const password = atob(passwordPartPrivate) + atob(passwordPartPublic);

          if (Object.prototype.hasOwnProperty.call(payload, "ct")) {
            return sjcl.decrypt(password, atob(ciphertext));
          }

          const passwordKey = await crypto.subtle.importKey(
            "raw",
            new TextEncoder().encode(password),
            "PBKDF2",
            false,
            ["deriveKey"]
          );

          const key = await crypto.subtle.deriveKey(
            {
              name: "PBKDF2",
              salt: base64ToBytes(payload.salt),
              iterations: payload.iter || 10000,
              hash: "SHA-256"
            },
            passwordKey,
            { name: "AES-GCM", length: 256 },
            false,
            ["decrypt"]
          );

          const plaintext = await crypto.subtle.decrypt(
            { name: "AES-GCM", iv: base64ToBytes(payload.iv) },
            key,
            base64ToBytes(payload.cipher)
          );

          return new TextDecoder().decode(plaintext);
        }

        // Decode a Base64 string into a byte array
        function base64ToBytes(base64) {
          const binary = atob(base64);
          const bytes = new Uint8Array(binary.length);

          for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
          }

          return bytes;
        }

        fetchAndDecryptSecret().catch(function(error) {
          document.getElementById("secret").textContent = "Error: " + error.message;
        });
      })();
    </script>
  </body>
</html>