diff --git a/examples/hero/crypt/hero_crypt_example.vsh b/examples/hero/crypt/hero_crypt_example.vsh new file mode 100755 index 00000000..797f110e --- /dev/null +++ b/examples/hero/crypt/hero_crypt_example.vsh @@ -0,0 +1,112 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.hero.crypt +import freeflowuniverse.herolib.core.redisclient + +// Connect to default Redis instance (127.0.0.1:6379) +mut age_client := crypt.new_age_client()! + +// Test stateless encryption +println('Testing stateless encryption...') +keypair := age_client.generate_keypair() or { + println('Error generating keypair: ${err}') + return +} + +message := 'Hello, AGE encryption!' +encrypted := age_client.encrypt(keypair.recipient, message) or { + println('Error encrypting message: ${err}') + return +} + +decrypted := age_client.decrypt(keypair.identity, encrypted.ciphertext) or { + println('Error decrypting message: ${err}') + return +} + +println('Original message: ${message}') +println('Encrypted message: ${encrypted.ciphertext}') +println('Decrypted message: ${decrypted}') + +assert decrypted == message +println('Stateless encryption test passed!') + +// Test stateless signing +println('\nTesting stateless signing...') +signing_keypair := age_client.generate_signing_keypair() or { + println('Error generating signing keypair: ${err}') + return +} + +!signed := age_client.sign(signing_keypair.sign_key, message) or { + println('Error signing message: ${err}') + return +} + +verified := age_client.verify(signing_keypair.verify_key, message, signed.signature) or { + println('Error verifying signature: ${err}') + return +} +!println('Message: ${message}') +println('Signature: ${signed.signature}') +println('Signature valid: ${verified}') + +assert verified == true +println('Stateless signing test passed!') + +// Test key-managed encryption +println('\nTesting key-managed encryption...') +key_name := 'example_encryption_key' +named_keypair := age_client.create_named_keypair(key_name) or { + println('Error creating named keypair: ${err}') + return +} + +named_encrypted := age_client.encrypt_with_named_key(key_name, message) or { + println('Error encrypting with named key: ${err}') + return +} + +named_decrypted := age_client.decrypt_with_named_key(key_name, named_encrypted.ciphertext) or { + println('Error decrypting with named key: ${err}') + return +} +println('Key name: ${key_name}') +println('Encrypted with named key: ${named_encrypted.ciphertext}') +println('Decrypted with named key: ${named_decrypted}') + +assert named_decrypted == message +println('Key-managed encryption test passed!') + +// Test key-managed signing +println('\nTesting key-managed signing...') +signing_key_name := 'example_signing_key' +age_client.create_named_signing_keypair(signing_key_name) or { + println('Error creating named signing keypair: ${err}') + return +} + +named_signed := age_client.sign_with_named_key(signing_key_name, message) or { + println('Error signing with named key: ${err}') + return +} + +named_verified := age_client.verify_with_named_key(signing_key_name, message, named_signed.signature) or { + println('Error verifying with named key: ${err}') + return +} +println('Signing key name: ${signing_key_name}') +println('Signature: ${named_signed.signature}') +println('Signature valid: ${named_verified}') + +assert named_verified == true +println('Key-managed signing test passed!') + +// Test list keys +println('\nTesting list keys...') +keys := age_client.list_keys() or { + println('Error listing keys: ${err}') + return +} +println('Stored keys: ${keys}') +println('All tests completed successfully!') diff --git a/lib/hero/crypt/age.v b/lib/hero/crypt/age.v new file mode 100644 index 00000000..5b8159ef --- /dev/null +++ b/lib/hero/crypt/age.v @@ -0,0 +1,133 @@ +module crypt + +import freeflowuniverse.herolib.core.redisclient + +// Stateless AGE operations + +// generate_keypair creates a new Age encryption key pair +pub fn (client &AGEClient) generate_keypair() !KeyPair { + response := client.redis.send_expect_list(['AGE', 'GENENC'])! + + if response.len < 2 { + return error('Invalid response from AGE GENENC command') + } + + return KeyPair{ + recipient: response[0].str() + identity: response[1].str() + } +} + +// generate_signing_keypair creates a new Age signing key pair +pub fn (client &AGEClient) generate_signing_keypair() !SigningKeyPair { + response := client.redis.send_expect_list(['AGE', 'GENSIGN'])! + + if response.len < 2 { + return error('Invalid response from AGE GENSIGN command') + } + + return SigningKeyPair{ + verify_key: response[0].str() + sign_key: response[1].str() + } +} + +// encrypt encrypts a message with the recipient's public key +pub fn (client &AGEClient) encrypt(recipient string, message string) !EncryptionResult { + ciphertext := client.redis.send_expect_str(['AGE', 'ENCRYPT', recipient, message])! + + return EncryptionResult{ + ciphertext: ciphertext + } +} + +// decrypt decrypts a message with the identity (private key) +pub fn (client &AGEClient) decrypt(identity string, ciphertext string) !string { + return client.redis.send_expect_str(['AGE', 'DECRYPT', identity, ciphertext])! +} + +// sign signs a message with the signing key +pub fn (client &AGEClient) sign(sign_key string, message string) !SignatureResult { + signature := client.redis.send_expect_str(['AGE', 'SIGN', sign_key, message])! + + return SignatureResult{ + signature: signature + } +} + +// verify verifies a signature with the verification key +pub fn (client &AGEClient) verify(verify_key string, message string, signature string) !bool { + result := client.redis.send_expect_int(['AGE', 'VERIFY', verify_key, message, signature])! + return result == 1 +} + +// Key-managed AGE operations + +// create_named_keypair creates and stores a named encryption key pair +pub fn (client &AGEClient) create_named_keypair(name string) !KeyPair { + response := client.redis.send_expect_list(['AGE', 'KEYGEN', name])! + + if response.len < 2 { + return error('Invalid response from AGE KEYGEN command') + } + + return KeyPair{ + recipient: response[0].str() + identity: response[1].str() + } +} + +// create_named_signing_keypair creates and stores a named signing key pair +pub fn (client &AGEClient) create_named_signing_keypair(name string) !SigningKeyPair { + response := client.redis.send_expect_list(['AGE', 'SIGNKEYGEN', name])! + + if response.len < 2 { + return error('Invalid response from AGE SIGNKEYGEN command') + } + + return SigningKeyPair{ + verify_key: response[0].str() + sign_key: response[1].str() + } +} + +// encrypt_with_named_key encrypts a message using a named key +pub fn (client &AGEClient) encrypt_with_named_key(key_name string, message string) !EncryptionResult { + ciphertext := client.redis.send_expect_str(['AGE', 'ENCRYPTNAME', key_name, message])! + + return EncryptionResult{ + ciphertext: ciphertext + } +} + +// decrypt_with_named_key decrypts a message using a named key +pub fn (client &AGEClient) decrypt_with_named_key(key_name string, ciphertext string) !string { + return client.redis.send_expect_str(['AGE', 'DECRYPTNAME', key_name, ciphertext])! +} + +// sign_with_named_key signs a message using a named signing key +pub fn (client &AGEClient) sign_with_named_key(key_name string, message string) !SignatureResult { + signature := client.redis.send_expect_str(['AGE', 'SIGNNAME', key_name, message])! + + return SignatureResult{ + signature: signature + } +} + +// verify_with_named_key verifies a signature using a named verification key +pub fn (client &AGEClient) verify_with_named_key(key_name string, message string, signature string) !bool { + result := client.redis.send_expect_int(['AGE', 'VERIFYNAME', key_name, message, signature])! + return result == 1 +} + +// list_keys lists all stored AGE keys +pub fn (client &AGEClient) list_keys() ![]string { + response := client.redis.send_expect_list(['AGE', 'LIST'])! + + mut keys := []string{} + for i in 0 .. response.len { + keys << response[i].str() + } + + return keys +} diff --git a/lib/hero/crypt/age_test.v b/lib/hero/crypt/age_test.v new file mode 100644 index 00000000..ea6b3755 --- /dev/null +++ b/lib/hero/crypt/age_test.v @@ -0,0 +1,81 @@ +module crypt + +fn test_stateless_encryption() { + mut client := new_age_client()! + + // Generate a keypair + keypair := client.generate_keypair()! + + // Encrypt a message + message := 'Hello, AGE encryption!' + encrypted := client.encrypt(keypair.recipient, message)! + + // Decrypt the message + decrypted := client.decrypt(keypair.identity, encrypted.ciphertext)! + + assert decrypted == message +} + +fn test_stateless_signing() { + mut client := new_age_client()! + + // Generate a signing keypair + keypair := client.generate_signing_keypair()! + + // Sign a message + message := 'This message is signed' + signed := client.sign(keypair.sign_key, message)! + + // Verify the signature + verified := client.verify(keypair.verify_key, message, signed.signature)! + + assert verified == true +} + +fn test_key_managed_encryption() { + mut client := new_age_client()! + + // Create a named keypair + key_name := 'test_encryption_key' + client.create_named_keypair(key_name)! + + // Encrypt with the named key + message := 'Hello, key-managed encryption!' + encrypted := client.encrypt_with_named_key(key_name, message)! + + // Decrypt with the named key + decrypted := client.decrypt_with_named_key(key_name, encrypted.ciphertext)! + + assert decrypted == message +} + +fn test_key_managed_signing() { + mut client := new_age_client()! + + // Create a named signing keypair + key_name := 'test_signing_key' + client.create_named_signing_keypair(key_name)! + + // Sign with the named key + message := 'This message is signed with a named key' + signed := client.sign_with_named_key(key_name, message)! + + // Verify with the named key + verified := client.verify_with_named_key(key_name, message, signed.signature)! + + assert verified == true +} + +fn test_list_keys() { + mut client := new_age_client()! + + // Create some named keys + client.create_named_keypair('list_test_key1')! + client.create_named_signing_keypair('list_test_key2')! + + // List the keys + keys := client.list_keys()! + + assert 'list_test_key1' in keys + assert 'list_test_key2' in keys +} diff --git a/lib/hero/crypt/factory.v b/lib/hero/crypt/factory.v new file mode 100644 index 00000000..f136f8ab --- /dev/null +++ b/lib/hero/crypt/factory.v @@ -0,0 +1,31 @@ +module crypt + +import freeflowuniverse.herolib.core.redisclient + +// AGEClient provides access to the Age encryption features in HeroDB +pub struct AGEClient { +pub mut: + redis &redisclient.Redis @[str: skip] +} + +@[params] +pub struct AGEClientConfig { +pub mut: + redis_url redisclient.RedisURL = redisclient.RedisURL{} + redis ?&redisclient.Redis +} + +// new_age_client creates a new AGE encryption client +pub fn new_age_client(config AGEClientConfig) !&AGEClient { + // If a Redis client is provided, use it + mut redis := if r := config.redis { + r + } else { + // Otherwise create a new Redis client + redisclient.core_get(config.redis_url)! + } + + return &AGEClient{ + redis: redis + } +} diff --git a/lib/hero/crypt/model.v b/lib/hero/crypt/model.v new file mode 100644 index 00000000..43b7ce0c --- /dev/null +++ b/lib/hero/crypt/model.v @@ -0,0 +1,27 @@ +module crypt + +// KeyPair represents an Age encryption key pair +pub struct KeyPair { +pub: + recipient string // Public key (can be shared) + identity string // Private key (must be kept secret) +} + +// SigningKeyPair represents an Age signing key pair +pub struct SigningKeyPair { +pub: + verify_key string // Public verification key + sign_key string // Private signing key +} + +// EncryptionResult represents the result of an encryption operation +pub struct EncryptionResult { +pub: + ciphertext string // Base64-encoded encrypted data +} + +// SignatureResult represents the result of a signing operation +pub struct SignatureResult { +pub: + signature string // Base64-encoded signature +} diff --git a/lib/hero/crypt/readme.md b/lib/hero/crypt/readme.md index bb27f194..5fdeeb37 100644 --- a/lib/hero/crypt/readme.md +++ b/lib/hero/crypt/readme.md @@ -1,2 +1,187 @@ -this will have client using the redis client as basis, but with some added functionality -this functionality is for AGE features from herodb which is our own customer redis server \ No newline at end of file +# HeroDB AGE Encryption (hero.crypt) + +This module provides client-side access to HeroDB's AGE encryption features, offering two distinct modes of operation: + +- **Stateless Mode**: Client manages keys, nothing stored on server +- **Key-Managed Mode**: Server stores named keys + +## Installation + +Ensure HeroDB is running with encryption enabled: + +```bash +herodb --dir /path/to/data --port 6379 --encrypt --encryption-key yoursecretkey +``` + +## Usage + +### Creating an AGE Client + +```v +import freeflowuniverse.herolib.hero.crypt +import freeflowuniverse.herolib.core.redisclient + +// Connect to default Redis instance (127.0.0.1:6379) +mut age_client := crypt.new_age_client()! + +// Or connect to a specific Redis instance +mut age_client := crypt.new_age_client( + redis_url: redisclient.RedisURL{ + address: 'my.herodb.server', + port: 6379 + } +)! + +// Or use an existing Redis client +mut redis := redisclient.core_get()! +mut age_client := crypt.new_age_client( + redis: redis +)! +``` + +## Stateless Encryption (Client-Managed Keys) + +In stateless mode, the client generates and manages keys. The server never stores any keys. + +### Generate a Keypair + +```v +// Generate a new encryption keypair +keypair := age_client.generate_keypair()! +println('Public key: ${keypair.recipient}') +println('Private key: ${keypair.identity}') + +// Important: Store the identity (private key) securely! +``` + +### Encrypt and Decrypt + +```v +// Encrypt a message with the public key +message := 'Hello, encrypted world!' +encrypted := age_client.encrypt(keypair.recipient, message)! +println('Encrypted: ${encrypted.ciphertext}') + +// Decrypt with the private key +decrypted := age_client.decrypt(keypair.identity, encrypted.ciphertext)! +println('Decrypted: ${decrypted}') +``` + +### Signing and Verification + +```v +// Generate a signing keypair +signing_keypair := age_client.generate_signing_keypair()! + +// Sign a message +message := 'This message is authentic' +signed := age_client.sign(signing_keypair.sign_key, message)! +println('Signature: ${signed.signature}') + +// Verify the signature +is_valid := age_client.verify(signing_keypair.verify_key, message, signed.signature)! +println('Signature valid: ${is_valid}') +``` + +## Key-Managed Encryption (Server-Stored Keys) + +In key-managed mode, the server generates and stores named keys. Clients reference keys by name. + +### Create Named Keys + +```v +// Create a named encryption keypair +named_keypair := age_client.create_named_keypair('app1_encryption')! +println('Created encryption keypair: ${named_keypair.recipient}') + +// Create a named signing keypair +named_signing_keypair := age_client.create_named_signing_keypair('app1_signing')! +println('Created signing keypair: ${named_signing_keypair.verify_key}') +``` + +### Encrypt and Decrypt with Named Keys + +```v +// Encrypt with a named key +message := 'This message is encrypted with a named key' +encrypted := age_client.encrypt_with_named_key('app1_encryption', message)! +println('Encrypted: ${encrypted.ciphertext}') + +// Decrypt with a named key +decrypted := age_client.decrypt_with_named_key('app1_encryption', encrypted.ciphertext)! +println('Decrypted: ${decrypted}') +``` + +### Sign and Verify with Named Keys + +```v +// Sign with a named key +message := 'This message is signed with a named key' +signed := age_client.sign_with_named_key('app1_signing', message)! +println('Signature: ${signed.signature}') + +// Verify with a named key +is_valid := age_client.verify_with_named_key('app1_signing', message, signed.signature)! +println('Signature valid: ${is_valid}') +``` + +### List Stored Keys + +```v +// List all stored keys +keys := age_client.list_keys()! +println('Stored keys: ${keys}') +``` + +## Choosing Between Modes + +### Use Stateless Mode When: + +- You want complete control over key management +- You don't want the server to store any private keys +- You need to use the same keys across multiple servers + +### Use Key-Managed Mode When: + +- You want simpler client code (no need to manage keys) +- You trust the server to securely store private keys +- You need centralized key management with rotation options + +## Security Considerations + +1. **Private Keys**: In stateless mode, you are responsible for securely storing private keys. +2. **Server Security**: In key-managed mode, ensure your HeroDB server is properly secured. +3. **Encryption at Rest**: HeroDB supports database-level encryption separate from AGE commands. +4. **Transport Security**: Consider using SSL/TLS to protect data in transit. + +## Examples + +### Secure Message Exchange + +```v +// Sender (Alice) +mut alice_client := crypt.new_age_client()! +alice_keypair := alice_client.generate_keypair()! + +// Bob gets Alice's public key (recipient) +mut bob_client := crypt.new_age_client()! +bob_message := 'Secret message for Alice' +encrypted := bob_client.encrypt(alice_keypair.recipient, bob_message)! + +// Alice decrypts Bob's message +decrypted := alice_client.decrypt(alice_keypair.identity, encrypted.ciphertext)! +assert decrypted == bob_message +``` + +### Document Signing + +```v +// Author +mut author_client := crypt.new_age_client()! +author_keypair := author_client.create_named_signing_keypair('document_author')! +document := 'This is an official document' +signature := author_client.sign_with_named_key('document_author', document)! + +// Verifier +mut verifier_client := crypt.new_age_client()! +is_authentic := verifier_client.verify_with_named_key('document_author', document, signature.signature)!