Skip to content

PGP Encrypt & Decrypt — TypeScript source

Encrypt or decrypt messages with PGP public/private keys. Powered by OpenPGP.js, runs entirely in your browser.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

import * as openpgp from 'openpgp';

export interface PGPKeyInfo {
  userID: string;
  fingerprint: string;
  algorithm: string;
  creationDate: string;
  expiry: string | null;
  isPrivate: boolean;
}

/** Format an expiration value (Date | Infinity) as an ISO date string, or null if never expires. */
function formatExpiry(expiration: Date | Infinity | null): string | null {
  if (!expiration || expiration === Infinity) return null;
  return expiration.toISOString().split('T')[0];
}

/**
 * Read a PGP key (public or private) and extract its metadata.
 * Throws on invalid or unrecognized key material.
 */
export async function readKeyInfo(armoredKey: string): Promise<PGPKeyInfo> {
  if (!armoredKey || typeof armoredKey !== 'string') {
    throw new Error('Key input is empty or invalid.');
  }

  const trimmed = armoredKey.trim();

  // Detect key type from armor header
  const isPrivateKey =
    trimmed.includes('-----BEGIN PGP PRIVATE KEY BLOCK-----') ||
    trimmed.includes('-----BEGIN PGP SECRET KEY BLOCK-----');

  try {
    const key = await openpgp.readKey({ armoredKey: trimmed });

    const users = key.getUserIDs();
    const userID = users[0] || 'unknown';

    const expiration = await key.getExpirationTime();

    return {
      userID,
      fingerprint: key.getFingerprint().toUpperCase(),
      algorithm: key.getAlgorithmInfo().algorithm,
      creationDate: key.getCreationTime().toISOString().split('T')[0],
      expiry: formatExpiry(expiration),
      isPrivate: isPrivateKey,
    };
  } catch (e) {
    throw new Error(
      `Invalid PGP key: ${e instanceof Error ? e.message : 'unrecognized key format'}`
    );
  }
}

/**
 * Encrypt a plaintext message for a recipient's public key.
 * Returns an ASCII-armored PGP message.
 *
 * Optionally signs with the sender's private key (signingPrivateKeyArmored + passphrase).
 */
export async function pgpEncrypt(
  message: string,
  publicKeyArmored: string,
  signingPrivateKeyArmored?: string,
  passphrase?: string
): Promise<string> {
  if (!message) throw new Error('Message must not be empty.');
  if (!publicKeyArmored) throw new Error('Recipient public key must not be empty.');

  const publicKey = await openpgp.readKey({
    armoredKey: publicKeyArmored.trim(),
  });

  let signingKeys: openpgp.PrivateKey | undefined;
  if (signingPrivateKeyArmored) {
    const raw = await openpgp.readPrivateKey({
      armoredKey: signingPrivateKeyArmored.trim(),
    });
    signingKeys = passphrase
      ? await openpgp.decryptKey({ privateKey: raw, passphrase })
      : raw;
  }

  const encrypted = await openpgp.encrypt({
    message: await openpgp.createMessage({ text: message }),
    encryptionKeys: publicKey,
    ...(signingKeys ? { signingKeys } : {}),
  });

  return encrypted;
}

/**
 * Decrypt an ASCII-armored PGP message with the recipient's private key.
 * Returns the plaintext message.
 */
export async function pgpDecrypt(
  armoredMessage: string,
  privateKeyArmored: string,
  passphrase?: string
): Promise<string> {
  if (!armoredMessage) throw new Error('Armored message must not be empty.');
  if (!privateKeyArmored) throw new Error('Private key must not be empty.');

  const privateKey = await openpgp.readPrivateKey({
    armoredKey: privateKeyArmored.trim(),
  });

  const decryptedKey = passphrase
    ? await openpgp.decryptKey({ privateKey, passphrase })
    : privateKey;

  const message = await openpgp.readMessage({
    armoredMessage: armoredMessage.trim(),
  });

  const { data } = await openpgp.decrypt({
    message,
    decryptionKeys: decryptedKey,
  });

  return data;
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →