The NaCl Box method provides many of the encrypted methods that we might need to encrypt data between Bob and Alice. This includes where Bob and Alice have public keys, and where they might share a secret key. In this case, we will implement with Box, Sealed Box and Secret Box with Zig.

Let's say that Bob wants to send a present to Alice, and wants to seal the present in a box. If Alice sends Bob her padlock, he could close the box with her padlock. He could also sign a card and put it into another box, for Alice to open it. She will then open the present with the key to the padlock, nd check that it was Bob who sent it. This is the Box method.

But, let's say, that Bob and Alice have a key that they use to lock a box. With this, Bob could lock the box with this key, and Alice would open it with the same key. This is the Secret Box method.

Finally, it might be Valentine's Day, so Bob wants to send the box anonymously, and so will lock it with Alice's padlock, but not put his signature on the box. This is the Sealed Box method.

For Secret Box, we seal the box with a secret key, and open it with a secret key:

nacl.SecretBox.seal(ciphertext, message, salt, secret_key);
nacl.SecretBox.open(plaintext, ciphertext, salt, secret_key);

With Box, we encrypted with Alice's public key and signed with Bob's private key. We then open the box with Alice's private key, and check that Bob sent the box with his public key:


nacl.Box.seal(ciphertext, message, salt, AliceKeyPair.public_key, BobKeyPair.secret_key);
nacl.Box.open(plaintext, ciphertext, salt, BobKeyPair.public_key, AliceKeyPair.secret_key);

With a Sealed Box, Bob sends an anonymous box, and where we use Alice's public key to encrypt, and decrypt with Alice's private key:

nacl.SealedBox.seal(ciphertext, message, AliceKeyPair.public_key);
nacl.SealedBox.open(plaintext, ciphertext, AliceKeyPair);

NaCl Box

The Box encryption method is just beautiful and uses the power of public key cryptography and symmetric key. With this, Bob sends Alice an encrypted message using her public key, and then she uses her private key to decrypt it. Bob can then sign the message with his private key, and Alice proves that Bob sent it using his public key. Basically, it uses hybrid encryption, and where a symmetric secret key encrypts the message. The ciphertext will have an additional of a 16-byte tag for the signature. It uses X25519 for the key exchange method in passing the encrypted key, Ed25519 for digital signatures, and XSala20/Poly1305 for the symmetric key encryption (and using a 192-bit nonce):

None

The following code was compiled with Zig Version 0.15.1 [here]. With ZIg, we can use Box encryption [here]:

const std = @import("std");
const nacl = @import("std").crypto.nacl;
const crypto = @import("std").crypto;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;
    // Get the command-line arguments
    var message: []const u8 = undefined;
    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);
    // Check if there are any arguments
    if (args.len > 1) {
        message = args[1];
    }
    const BobKeyPair = nacl.Box.KeyPair.generate();
    const AliceKeyPair = nacl.Box.KeyPair.generate();

    var salt: [24]u8 = undefined;
    crypto.random.bytes(&salt);

    const ciphertext_size = message.len + @as(usize, nacl.Box.tag_length);
    const ciphertext = try allocator.alloc(u8, ciphertext_size);
    defer allocator.free(ciphertext);

    const plaintext = try allocator.alloc(u8, message.len);
    defer allocator.free(plaintext);

    // Encrypt with Alice's public key and sign with Bob's private key
    try nacl.Box.seal(ciphertext, message, salt, AliceKeyPair.public_key, BobKeyPair.secret_key);

    // Decrypt with Alice's private key and verify with Bob's public key
    try nacl.Box.open(plaintext, ciphertext, salt, BobKeyPair.public_key, AliceKeyPair.secret_key);

    try stdout.print("== Box encryption Message: {s} \n", .{message});
    try stdout.print("== Box encryption Salt: {x} \n", .{salt});
    try stdout.print("\nBob Secret Key: {x} \n", .{BobKeyPair.secret_key});
    try stdout.print("Bob Public Key: {x}\n", .{BobKeyPair.public_key});
    try stdout.print("\nAlice Secret Key: {x} \n", .{AliceKeyPair.secret_key});
    try stdout.print("Alice Public Key: {x}\n", .{AliceKeyPair.public_key});
    try stdout.print("\nCiphertext passed to Alice (using Alice's public key): {x}\n", .{ciphertext});
    try stdout.print("\nPlaintext recovered by Alice (with her private key): {s}\n", .{plaintext});
    try stdout.flush();
}

A sample run with the message of "Hello Alice. I'm Bob!" [here]:

== Box encryption Message: Hello Alice. I'm Bob! 
== Box encryption Salt: 63353c9f0c9263bae43d9cf545502c9bc7f7cc8a9d4a52ec 

Bob Secret Key: bd6ef81bd4244bca098918f6d042f95029b0572dd0d2f3f8232bf340df43d974 
Bob Public Key: ccc18db42a1f37479e9d53e38acd49d8bd24aed3e22cc60a574752ac8d91e279

Alice Secret Key: 350e678b75efb32dc808292d3483517ed47f42e04a974581cc7a2697a0b88691 
Alice Public Key: cdd826741d877e5bc750855f61eb824876141b6ce7305cbbae0d7721d11ccd01

Ciphertext passed to Alice (using Alice's public key): b353ff2b846d573f424e8ae4c10dc64815db4051a48305f773a879c047af2695384cbbfbb3

Plaintext recovered by Alice (with her private key): Hello Alice. I'm Bob!

We can see that the plaintext has 21 bytes, and that the ciphertext has 37 bytes. This means that the tag for the signature is 16 bytes (128 bits).

NaCl Secret Box

With the Secret Box, Bob and Alice share a secret key, and which is used to encrypt the data, and then decrypt it. This uses XSala20/Poly1305 for the symmetric key encryption (and using a 192-bit nonce):

None

The following code was compiled with Zig Version 0.15.1 [here]. With ZIg, we can use Box Sealed encryption [here]:

const std = @import("std");
const nacl = @import("std").crypto.nacl;
const crypto = @import("std").crypto;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;
    // Get the command-line arguments
    var message: []const u8 = undefined;
    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);
    // Check if there are any arguments
    if (args.len > 1) {
        message = args[1];
    }
    const AliceKeyPair = nacl.SealedBox.KeyPair.generate();

    var salt: [24]u8 = undefined;
    crypto.random.bytes(&salt);

    const ciphertext_size = message.len + @as(usize, nacl.SealedBox.seal_length);
    const ciphertext = try allocator.alloc(u8, ciphertext_size);
    defer allocator.free(ciphertext);

    const plaintext = try allocator.alloc(u8, message.len);
    defer allocator.free(plaintext);

    try nacl.SealedBox.seal(ciphertext, message, AliceKeyPair.public_key);
    try nacl.SealedBox.open(plaintext, ciphertext, AliceKeyPair);

    try stdout.print("== Box encryption Message: {s} \n", .{message});
    try stdout.print("== Box encryption Salt: {x} \n", .{salt});
    try stdout.print("\nAlice Secret Key: {x} \n", .{AliceKeyPair.secret_key});
    try stdout.print("Alice Public Key: {x}\n", .{AliceKeyPair.public_key});
    try stdout.print("\nCiphertext passed to Alice (using Alice's public key): {x}\n", .{ciphertext});
    try stdout.print("\nPlaintext recovered by Alice (with her private key): {s}\n", .{plaintext});
    try stdout.flush();
}

A sample run with the message of "Hello Alice. I'm Bob!" [here]:

== Box encryption Message: Hello Alice. I'm Bob! 
== Box encryption Salt: 8b4c170d4a31a3afe6eedc1dc2f57091fb50750d81503f64 

Alice Secret Key: 65023dae30945d748b9806545790e9238bbea81350d914b5fe41f6efa5f805ae 
Alice Public Key: e4aadb85b13d358a90ea47d720bb051b14b4196dca23ad0fac7bbe94329a7a53

Ciphertext passed to Alice (using Alice's public key): 6896321116806e46702bcbb5174b714198916ad577db1afbfddc2cac4c3c5149dd3582409dae6abde150d13e73b9e87e36301753280ee56e55cc4371e1414b6c7a2d72c18a

Plaintext recovered by Alice (with her private key): Hello Alice. I'm Bob!

We can see that the plaintext has 21 bytes, and that the ciphertext has 69 bytes. This means that the tag is 48 bytes.

NaCl Sealed Box

With a Sealed Box, Bob does not identify himself, and thus creates an anonymous encrypted message. This uses X25519 to exchange the symmetric key, and uses Alice's public key. It will use symmetric key encryption with XSala20/Poly1305 to encrypt the data:

None

The following code was compiled with Zig Version 0.15.1 [here]. With Zig, we can use Secret Box encryption:

const std = @import("std");
const nacl = @import("std").crypto.nacl;
const crypto = @import("std").crypto;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;
    // Get the command-line arguments
    var message: []const u8 = undefined;
    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);
    // Check if there are any arguments
    if (args.len > 1) {
        message = args[1];
    }
    var salt: [24]u8 = undefined;
    crypto.random.bytes(&salt);

    var secret_key: [32]u8 = undefined;
    crypto.random.bytes(&secret_key);

    const ciphertext_size = message.len + @as(usize, nacl.Box.tag_length);
    const ciphertext = try allocator.alloc(u8, ciphertext_size);
    defer allocator.free(ciphertext);

    const plaintext = try allocator.alloc(u8, message.len);
    defer allocator.free(plaintext);

    nacl.SecretBox.seal(ciphertext, message, salt, secret_key);

    try nacl.SecretBox.open(plaintext, ciphertext, salt, secret_key);

    try stdout.print("== Box encryption Message: {s} \n", .{message});
    try stdout.print("== Box encryption Salt: {x} \n", .{salt});
    try stdout.print("\nSecret key: {x} \n", .{secret_key});
    try stdout.print("\nCiphertext passed to Alice (using Alice's public key): {x}\n", .{ciphertext});
    try stdout.print("\nPlaintext recovered by Alice (with her private key): {s}\n", .{plaintext});
    try stdout.flush();
}

A sample run with the message of "Hello Alice. I'm Bob!":

== Box encryption Message: Hello Alice. I'm Bob! 
== Box encryption Salt: f0d254c022e86f332b1e04f70185d426b4beb4ebf5942675 

Secret key: b064b322e96fcb8767b19fdfaab78ae8d8ee76edee32fdb1e412f1e186d20dc8 

Ciphertext passed to Alice (using Alice's public key): 4d039df330ce38c3c788ecbaf43b3dae45b97d40a2568ae54886d61f8e3b5cd9ebc9c40b40

Plaintext recovered by Alice (with her private key): Hello Alice. I'm Bob!

We can see that the plaintext has 21 bytes, and that the ciphertext has 37 bytes. This means that the tag for the signature is 16 bytes (128 bits).

Conclusions

Go do some Zig: https://asecuritysite.com/zig