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-jsonprocess 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
- Chrome Extension: Chrome Web Store
- Firefox Extension: Firefox Add-ons
Installation
This guide covers installing the LocalPGP browser extension and the required system dependencies.
Requirements
- GnuPG with
gpgme-jsonsupport - 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:
- Visit the LocalPGP Chrome Extension page
- Click “Add to Chrome”
- Click “Add extension” in the confirmation dialog
Firefox
Install from Firefox Add-ons:
- Visit the LocalPGP Firefox Add-on page
- Click “Add to Firefox”
- 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:
- Click on the LocalPGP extension icon in your browser toolbar
- The popup should show “Connected” status with a green indicator
- If you have GPG keys, they should be listed in the extension popup
If you see “Disconnected” or an error:
- Verify
gpgme-jsonis 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
- Navigate to the website you want to allow (e.g.,
https://example.com) - Click on the LocalPGP extension icon in your browser toolbar
- The popup will show the current site’s origin under “Current Site”
- If the site is not allowed, you’ll see a status like “Not allowed”
- Click the “Allow” button to grant the website access
- The status will change to “Allowed” with a green indicator
Using the Extension Options Page
- Right-click on the LocalPGP extension icon
- Select “Options” (Chrome) or “Manage Extension” → “Preferences” (Firefox)
- 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:
- Make sure you have the LocalPGP extension installed (see Installation)
- Allow the website
https://localpgp.orgin 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:
- Click All to list all keys in your keyring
- Use the search box to find specific keys by email, name, or fingerprint
- 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:
- Enter the recipient’s fingerprint or email in the “Recipient Key” field
- Type your secret message in the “Message to Encrypt” textarea
- Optionally check “Sign while encrypting” to also sign the message
- 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:
- Paste a PGP encrypted message in the textarea
- Click 🔓 Decrypt
- 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:
- Optionally enter a specific signing key fingerprint (leave empty to use your default key)
- Type the message you want to sign
- Choose the signature type:
- Clearsign - The message and signature are combined (readable message)
- Detached - Only the signature is output (message stays separate)
- Click ✍️ Sign
The signed message or signature will appear in the output area.
Step 6: Verify a Signature
In the Verify section:
- Choose the verification mode:
- Clearsigned - For messages that include the signature
- Detached Signature - For separate message and signature
- Paste the clearsigned message OR the original message and signature
- 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)