Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

LocalPGP provides browser extensions for Chrome and Firefox that allow you to perform OpenPGP operations (encrypt, decrypt, sign, verify) using your system’s GnuPG installation. This means you can use your existing GPG keyring, including hardware security keys like Yubikey, directly from your browser.

Features

  • Encrypt - Encrypt messages for one or more recipients
  • Decrypt - Decrypt messages using your private keys
  • Sign - Create clearsign or detached signatures
  • Verify - Verify signatures on messages
  • Key Management - List, import, and manage keys via your GPG keyring
  • Hardware Key Support - Full support for Yubikey and other OpenPGP cards

Architecture

LocalPGP uses Native Messaging to communicate with your local gpgme-json binary, which is part of the GnuPG project. This architecture ensures that:

  • All cryptographic operations are performed by your local GnuPG installation
  • Private keys never leave your system
  • Hardware key operations are handled by gpg-agent (pinentry will prompt for PIN)
  • The extension only communicates with the local gpgme-json process via Native Messaging

Libraries

SlayOps - For Website Integration

The SlayOps library is the main user-level API for integrating OpenPGP operations into your websites. It communicates with the LocalPGP browser extension to perform cryptographic operations using your local GnuPG installation.

Install via npm:

npm install slayops
# or
pnpm add slayops

Or include directly in your HTML:

<script src="https://unpkg.com/slayops/dist/slayops.js"></script>

See the Usage chapter for detailed examples.

Browser Extensions

Installation

This guide covers installing the LocalPGP browser extension and the required system dependencies.

Requirements

  • GnuPG with gpgme-json support
  • Chrome or Firefox browser

Step 1: Install gpgme-json

The gpgme-json binary is required for the extension to communicate with GnuPG.

Debian/Ubuntu:

sudo apt install gpgme-json

Fedora:

sudo dnf install gpgme

Arch Linux:

sudo pacman -S gpgme

Verify the installation:

which gpgme-json
# Should output: /usr/bin/gpgme-json

Step 2: Install the Browser Extension

Chrome

Install from the Chrome Web Store:

  1. Visit the LocalPGP Chrome Extension page
  2. Click “Add to Chrome”
  3. Click “Add extension” in the confirmation dialog

Firefox

Install from Firefox Add-ons:

  1. Visit the LocalPGP Firefox Add-on page
  2. Click “Add to Firefox”
  3. Click “Add” in the confirmation dialog

Step 3: Configure Native Messaging Host

Native messaging allows the browser extension to communicate with gpgme-json on your system. You need to create a configuration file to enable this.

Chrome/Chromium

Create the configuration file at the appropriate location:

  • Chromium: ~/.config/chromium/NativeMessagingHosts/gpgmejson.json
  • Chrome: ~/.config/google-chrome/NativeMessagingHosts/gpgmejson.json
# For Chrome
mkdir -p ~/.config/google-chrome/NativeMessagingHosts/
cat > ~/.config/google-chrome/NativeMessagingHosts/gpgmejson.json << 'EOF'
{
  "name": "gpgmejson",
  "description": "Integration with GnuPG",
  "path": "/usr/bin/gpgme-json",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://ckgehekhpgcaaikpadklkkjgdgoebdnh/"
  ]
}
EOF
# For Chromium
mkdir -p ~/.config/chromium/NativeMessagingHosts/
cat > ~/.config/chromium/NativeMessagingHosts/gpgmejson.json << 'EOF'
{
  "name": "gpgmejson",
  "description": "Integration with GnuPG",
  "path": "/usr/bin/gpgme-json",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://ckgehekhpgcaaikpadklkkjgdgoebdnh/"
  ]
}
EOF

Firefox

Create the configuration file at ~/.mozilla/native-messaging-hosts/gpgmejson.json:

mkdir -p ~/.mozilla/native-messaging-hosts/
cat > ~/.mozilla/native-messaging-hosts/gpgmejson.json << 'EOF'
{
  "name": "gpgmejson",
  "description": "Integration with GnuPG",
  "path": "/usr/bin/gpgme-json",
  "type": "stdio",
  "allowed_extensions": ["localpgp@localpgp.org"]
}
EOF

Step 4: Verify Installation

After installing the extension and configuring native messaging:

  1. Click on the LocalPGP extension icon in your browser toolbar
  2. The popup should show “Connected” status with a green indicator
  3. If you have GPG keys, they should be listed in the extension popup

If you see “Disconnected” or an error:

  • Verify gpgme-json is installed: which gpgme-json
  • Check the native messaging configuration file is in the correct location
  • Ensure the file contains valid JSON with no syntax errors
  • Restart your browser after making changes

Allowing Websites

Before a website can use LocalPGP for OpenPGP operations, you must explicitly allow it. This is a security feature to prevent unauthorized websites from accessing your keys.

Using the Extension Popup

  1. Navigate to the website you want to allow (e.g., https://example.com)
  2. Click on the LocalPGP extension icon in your browser toolbar
  3. The popup will show the current site’s origin under “Current Site”
  4. If the site is not allowed, you’ll see a status like “Not allowed”
  5. Click the “Allow” button to grant the website access
  6. The status will change to “Allowed” with a green indicator

Using the Extension Options Page

  1. Right-click on the LocalPGP extension icon
  2. Select “Options” (Chrome) or “Manage Extension” → “Preferences” (Firefox)
  3. In the “Allowed Origins” section, you can:
    • See all currently allowed websites
    • Add new origins by entering the URL (e.g., https://example.com) and clicking “Add Origin”
    • Remove origins by clicking the “Remove” button next to them

Security Notes

  • Only allow websites you trust
  • The extension will only respond to requests from allowed origins
  • You can revoke access at any time by removing the origin from the allowed list
  • The extension never sends your private keys to websites - all cryptographic operations happen locally

Usage

This chapter covers how to use the SlayOps JavaScript library to integrate OpenPGP operations into your website.

Live Demo

You can try out all the features described in this chapter using the live demo at https://localpgp.org/demo.html.

Before using the demo:

  1. Make sure you have the LocalPGP extension installed (see Installation)
  2. Allow the website https://localpgp.org in the extension (click the extension icon and click “Allow”)

Using the Demo Page

The demo page at https://localpgp.org/demo.html provides a complete interface to test all OpenPGP operations. Here’s how to use each section:

Step 1: Connect to the Extension

When you first load the demo page, you’ll see a status bar at the top showing the connection status:

  • “Chrome extension not responding” - The website is not yet allowed. Click the LocalPGP extension icon in your browser toolbar and click “Allow” to grant access.
  • “Connected” (with green indicator) - You’re connected and ready to use OpenPGP operations.

Click the Connect button to establish a connection with the extension.

Step 2: Key Management

The Key Management section lets you browse your GPG keyring:

  1. Click All to list all keys in your keyring
  2. Use the search box to find specific keys by email, name, or fingerprint
  3. Click Search to filter keys

The available keys will be displayed with their user IDs and fingerprints. You can click on a key to use its fingerprint in other operations.

Step 3: Encrypt a Message

In the Encrypt section:

  1. Enter the recipient’s fingerprint or email in the “Recipient Key” field
  2. Type your secret message in the “Message to Encrypt” textarea
  3. Optionally check “Sign while encrypting” to also sign the message
  4. Click 🔒 Encrypt

The encrypted PGP message will appear in the “Encrypted Output” area. You can copy this to send securely.

Step 4: Decrypt a Message

In the Decrypt section:

  1. Paste a PGP encrypted message in the textarea
  2. Click 🔓 Decrypt
  3. If you have the corresponding private key, the decrypted message will appear

You can also click ← Paste from Encrypt to quickly copy the output from the Encrypt section.

Note: If the message was encrypted to a key on a hardware device (like Yubikey), you’ll be prompted to enter your PIN.

Step 5: Sign a Message

In the Sign section:

  1. Optionally enter a specific signing key fingerprint (leave empty to use your default key)
  2. Type the message you want to sign
  3. Choose the signature type:
    • Clearsign - The message and signature are combined (readable message)
    • Detached - Only the signature is output (message stays separate)
  4. Click ✍️ Sign

The signed message or signature will appear in the output area.

Step 6: Verify a Signature

In the Verify section:

  1. Choose the verification mode:
    • Clearsigned - For messages that include the signature
    • Detached Signature - For separate message and signature
  2. Paste the clearsigned message OR the original message and signature
  3. Click ✓ Verify

The verification result will show whether the signature is valid and who signed it.

Additional Features

  • Get Default Key - Shows your default GPG signing key
  • Browser Info - Displays detected browser type and connection details

Quick Start

import { SlayOps } from 'slayops';

// Create instance
const pgp = new SlayOps();

// Connect to the extension
await pgp.connect();

// Now you can use OpenPGP operations
const keys = await pgp.getKeys();
console.log('Available keys:', keys);

Or with a CDN:

<script src="https://unpkg.com/slayops/dist/slayops.js"></script>
<script>
  const pgp = new SlayOps.SlayOps();
  
  async function init() {
    await pgp.connect();
    const keys = await pgp.getKeys();
    console.log('Available keys:', keys);
  }
  
  init();
</script>

Configuration

The SlayOps constructor accepts configuration options:

const pgp = new SlayOps({
  timeout: 30000,        // Request timeout in ms (default: 30000)
  autoConnect: false,    // Auto-connect on creation (default: false)
});

For Development

If you’re developing with a local unpacked extension, you can specify the extension ID:

// Option 1: Single dev ID
const pgp = new SlayOps({ chromeExtensionId: 'your-local-dev-id' });

// Option 2: Try multiple IDs (production first, then dev)
const pgp = new SlayOps({ 
  chromeExtensionIds: [
    'ckgehekhpgcaaikpadklkkjgdgoebdnh',  // Production
    'your-local-dev-id'                    // Dev fallback
  ]
});

Connection Management

Connecting to the Extension

const pgp = new SlayOps();

// Connect to extension
await pgp.connect();

// Check if connected
if (pgp.isConnected()) {
  console.log('Connected to LocalPGP');
}

// Get connection status
const status = pgp.getStatus();
// Returns: 'disconnected' | 'connecting' | 'connected' | 'error'

// Check if extension is available
if (pgp.isExtensionAvailable()) {
  console.log('Extension is installed');
}

// Get human-readable status message
console.log(pgp.getStatusMessage());

Events

SlayOps emits events you can listen to:

pgp.on('connected', () => {
  console.log('Connected to extension!');
});

pgp.on('disconnected', () => {
  console.log('Disconnected from extension');
});

pgp.on('error', (err) => {
  console.error('Error:', err);
});

pgp.on('ready', () => {
  console.log('Extension is ready');
});

// Remove a listener
pgp.off('connected', handler);

Key Management

Listing Keys

// Get all keys
const allKeys = await pgp.getKeys();

// Search for keys by email, name, or fingerprint
const searchResults = await pgp.getKeys('alice@example.com');

// Get only secret keys (keys you can sign/decrypt with)
const secretKeys = await pgp.getKeys('', true);

Getting the Default Key

const defaultKey = await pgp.getDefaultKey();
if (defaultKey) {
  console.log('Default key:', defaultKey.fingerprint);
  console.log('User IDs:', defaultKey.userids);
}

Importing Keys

const armoredPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;

await pgp.importKey(armoredPublicKey);
console.log('Key imported successfully');

Encryption

Basic Encryption

// Encrypt a message for a recipient
const encrypted = await pgp.encrypt(
  'Hello, this is a secret message!',
  ['recipient@example.com']  // Can be email, name, or fingerprint
);

console.log(encrypted);
// Output: -----BEGIN PGP MESSAGE-----...

Encrypt for Multiple Recipients

const encrypted = await pgp.encrypt(
  'Secret message for the team',
  [
    'alice@example.com',
    'bob@example.com',
    '5286C32E7C71E14C4C82F9AE0B207108925CB162'  // Fingerprint
  ]
);

Encrypt with Signing

const encrypted = await pgp.encrypt(
  'Signed and encrypted message',
  ['recipient@example.com'],
  {
    sign: true,                    // Sign while encrypting
    signingKey: 'FINGERPRINT',     // Optional: specific signing key
    alwaysTrust: true              // Trust recipients without verification
  }
);

Decryption

Basic Decryption

const encryptedMessage = `-----BEGIN PGP MESSAGE-----
...
-----END PGP MESSAGE-----`;

const result = await pgp.decrypt(encryptedMessage);

console.log('Decrypted:', result.data);

// If the message was signed
if (result.signatures && result.signatures.length > 0) {
  console.log('Signed by:', result.signatures[0].fingerprint);
  console.log('Signature valid:', result.signatures[0].valid);
}

Signing

Clearsign (Message + Signature Together)

// Default: clearsign mode
const signed = await pgp.sign('This is my message');

console.log(signed);
// Output:
// -----BEGIN PGP SIGNED MESSAGE-----
// Hash: SHA256
//
// This is my message
// -----BEGIN PGP SIGNATURE-----
// ...
// -----END PGP SIGNATURE-----

Detached Signature

const signature = await pgp.sign('This is my message', {
  mode: 'detached'
});

console.log(signature);
// Output: -----BEGIN PGP SIGNATURE-----...

Sign with Specific Key

const signed = await pgp.sign('My message', {
  signingKey: '5286C32E7C71E14C4C82F9AE0B207108925CB162'
});

Verification

Verify Clearsigned Message

const clearsignedMessage = `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

This is my message
-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----`;

const result = await pgp.verify(clearsignedMessage);

console.log('Valid:', result.isValid);
console.log('Signatures:', result.signatures);

// Each signature contains:
// - fingerprint: Signer's key fingerprint
// - valid: Whether the signature is valid
// - status: Detailed status information

Verify Detached Signature

const originalMessage = 'This is my message';
const detachedSignature = `-----BEGIN PGP SIGNATURE-----
...
-----END PGP SIGNATURE-----`;

const result = await pgp.verify(originalMessage, detachedSignature);

console.log('Valid:', result.isValid);

Complete Example

Here’s a complete example showing a typical workflow:

<!DOCTYPE html>
<html>
<head>
  <title>SlayOps Demo</title>
  <script src="https://unpkg.com/slayops/dist/slayops.js"></script>
</head>
<body>
  <h1>OpenPGP Demo</h1>
  
  <div id="status">Connecting...</div>
  <div id="keys"></div>
  
  <h2>Encrypt</h2>
  <textarea id="message" placeholder="Enter message"></textarea>
  <input id="recipient" placeholder="Recipient email or fingerprint">
  <button onclick="encryptMessage()">Encrypt</button>
  <pre id="encrypted-output"></pre>
  
  <h2>Sign</h2>
  <textarea id="sign-message" placeholder="Enter message to sign"></textarea>
  <button onclick="signMessage()">Sign</button>
  <pre id="signed-output"></pre>

  <script>
    const pgp = new SlayOps.SlayOps();
    
    async function init() {
      try {
        await pgp.connect();
        document.getElementById('status').textContent = 'Connected!';
        
        // List keys
        const keys = await pgp.getKeys();
        document.getElementById('keys').innerHTML = 
          '<h3>Available Keys:</h3>' +
          keys.map(k => `<p>${k.userids[0]?.uid || 'No UID'}<br><code>${k.fingerprint}</code></p>`).join('');
      } catch (err) {
        document.getElementById('status').textContent = 'Error: ' + err.message;
      }
    }
    
    async function encryptMessage() {
      const message = document.getElementById('message').value;
      const recipient = document.getElementById('recipient').value;
      
      try {
        const encrypted = await pgp.encrypt(message, [recipient]);
        document.getElementById('encrypted-output').textContent = encrypted;
      } catch (err) {
        document.getElementById('encrypted-output').textContent = 'Error: ' + err.message;
      }
    }
    
    async function signMessage() {
      const message = document.getElementById('sign-message').value;
      
      try {
        const signed = await pgp.sign(message);
        document.getElementById('signed-output').textContent = signed;
      } catch (err) {
        document.getElementById('signed-output').textContent = 'Error: ' + err.message;
      }
    }
    
    init();
  </script>
</body>
</html>

Browser Detection

SlayOps automatically handles browser differences between Chrome and Firefox:

// Get detected browser type
const browserType = pgp.getBrowserType();
// Returns: 'chrome' | 'firefox' | 'firefox-injected' | 'chrome-no-api' | 'unknown'

// Manually trigger browser detection
pgp.detectBrowser();

Error Handling

Always wrap SlayOps calls in try-catch blocks:

try {
  await pgp.connect();
  const encrypted = await pgp.encrypt('message', ['recipient@example.com']);
} catch (err) {
  if (err.message.includes('not responding')) {
    console.error('Extension not installed or website not allowed');
  } else if (err.message.includes('No matching keys')) {
    console.error('Recipient key not found in keyring');
  } else {
    console.error('Operation failed:', err.message);
  }
}

Common errors:

  • Extension not responding: The extension is not installed, or the website is not in the allowed list
  • No matching keys: The recipient’s public key is not in the local GPG keyring
  • Decryption failed: You don’t have the private key for this message, or the message is corrupted
  • Timeout: The operation took too long (e.g., waiting for hardware key PIN entry)