Secure Communication with User Keys

Learn how to encrypt data securely between two users, protecting their privacy in BSV applications.

Read as Markdown

This guide is for developers wanting to secure communications between two BRC-100–compatible wallets (e.g., “Alice” and “Bob”) using the encrypt and decrypt methods defined by BRC-2. These methods are exposed through the WalletClient interface in the BSV TypeScript SDK, letting your application handle data encryption and decryption with minimal overhead. We’ll also cover how to discover the correct public key (and thus confirm you’re truly encrypting for the right person) via the IdentityClient.

Prerequisites

  1. A BSV Wallet: Ensure the user is connected to a wallet implementing BRC-100—a standardized, vendor-neutral application-to-wallet interface within the BSV Blockchain ecosystem. If not, direct them over to the Getting a Wallet quickstart guide!
  2. WalletClient: Your application code will interface with the wallet using this client to call encrypt and decrypt. Install @bsv/sdk and import it.
  3. Two User Setup: In our example, Alice and Bob each have a BSV wallet/identity, which includes a public identity key for each.

Understanding Encryption

When encrypting data (e.g., a message or file) using the wallet:

  • Your app will specify:
    • The plaintext (as a byte array).
    • The protocolID and keyID (denoting the "context" in which the encryption is happening).
    • A counterparty (i.e., the recipient’s public key, or 'self' if you’re just encrypting for yourself).
  • Under the hood, the wallet calculates a shared secret between the user’s private key and the recipient’s public key.
  • The data is encrypted using AES-256-GCM, with the resulting ciphertext returned to your application. Then you can send the encrypted version to them.

On the receiving side, the decrypt process reverses it:

  • The wallet and your app supply the ciphertext plus the correct protocolID, keyID, and the recognized counterparty (which must be the sender’s public identity key).
  • The wallet computes the same shared secret, then decrypts.

Walkthrough: Alice Encrypting a Message for Bob

Step 1: Acquire Bob’s Public Key

Before Alice can encrypt anything for Bob, she needs Bob’s identity key:

  1. Manual: If Bob shares it directly, e.g., a hex-encoded public key string.
  2. IdentityClient: Use an identity resolution call to fetch Bob’s public key by a known handle or identity. This ensures we have confidence that 'bob@example.org' truly maps to the public key we obtain. In practice (in a React app) you could use the IdentitySearchField from @bsv/identity-react to let Alice find Bob.

Step 2: Call walletClient.encrypt with Bob’s Public Key

Alice’s application then does something like:

1import { WalletClient } from '@bsv/sdk'
2const walletClient = new WalletClient('auto'); // Connect to the BRC-100 wallet running on Alice's machine
3
4const plaintextMsg = new TextEncoder().encode("Hi Bob, let's talk privately!");
5const protocolID   = [2, 'private chat'];  // securityLevel=2, protocol name='private-chat'
6const keyID        = 'session1';          // an arbitrary key ID
7
8try {
9  const { ciphertext } = await walletClient.encrypt({
10    plaintext: [...plaintextMsg], // as an array of bytes
11    protocolID,
12    keyID,
13    counterparty: bobsPubKeyHex   // Bob’s public key string
14  });
15  // Now you have ciphertext to send/store
16} catch (err) {
17  console.error("Encryption failed:", err);
18}
19
20// Bob will need to know Alice's public identity key. Let's get it for him.
21const { publicKey } = await walletClient.getPublicKey({
22  identityKey: true
23})
Copied!
  • protocolID is [ securityLevel, "protocolName" ], fulfilling BRC-43 semantics. (Here, 2 indicates a “level-2” protocol that typically requires user permission for each new counterparty.)
  • keyID is any unique string (e.g. 'session1') to differentiate the keys used for operations under the same protocol.

Alice can now pass the resulting ciphertext to Bob via any channel (websocket, stored on-chain, etc.). In practice, consider using the Message Box Client for P2P comms!

Bob Decrypting Alice’s Message

Bob’s wallet likewise needs to handle the ciphertext:

1const walletClient = new WalletClient('auto');
2
3try {
4  // Suppose you’ve received `ciphertext` bytes from Alice.
5  const { plaintext } = await walletClient.decrypt({
6    ciphertext,
7    protocolID: [2, 'private chat'],
8    keyID: 'session1',
9    // Bob must name the 'counterparty' as Alice’s public key
10    counterparty: alicesPubKeyIdentityKeyHex
11  });
12
13  // Convert plaintext bytes back to string
14  const message = new TextDecoder().decode(new Uint8Array(plaintext));
15  console.log("Alice said: ", message);
16} catch (err) {
17  console.error("Decryption failed:", err);
18}
Copied!

Matching the same protocolID, keyID, and specifying Alice’s public identity key as counterparty ensures Bob’s wallet uses the correct underlying keys to do the decryption.

Identity Resolution / Public Key Discovery

As hinted above, you don’t want to just accept any string that calls itself “Bob’s public key.” BRC-100 standard wallets help facilitate using an IdentityClient or an analogous approach so you can do:

  1. Discover a user’s public identity key by some kind of attributes, like a certified name, email, or social media handle trusted by the user.
  2. Support the veracity of the returned key with certificates that link the user’s identity attributes to that key.

In many real apps, you might see something like:

1import { IdentityClient } from '@bsv/sdk'
2
3const fetchIdentities = async (query: string) => {
4  let results
5  const client = new IdentityClient()
6  // Figure out if the query is by IdentityKey
7  results = await client.resolveByAttributes({
8    attributes: {
9      any: query,
10    }
11  })
12  return results
13}
Copied!

You’ll get the same BRC-2 encryption benefits, but with better confidence that the public key genuinely belongs to Bob. This avoids the risk of man-in-the-middle attacks where a malicious party tries to trick you into encrypting data for an incorrect key.

Summary

  • Encryption:

    1. Acquire the desired recipient’s public key.
    2. Use walletClient.encrypt({plaintext, protocolID, keyID, counterparty}).
    3. Share the resulting ciphertext with the recipient.
  • Decryption:

    1. Retrieve the ciphertext.
    2. Call walletClient.decrypt({ciphertext, protocolID, keyID, counterparty}); the wallet verifies everything matches.
    3. Receive the resulting bytes as plaintext.
  • Identity:

    • For securely discovering a user’s public key, rely on the IdentityClient.

By weaving these steps together, you can create more private, user-friendly experiences in applications built on top of the BSV Blockchain. BRC-2 encryption helps keep messages secure, while standard methods for identity resolution (e.g., IdentityClient) ensure you’re using the right public key in the first place. Happy hacking!

Was this page helpful?