Documentation
¶
Overview ¶
Package crypto provides optional E2E encryption for mailbox messages.
The EncryptionPlugin implements mailbox.SendHook and encrypts the message body during send. Decryption is a client-side operation performed after retrieving a message via Open or Decrypt.
For compression, use the separate github.com/rbaliyan/mailbox/compress package. Register compression before encryption for compress-then-encrypt:
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(store),
mailbox.WithPlugins(
compress.NewPlugin(compress.Gzip),
crypto.NewEncryptionPlugin(keys, crypto.WithKeyType(crypto.X25519)),
),
)
Encryption ¶
Each message is encrypted with a random AES-256-GCM data encryption key (DEK). The DEK is then wrapped (encrypted) with each recipient's public key so that only the intended recipients can decrypt the message. The sender's DEK is also included so they can read their own sent messages.
Wrapped DEKs are stored in message metadata, not headers. The subject is NOT encrypted to preserve searchability.
Compression ¶
Message bodies are compressed before encryption using gzip or zstd. The Content-Encoding header indicates the algorithm used.
Decryption ¶
After retrieving a message, call Open to decrypt and decompress:
msg, _ := mb.Get(ctx, msgID) plaintext, err := crypto.Open(ctx, msg, "bob", privateKeyProvider)
Index ¶
- Constants
- Variables
- func Decrypt(ctx context.Context, msg MessageReader, userID string, keys PrivateKeyProvider) ([]byte, error)
- func IsEncrypted(msg MessageReader) bool
- func Open(ctx context.Context, msg MessageReader, userID string, keys PrivateKeyProvider) ([]byte, error)
- type EncryptionOption
- type EncryptionPlugin
- func (p *EncryptionPlugin) AfterSend(_ context.Context, _ string, _ store.Message) error
- func (p *EncryptionPlugin) BeforeSend(ctx context.Context, userID string, draft store.DraftMessage) error
- func (p *EncryptionPlugin) Close(_ context.Context) error
- func (p *EncryptionPlugin) Init(_ context.Context) error
- func (p *EncryptionPlugin) Name() string
- type KeyResolver
- type KeyType
- type MessageReader
- type PrivateKeyProvider
- type StaticKeyResolver
- func (r *StaticKeyResolver) AddUser(userID string, publicKey, privateKey []byte)
- func (r *StaticKeyResolver) PrivateKey(_ context.Context, userID string) ([]byte, error)
- func (r *StaticKeyResolver) PublicKey(_ context.Context, userID string) ([]byte, error)
- func (r *StaticKeyResolver) PublicKeys(_ context.Context, userIDs []string) (map[string][]byte, error)
Examples ¶
Constants ¶
const ( // HeaderEncryption is set on encrypted messages to indicate the body cipher. HeaderEncryption = "X-Encryption" // MetaEncryptedDEKs is the metadata key storing per-recipient wrapped DEKs. // Value is map[string]string where key is userID and value is base64-encoded wrapped DEK. MetaEncryptedDEKs = "x-encrypted-deks" // MetaEncryptionKeyType is the metadata key indicating the asymmetric algorithm // used for DEK wrapping (e.g., "x25519", "rsa-oaep"). MetaEncryptionKeyType = "x-encryption-key-type" )
Header and metadata keys used by the encryption and compression plugins.
const (
AlgoAES256GCM = "aes-256-gcm"
)
Algorithm identifiers.
Variables ¶
var ( // ErrNotEncrypted is returned when attempting to decrypt a message that is not encrypted. ErrNotEncrypted = errors.New("crypto: message is not encrypted") // ErrDEKNotFound is returned when the user's wrapped DEK is not in the message metadata. ErrDEKNotFound = errors.New("crypto: no DEK found for user") // ErrDecryptionFailed is returned when decryption fails (wrong key, tampered data). ErrDecryptionFailed = errors.New("crypto: decryption failed") // ErrKeyNotFound is returned when a user's public or private key cannot be resolved. ErrKeyNotFound = errors.New("crypto: key not found") // ErrUnsupportedKeyType is returned for unknown key types. ErrUnsupportedKeyType = errors.New("crypto: unsupported key type") )
Sentinel errors.
Functions ¶
func Decrypt ¶
func Decrypt(ctx context.Context, msg MessageReader, userID string, keys PrivateKeyProvider) ([]byte, error)
Decrypt decrypts an encrypted message body for the given user. Returns ErrNotEncrypted if the message is not encrypted. Returns ErrDEKNotFound if the user has no wrapped DEK in the message.
func IsEncrypted ¶
func IsEncrypted(msg MessageReader) bool
IsEncrypted returns true if the message has the X-Encryption header set.
Example ¶
package main
import (
"context"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
svc, _ := mailbox.New(mailbox.Config{}, mailbox.WithStore(memory.New()))
svc.Connect(ctx)
defer svc.Close(ctx)
svc.Client("alice").SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Plain",
Body: "not encrypted",
})
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
fmt.Println("encrypted:", crypto.IsEncrypted(inbox.All()[0]))
}
Output: encrypted: false
func Open ¶
func Open(ctx context.Context, msg MessageReader, userID string, keys PrivateKeyProvider) ([]byte, error)
Open decrypts and decompresses a message body. This is the recommended single-call function for reading encrypted messages. It handles: base64 decode -> decrypt -> decompress, based on message headers.
If the message is not encrypted, returns the raw body bytes. If the message is encrypted but not compressed, just decrypts.
Example ¶
package main
import (
"context"
"crypto/rand"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/compress"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
"golang.org/x/crypto/curve25519"
)
func main() {
ctx := context.Background()
// Generate keypairs.
alicePriv := make([]byte, 32)
rand.Read(alicePriv)
alicePub, _ := curve25519.X25519(alicePriv, curve25519.Basepoint)
bobPriv := make([]byte, 32)
rand.Read(bobPriv)
bobPub, _ := curve25519.X25519(bobPriv, curve25519.Basepoint)
keys := crypto.NewStaticKeyResolver()
keys.AddUser("alice", alicePub, alicePriv)
keys.AddUser("bob", bobPub, bobPriv)
// Create service with compress-then-encrypt plugins.
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(
compress.NewPlugin(compress.Gzip),
crypto.NewEncryptionPlugin(keys),
),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Alice sends an encrypted message.
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Secret",
Body: "Hello Bob!",
})
// Bob reads and decrypts.
bob := svc.Client("bob")
inbox, _ := bob.Folder(ctx, store.FolderInbox, store.ListOptions{})
plaintext, _ := crypto.Open(ctx, inbox.All()[0], "bob", keys)
fmt.Println(string(plaintext))
}
Output: Hello Bob!
Example (CompressThenEncrypt) ¶
ExampleOpen_compressThenEncrypt demonstrates the full compress-then-encrypt pipeline.
package main
import (
"context"
"crypto/rand"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/compress"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
"golang.org/x/crypto/curve25519"
)
func main() {
ctx := context.Background()
keys := crypto.NewStaticKeyResolver()
for _, user := range []string{"alice", "bob"} {
priv := make([]byte, 32)
rand.Read(priv)
pub, _ := curve25519.X25519(priv, curve25519.Basepoint)
keys.AddUser(user, pub, priv)
}
// Register compression BEFORE encryption for compress-then-encrypt.
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(
compress.NewPlugin(compress.Gzip), // step 1: compress
crypto.NewEncryptionPlugin(keys), // step 2: encrypt
),
)
svc.Connect(ctx)
defer svc.Close(ctx)
svc.Client("alice").SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "Compressed and encrypted",
Body: "This message is compressed then encrypted. Subject stays searchable.",
})
inbox, _ := svc.Client("bob").Folder(ctx, store.FolderInbox, store.ListOptions{})
msg := inbox.All()[0]
// Subject is always plaintext (searchable).
fmt.Println("subject:", msg.GetSubject())
fmt.Println("encrypted:", crypto.IsEncrypted(msg))
// Open handles decrypt then decompress automatically.
plaintext, _ := crypto.Open(ctx, msg, "bob", keys)
fmt.Println("body:", string(plaintext))
}
Output: subject: Compressed and encrypted encrypted: true body: This message is compressed then encrypted. Subject stays searchable.
Example (Metadata) ¶
ExampleOpen_metadata demonstrates that encryption metadata is accessible.
package main
import (
"context"
"crypto/rand"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
"golang.org/x/crypto/curve25519"
)
func main() {
ctx := context.Background()
keys := crypto.NewStaticKeyResolver()
for _, user := range []string{"alice", "bob"} {
priv := make([]byte, 32)
rand.Read(priv)
pub, _ := curve25519.X25519(priv, curve25519.Basepoint)
keys.AddUser(user, pub, priv)
}
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(crypto.NewEncryptionPlugin(keys)),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Send with application metadata alongside encryption metadata.
svc.Client("alice").SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "With metadata",
Body: "secret",
Metadata: map[string]any{"priority": "high"},
})
inbox, _ := svc.Client("bob").Folder(ctx, store.FolderInbox, store.ListOptions{})
msg := inbox.All()[0]
meta := msg.GetMetadata()
// Application metadata is preserved alongside encryption metadata.
fmt.Println("priority:", meta["priority"])
fmt.Println("has deks:", meta[crypto.MetaEncryptedDEKs] != nil)
fmt.Println("key type:", meta[crypto.MetaEncryptionKeyType])
}
Output: priority: high has deks: true key type: x25519
Example (SenderDecrypt) ¶
ExampleOpen_senderDecrypt demonstrates that senders can decrypt their own sent messages.
package main
import (
"context"
"crypto/rand"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
"golang.org/x/crypto/curve25519"
)
func main() {
ctx := context.Background()
keys := crypto.NewStaticKeyResolver()
for _, user := range []string{"alice", "bob"} {
priv := make([]byte, 32)
rand.Read(priv)
pub, _ := curve25519.X25519(priv, curve25519.Basepoint)
keys.AddUser(user, pub, priv)
}
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(crypto.NewEncryptionPlugin(keys)),
)
svc.Connect(ctx)
defer svc.Close(ctx)
alice := svc.Client("alice")
alice.SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "My sent message",
Body: "I can read this too",
})
// Alice decrypts her own sent copy.
sent, _ := alice.Folder(ctx, store.FolderSent, store.ListOptions{})
plaintext, _ := crypto.Open(ctx, sent.All()[0], "alice", keys)
fmt.Println(string(plaintext))
}
Output: I can read this too
Types ¶
type EncryptionOption ¶
type EncryptionOption func(*encryptionOptions)
EncryptionOption configures the EncryptionPlugin.
func WithKeyType ¶
func WithKeyType(kt KeyType) EncryptionOption
WithKeyType sets the asymmetric key type for DEK wrapping. Default is X25519.
type EncryptionPlugin ¶
type EncryptionPlugin struct {
// contains filtered or unexported fields
}
EncryptionPlugin encrypts message bodies using envelope encryption. Each message gets a random AES-256-GCM DEK, which is wrapped with each recipient's public key. Register after CompressionPlugin for compress-then-encrypt ordering.
func NewEncryptionPlugin ¶
func NewEncryptionPlugin(keys KeyResolver, opts ...EncryptionOption) *EncryptionPlugin
NewEncryptionPlugin creates an encryption plugin. The KeyResolver provides public keys for recipients. Options control the asymmetric algorithm (default X25519).
Example (MultiRecipient) ¶
ExampleNewEncryptionPlugin_multiRecipient demonstrates multi-recipient encryption.
package main
import (
"context"
"crypto/rand"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
"golang.org/x/crypto/curve25519"
)
func main() {
ctx := context.Background()
keys := crypto.NewStaticKeyResolver()
for _, user := range []string{"alice", "bob", "charlie"} {
priv := make([]byte, 32)
rand.Read(priv)
pub, _ := curve25519.X25519(priv, curve25519.Basepoint)
keys.AddUser(user, pub, priv)
}
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(crypto.NewEncryptionPlugin(keys)),
)
svc.Connect(ctx)
defer svc.Close(ctx)
// Alice sends to bob and charlie. Each gets their own wrapped DEK.
svc.Client("alice").SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob", "charlie"},
Subject: "Group secret",
Body: "Only recipients can read this",
})
// Both recipients can decrypt independently.
for _, user := range []string{"bob", "charlie"} {
inbox, _ := svc.Client(user).Folder(ctx, store.FolderInbox, store.ListOptions{})
plaintext, _ := crypto.Open(ctx, inbox.All()[0], user, keys)
fmt.Printf("%s: %s\n", user, string(plaintext))
}
}
Output: bob: Only recipients can read this charlie: Only recipients can read this
Example (Rsa) ¶
ExampleNewEncryptionPlugin_rsa demonstrates RSA-OAEP encryption.
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"github.com/rbaliyan/mailbox"
"github.com/rbaliyan/mailbox/crypto"
"github.com/rbaliyan/mailbox/store"
"github.com/rbaliyan/mailbox/store/memory"
)
func main() {
ctx := context.Background()
// Generate RSA keypairs.
aliceKey, _ := rsa.GenerateKey(rand.Reader, 2048)
bobKey, _ := rsa.GenerateKey(rand.Reader, 2048)
alicePub, _ := x509.MarshalPKIXPublicKey(&aliceKey.PublicKey)
alicePriv, _ := x509.MarshalPKCS8PrivateKey(aliceKey)
bobPub, _ := x509.MarshalPKIXPublicKey(&bobKey.PublicKey)
bobPriv, _ := x509.MarshalPKCS8PrivateKey(bobKey)
keys := crypto.NewStaticKeyResolver()
keys.AddUser("alice", alicePub, alicePriv)
keys.AddUser("bob", bobPub, bobPriv)
svc, _ := mailbox.New(mailbox.Config{},
mailbox.WithStore(memory.New()),
mailbox.WithPlugins(
crypto.NewEncryptionPlugin(keys, crypto.WithKeyType(crypto.RSAOAEP)),
),
)
svc.Connect(ctx)
defer svc.Close(ctx)
svc.Client("alice").SendMessage(ctx, mailbox.SendRequest{
RecipientIDs: []string{"bob"},
Subject: "RSA encrypted",
Body: "Classified content",
})
inbox, _ := svc.Client("bob").Folder(ctx, store.FolderInbox, store.ListOptions{})
plaintext, _ := crypto.Open(ctx, inbox.All()[0], "bob", keys)
fmt.Println(string(plaintext))
}
Output: Classified content
func (*EncryptionPlugin) BeforeSend ¶
func (p *EncryptionPlugin) BeforeSend(ctx context.Context, userID string, draft store.DraftMessage) error
BeforeSend encrypts the message body and stores per-recipient wrapped DEKs in metadata.
func (*EncryptionPlugin) Name ¶
func (p *EncryptionPlugin) Name() string
type KeyResolver ¶
type KeyResolver interface {
// PublicKey returns the public key for a single user.
// Returns ErrKeyNotFound if the user has no registered key.
PublicKey(ctx context.Context, userID string) ([]byte, error)
// PublicKeys returns public keys for multiple users.
// The returned map contains only users with available keys.
// Users without keys are silently omitted (not an error).
PublicKeys(ctx context.Context, userIDs []string) (map[string][]byte, error)
}
KeyResolver provides public keys for message recipients. Implementations must be safe for concurrent use.
type KeyType ¶
type KeyType string
KeyType identifies the asymmetric algorithm used for DEK wrapping.
const ( // X25519 uses X25519 Diffie-Hellman key agreement with AES-256-GCM for DEK wrapping. // Compact keys (32 bytes) and wrapped DEKs (~80 bytes). Recommended for most use cases. X25519 KeyType = "x25519" // RSAOAEP uses RSA-OAEP with SHA-256 for DEK wrapping. // Larger keys and wrapped DEKs (~256 bytes for RSA-2048). Compatible with legacy PKI. RSAOAEP KeyType = "rsa-oaep" )
type MessageReader ¶
type MessageReader interface {
GetBody() string
GetHeaders() map[string]string
GetMetadata() map[string]any
}
MessageReader is the minimal interface needed for decryption. Satisfied by store.Message and mailbox.Message.
type PrivateKeyProvider ¶
type PrivateKeyProvider interface {
// PrivateKey returns the private key for a user.
// Returns ErrKeyNotFound if the user has no registered key.
PrivateKey(ctx context.Context, userID string) ([]byte, error)
}
PrivateKeyProvider provides private keys for message decryption. Implementations must be safe for concurrent use.
type StaticKeyResolver ¶
type StaticKeyResolver struct {
// contains filtered or unexported fields
}
StaticKeyResolver is an in-memory key resolver for testing and simple deployments. It implements both KeyResolver and PrivateKeyProvider.
func NewStaticKeyResolver ¶
func NewStaticKeyResolver() *StaticKeyResolver
NewStaticKeyResolver creates an empty static key resolver.
func (*StaticKeyResolver) AddUser ¶
func (r *StaticKeyResolver) AddUser(userID string, publicKey, privateKey []byte)
AddUser registers a public and private key pair for a user.
func (*StaticKeyResolver) PrivateKey ¶
PrivateKey returns the private key for a user.
func (*StaticKeyResolver) PublicKeys ¶
func (r *StaticKeyResolver) PublicKeys(_ context.Context, userIDs []string) (map[string][]byte, error)
PublicKeys returns public keys for multiple users.