This commit is contained in:
2025-09-14 16:17:05 +02:00
parent 0cbf0758f9
commit 0c2d805fa0
6 changed files with 571 additions and 2 deletions

View File

@@ -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!')

133
lib/hero/crypt/age.v Normal file
View File

@@ -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
}

81
lib/hero/crypt/age_test.v Normal file
View File

@@ -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
}

31
lib/hero/crypt/factory.v Normal file
View File

@@ -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
}
}

27
lib/hero/crypt/model.v Normal file
View File

@@ -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
}

View File

@@ -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
# 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)!