1 Commits
master ... seif

Author SHA1 Message Date
John Alanbrook
f4ea552271 initial try at seif handshake example
Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
2025-05-25 00:07:20 -05:00
5 changed files with 566 additions and 0 deletions

50
examples/seif_client.js Normal file
View File

@@ -0,0 +1,50 @@
// Seif Handshake Client Example
// Implements the Seif Protocol handshake client side
var crypto = use('crypto');
var json = use('json');
var io = use('io');
// Alice's key pair
var alice_keys = crypto.keypair();
console.log("Alice public key:", alice_keys.public);
console.log("Alice private key:", alice_keys.private);
// Bob's public key (in real usage, this would be obtained separately)
// For this example, we'll use a hardcoded key or read from file
var bob_public_key = null;
// Try to read Bob's public key from file if it exists
if (io.exists('bob_public.key')) {
bob_public_key = io.slurp('bob_public.key').trim();
console.log("Loaded Bob's public key from file:", bob_public_key);
} else {
// For testing, use the server's public key printed by seif_server.js
console.log("Please create bob_public.key with the server's public key");
console.log("Run seif_server.js first to get the public key");
return;
}
// Generate random handshake key
var handshake_key = crypto.keypair().public; // Using public key as random 32-byte value
console.log("Generated handshake key:", handshake_key);
console.log("Sending handshake to server...");
// Contact the server
$_.contact((actor, reason) => {
if (!actor) {
console.error("Could not establish connection:", reason);
return;
}
}, {
address: "localhost",
port: 5678,
seif: 1,
handshake: crypto.encrypt_pk(bob_public_key, handshake_key),
payload: crypto.encrypt(handshake_key, alice_keys.public)
});
$_.receiver(e => {
console.log("Received message:", e);
});

View File

@@ -0,0 +1,83 @@
# Seif Handshake Examples
This directory contains examples demonstrating the Seif Protocol handshake implementation in Prosperon.
## Files
- `seif_simple.js` - A standalone demonstration of the Seif handshake cryptographic operations
- `seif_server.js` - A server that accepts Seif handshake connections
- `seif_client.js` - A client that initiates Seif handshake with a server
## Running the Examples
### Simple Demo
To see the cryptographic operations in action:
```bash
./prosperon examples/seif_simple.js
```
### Client-Server Demo
1. First, start the server:
```bash
./prosperon examples/seif_server.js
```
2. Note the server's public key that is printed
3. Create a file `bob_public.key` with the server's public key:
```bash
echo "SERVER_PUBLIC_KEY_HERE" > bob_public.key
```
4. In another terminal, run the client:
```bash
./prosperon examples/seif_client.js
```
## The Seif Protocol
The Seif handshake establishes a secure session in one round trip:
1. **Alice's Message**:
- Generates random `handshake_key`
- Sends: `{seif: 1, handshake: encrypt_pk(bob_public, handshake_key), payload: encrypt(handshake_key, alice_public)}`
2. **Bob's Response**:
- Decrypts `handshake_key` using his private key
- Decrypts Alice's public key from payload
- Generates `session_key`
- Sends: `encrypt(handshake_key, {session: encrypt_pk(alice_public, session_key)})`
3. **Result**: Both parties share `session_key` for symmetric encryption
## Actor System Integration
In Prosperon's actor system:
- Actor objects can serve as public key identifiers (they contain unique IDs)
- The `$_.portal()` function creates a listening endpoint
- The `$_.contact()` function initiates connections
- Messages are automatically routed through the actor system
## Security Properties
- **Authentication**: Both parties prove possession of their private keys
- **Forward Secrecy**: Session keys are ephemeral
- **Man-in-the-Middle Protection**: Requires knowledge of both private keys
- **One Round Trip**: Efficient session establishment
## Notes on Implementation
The current implementation uses the actor object's ID as part of the identity system. In a production system, you might want to:
1. Store the public key as part of the actor's data
2. Use a proper key derivation function for session keys
3. Add additional metadata in the handshake (timestamps, nonces, etc.)
4. Implement key rotation and session management
The crypto module provides:
- `crypto.keypair()` - Generate X25519 key pairs
- `crypto.encrypt_pk(public_key, data)` - Public key encryption
- `crypto.decrypt_pk(private_key, data)` - Public key decryption
- `crypto.encrypt(key, data)` - Symmetric encryption
- `crypto.decrypt(key, data)` - Symmetric decryption

85
examples/seif_server.js Normal file
View File

@@ -0,0 +1,85 @@
// Seif Handshake Server Example
// Implements the Seif Protocol handshake as described in the documentation
var crypto = use('crypto');
var json = use('json');
var io = use('io');
// Server's key pair
var server_keys = crypto.keypair();
console.log("Server public key:", server_keys.public);
console.log("Server private key:", server_keys.private);
// Store connected clients
var clients = {};
$_.portal(e => {
// Verify the handshake message format
if (!e.seif || !e.handshake || !e.payload) {
send(e, {error:"Invalid Seif handshake format"});
return;
}
if (e.seif !== 1) {
send(e, {error:"Unsupported Seif protocol version:", e.seif});
return;
}
try {
// Decrypt the handshake key with server's private key
var handshake_key_encrypted = e.handshake;
var handshake_key_hex = crypto.decrypt_pk(server_keys.private, handshake_key_encrypted);
// Convert ArrayBuffer to hex string
var handshake_key_bytes = new Uint8Array(handshake_key_hex);
var handshake_key = '';
for (var i = 0; i < handshake_key_bytes.length; i++) {
var hex = handshake_key_bytes[i].toString(16);
handshake_key += (hex.length === 1 ? '0' : '') + hex;
}
console.log("Decrypted handshake key:", handshake_key);
// Decrypt the payload (client's public key) with handshake key
var client_public_encrypted = e.payload;
var client_public_buffer = crypto.decrypt(handshake_key, client_public_encrypted);
// Convert decrypted buffer to string
var client_public_bytes = new Uint8Array(client_public_buffer);
var client_public = '';
for (var i = 0; i < client_public_bytes.length; i++) {
client_public += String.fromCharCode(client_public_bytes[i]);
}
console.log("Client's public key:", client_public);
// Generate session key
var session_key = crypto.keypair();
console.log("Generated session key:", session_key.public);
// Create response encrypted with handshake key
var response = {
session: crypto.encrypt_pk(client_public, session_key.public)
};
var response_encrypted = crypto.encrypt(handshake_key, json.encode(response));
// Send encrypted response
send(e, response_encrypted);
console.log("Handshake complete with client:", client_public);
} catch (err) {
send(e, {error:err})
}
}, 5678);
// Handle messages from connected clients
$_.receiver(e => {
if (e.type === 'encrypted_message') {
console.log("Received encrypted message");
// In a real implementation, decrypt with session key and process
}
});
console.log("Seif server listening on port 5678");

118
examples/seif_simple.js Normal file
View File

@@ -0,0 +1,118 @@
// Simplified Seif Handshake Example
// This demonstrates the core cryptographic operations of the Seif handshake
var crypto = use('crypto');
var json = use('json');
console.log("=== Seif Handshake Demo ===\n");
// Step 1: Generate key pairs for Alice and Bob
console.log("1. Generating key pairs...");
var alice_keys = crypto.keypair();
var bob_keys = crypto.keypair();
console.log("Alice's public key:", alice_keys.public);
console.log("Bob's public key:", bob_keys.public);
// Step 2: Alice initiates handshake
console.log("\n2. Alice initiates handshake...");
// Alice generates a random handshake key
var handshake_key = crypto.keypair().public; // Using public key generation for random 32 bytes
console.log("Handshake key:", handshake_key);
// Alice creates the handshake message
var alice_message = {
seif: 1,
handshake: crypto.encrypt_pk(bob_keys.public, handshake_key),
payload: crypto.encrypt(handshake_key, alice_keys.public)
};
console.log("Alice's message created (encrypted components)");
// Step 3: Bob processes the handshake
console.log("\n3. Bob processes the handshake...");
// Bob decrypts the handshake key
var decrypted_handshake_key_buffer = crypto.decrypt_pk(bob_keys.private, alice_message.handshake);
// Convert buffer to hex string
var handshake_key_bytes = new Uint8Array(decrypted_handshake_key_buffer);
var recovered_handshake_key = '';
for (var i = 0; i < handshake_key_bytes.length; i++) {
var hex = handshake_key_bytes[i].toString(16);
recovered_handshake_key += (hex.length === 1 ? '0' : '') + hex;
}
console.log("Bob recovered handshake key:", recovered_handshake_key);
console.log("Keys match:", recovered_handshake_key === handshake_key);
// Bob decrypts Alice's public key
var alice_public_buffer = crypto.decrypt(recovered_handshake_key, alice_message.payload);
var alice_public_bytes = new Uint8Array(alice_public_buffer);
var recovered_alice_public = '';
for (var i = 0; i < alice_public_bytes.length; i++) {
recovered_alice_public += String.fromCharCode(alice_public_bytes[i]);
}
console.log("Bob recovered Alice's public key:", recovered_alice_public);
console.log("Public keys match:", recovered_alice_public === alice_keys.public);
// Step 4: Bob generates session key and responds
console.log("\n4. Bob generates session key and responds...");
// Generate a random session key
var session_key = crypto.keypair().public;
console.log("Session key:", session_key);
// Bob encrypts the session key with Alice's public key
var bob_response = {
session: crypto.encrypt_pk(alice_keys.public, session_key)
};
// Encrypt the entire response with the handshake key
var encrypted_response = crypto.encrypt(handshake_key, json.encode(bob_response));
console.log("Bob's encrypted response created");
// Step 5: Alice processes Bob's response
console.log("\n5. Alice processes Bob's response...");
// Alice decrypts the response
var decrypted_response_buffer = crypto.decrypt(handshake_key, encrypted_response);
var response_json = '';
var response_bytes = new Uint8Array(decrypted_response_buffer);
for (var i = 0; i < response_bytes.length; i++) {
response_json += String.fromCharCode(response_bytes[i]);
}
var response_data = json.decode(response_json);
// Alice decrypts the session key
var session_key_buffer = crypto.decrypt_pk(alice_keys.private, response_data.session);
var session_key_bytes = new Uint8Array(session_key_buffer);
var recovered_session_key = '';
for (var i = 0; i < session_key_bytes.length; i++) {
var hex = session_key_bytes[i].toString(16);
recovered_session_key += (hex.length === 1 ? '0' : '') + hex;
}
console.log("Alice recovered session key:", recovered_session_key);
console.log("Session keys match:", recovered_session_key === session_key);
// Step 6: Demonstrate secure communication
console.log("\n6. Secure communication established!");
console.log("Both parties now share the session key and can communicate securely.");
// Example encrypted message
var message = "Hello, this is a secret message!";
var encrypted_msg = crypto.encrypt(session_key, message);
console.log("\nAlice encrypts:", message);
var decrypted_msg_buffer = crypto.decrypt(session_key, encrypted_msg);
var decrypted_msg = '';
var msg_bytes = new Uint8Array(decrypted_msg_buffer);
for (var i = 0; i < msg_bytes.length; i++) {
decrypted_msg += String.fromCharCode(msg_bytes[i]);
}
console.log("Bob decrypts:", decrypted_msg);
console.log("\n=== Seif Handshake Complete ===");

View File

@@ -199,10 +199,240 @@ JSValue js_crypto_random(JSContext *js, JSValue self, int argc, JSValue *argv)
return JS_NewFloat64(js, val);
}
JSValue js_crypto_encrypt_pk(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2) {
return JS_ThrowTypeError(js, "crypto.encrypt_pk: expected 2 arguments (public_key, plaintext)");
}
uint8_t public_key[32];
js2crypto(js, argv[0], public_key);
size_t plaintext_len;
uint8_t *plaintext;
if (JS_IsString(argv[1])) {
const char *str = JS_ToCStringLen(js, &plaintext_len, argv[1]);
if (!str) return JS_EXCEPTION;
plaintext = (uint8_t *)str;
} else {
plaintext = JS_GetArrayBuffer(js, &plaintext_len, argv[1]);
if (!plaintext) {
return JS_ThrowTypeError(js, "crypto.encrypt_pk: plaintext must be string or ArrayBuffer");
}
}
// Generate ephemeral keypair
uint8_t ephemeral_secret[32];
uint8_t ephemeral_public[32];
randombytes(ephemeral_secret, 32);
ephemeral_secret[0] &= 248;
ephemeral_secret[31] &= 127;
ephemeral_secret[31] |= 64;
crypto_x25519_public_key(ephemeral_public, ephemeral_secret);
// Compute shared secret
uint8_t shared[32];
crypto_x25519(shared, ephemeral_secret, public_key);
// Derive encryption key using BLAKE2b
uint8_t key[32];
crypto_blake2b(key, 32, shared, 32);
// Generate random nonce
uint8_t nonce[24];
randombytes(nonce, 24);
// Allocate output buffer: ephemeral_public(32) + nonce(24) + ciphertext + mac(16)
size_t output_size = 32 + 24 + plaintext_len + 16;
uint8_t *output = js_malloc(js, output_size);
if (!output) {
if (JS_IsString(argv[1])) JS_FreeCString(js, (const char *)plaintext);
return JS_EXCEPTION;
}
// Copy ephemeral public key and nonce to output
memcpy(output, ephemeral_public, 32);
memcpy(output + 32, nonce, 24);
// Encrypt
crypto_aead_lock(output + 32 + 24, output + 32 + 24 + plaintext_len,
key, nonce, NULL, 0, plaintext, plaintext_len);
if (JS_IsString(argv[1])) JS_FreeCString(js, (const char *)plaintext);
// Wipe sensitive data
crypto_wipe(ephemeral_secret, 32);
crypto_wipe(shared, 32);
crypto_wipe(key, 32);
JSValue result = JS_NewArrayBufferCopy(js, output, output_size);
js_free(js, output);
return result;
}
JSValue js_crypto_decrypt_pk(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2) {
return JS_ThrowTypeError(js, "crypto.decrypt_pk: expected 2 arguments (private_key, ciphertext)");
}
uint8_t private_key[32];
js2crypto(js, argv[0], private_key);
size_t ciphertext_len;
uint8_t *ciphertext = JS_GetArrayBuffer(js, &ciphertext_len, argv[1]);
if (!ciphertext) {
return JS_ThrowTypeError(js, "crypto.decrypt_pk: ciphertext must be ArrayBuffer");
}
if (ciphertext_len < 32 + 24 + 16) {
return JS_ThrowTypeError(js, "crypto.decrypt_pk: ciphertext too short");
}
// Extract ephemeral public key and nonce
uint8_t ephemeral_public[32];
uint8_t nonce[24];
memcpy(ephemeral_public, ciphertext, 32);
memcpy(nonce, ciphertext + 32, 24);
// Compute shared secret
uint8_t shared[32];
crypto_x25519(shared, private_key, ephemeral_public);
// Derive decryption key
uint8_t key[32];
crypto_blake2b(key, 32, shared, 32);
// Decrypt
size_t plaintext_len = ciphertext_len - 32 - 24 - 16;
uint8_t *plaintext = js_malloc(js, plaintext_len);
if (!plaintext) {
crypto_wipe(shared, 32);
crypto_wipe(key, 32);
return JS_EXCEPTION;
}
int result = crypto_aead_unlock(plaintext,
ciphertext + ciphertext_len - 16,
key, nonce, NULL, 0,
ciphertext + 32 + 24, plaintext_len);
crypto_wipe(shared, 32);
crypto_wipe(key, 32);
if (result != 0) {
js_free(js, plaintext);
return JS_ThrowTypeError(js, "crypto.decrypt_pk: decryption failed");
}
JSValue ret = JS_NewArrayBufferCopy(js, plaintext, plaintext_len);
js_free(js, plaintext);
return ret;
}
JSValue js_crypto_encrypt(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2) {
return JS_ThrowTypeError(js, "crypto.encrypt: expected 2 arguments (key, plaintext)");
}
uint8_t key[32];
js2crypto(js, argv[0], key);
size_t plaintext_len;
uint8_t *plaintext;
if (JS_IsString(argv[1])) {
const char *str = JS_ToCStringLen(js, &plaintext_len, argv[1]);
if (!str) return JS_EXCEPTION;
plaintext = (uint8_t *)str;
} else {
plaintext = JS_GetArrayBuffer(js, &plaintext_len, argv[1]);
if (!plaintext) {
return JS_ThrowTypeError(js, "crypto.encrypt: plaintext must be string or ArrayBuffer");
}
}
// Generate random nonce
uint8_t nonce[24];
randombytes(nonce, 24);
// Allocate output buffer: nonce(24) + ciphertext + mac(16)
size_t output_size = 24 + plaintext_len + 16;
uint8_t *output = js_malloc(js, output_size);
if (!output) {
if (JS_IsString(argv[1])) JS_FreeCString(js, (const char *)plaintext);
return JS_EXCEPTION;
}
// Copy nonce to output
memcpy(output, nonce, 24);
// Encrypt
crypto_aead_lock(output + 24, output + 24 + plaintext_len,
key, nonce, NULL, 0, plaintext, plaintext_len);
if (JS_IsString(argv[1])) JS_FreeCString(js, (const char *)plaintext);
JSValue result = JS_NewArrayBufferCopy(js, output, output_size);
js_free(js, output);
return result;
}
JSValue js_crypto_decrypt(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2) {
return JS_ThrowTypeError(js, "crypto.decrypt: expected 2 arguments (key, ciphertext)");
}
uint8_t key[32];
js2crypto(js, argv[0], key);
size_t ciphertext_len;
uint8_t *ciphertext = JS_GetArrayBuffer(js, &ciphertext_len, argv[1]);
if (!ciphertext) {
return JS_ThrowTypeError(js, "crypto.decrypt: ciphertext must be ArrayBuffer");
}
if (ciphertext_len < 24 + 16) {
return JS_ThrowTypeError(js, "crypto.decrypt: ciphertext too short");
}
// Extract nonce
uint8_t nonce[24];
memcpy(nonce, ciphertext, 24);
// Decrypt
size_t plaintext_len = ciphertext_len - 24 - 16;
uint8_t *plaintext = js_malloc(js, plaintext_len);
if (!plaintext) {
return JS_EXCEPTION;
}
int result = crypto_aead_unlock(plaintext,
ciphertext + ciphertext_len - 16,
key, nonce, NULL, 0,
ciphertext + 24, plaintext_len);
if (result != 0) {
js_free(js, plaintext);
return JS_ThrowTypeError(js, "crypto.decrypt: decryption failed");
}
JSValue ret = JS_NewArrayBufferCopy(js, plaintext, plaintext_len);
js_free(js, plaintext);
return ret;
}
static const JSCFunctionListEntry js_crypto_funcs[] = {
JS_CFUNC_DEF("keypair", 0, js_crypto_keypair),
JS_CFUNC_DEF("shared", 1, js_crypto_shared),
JS_CFUNC_DEF("random", 0, js_crypto_random),
JS_CFUNC_DEF("encrypt_pk", 2, js_crypto_encrypt_pk),
JS_CFUNC_DEF("decrypt_pk", 2, js_crypto_decrypt_pk),
JS_CFUNC_DEF("encrypt", 2, js_crypto_encrypt),
JS_CFUNC_DEF("decrypt", 2, js_crypto_decrypt),
};
JSValue js_crypto_use(JSContext *js)