Skip to content

XSalsa20: make hsalsa20 a public API function - #728

Open
karel-m wants to merge 5 commits into
developfrom
pr/xsalsa20-hsalsa20
Open

XSalsa20: make hsalsa20 a public API function#728
karel-m wants to merge 5 commits into
developfrom
pr/xsalsa20-hsalsa20

Conversation

@karel-m

Copy link
Copy Markdown
Member

While checking the interoperability of libtomcrypt with NaCl/libsodium seal box, crypto box and secret box, I ended up with this patch.

HSalsa20 was a missing piece needed to implement NaCl's crypto_box (= authenticated public-key encryption using X25519 + HSalsa20 + XSalsa20-Poly1305).

Technically HSalsa20 was already implemented inside xsalsa20_setup(..) - this PR simply exposes it as xsalsa20_hsalsa20(..) as part of the public API.

NaCl sealed box (crypto_box_seal) needed unkeyed BLAKE2b. However, blake2bmac_init() rejected (NULL, 0) even though blake2b_init() supports it. This PR allows NULL key when keylen is 0 (in line with the BLAKE2 specification).

Checklist

  • documentation is added or updated
  • tests are added or updated

@karel-m

Copy link
Copy Markdown
MemberAuthor

If anybody is interested in how interoperability between libtomcrypt and libsodium works, here it is:

/* Interoperability test between libtomcrypt and libsodium for: 1. secretbox (XSalsa20-Poly1305) 2. cryptobox (X25519 + HSalsa20 + XSalsa20-Poly1305) 3. sealbox (ephemeral X25519 + BLAKE2b nonce + cryptobox)*/#include<stdio.h>#include<string.h>#include<tomcrypt.h>#include<sodium.h>#defineCHECK(e) do { if ((e) != 0) { fprintf(stderr, "FAIL line %d\n", __LINE__); return 1; } } while(0)
staticintltc_secretbox_create(unsigned char*out, constunsigned char*msg, unsigned longmsglen, constunsigned charnonce[24], constunsigned charkey[32])
{
salsa20_statest;
poly1305_statepoly;
unsigned charpolykey[32];
unsigned longtaglen=16;
interr;
if ((err=xsalsa20_setup(&st, key, 32, nonce, 24, 20)) !=CRYPT_OK) returnerr;
if ((err=salsa20_keystream(&st, polykey, 32)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
if ((err=salsa20_crypt(&st, msg, msglen, out+16)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
salsa20_done(&st);
if ((err=poly1305_init(&poly, polykey, 32)) !=CRYPT_OK) returnerr;
if ((err=poly1305_process(&poly, out+16, msglen)) !=CRYPT_OK) returnerr;
if ((err=poly1305_done(&poly, out, &taglen)) !=CRYPT_OK) returnerr;
returnCRYPT_OK;
}
staticintltc_secretbox_open(unsigned char*msg, constunsigned char*box, unsigned longboxlen, constunsigned charnonce[24], constunsigned charkey[32])
{
salsa20_statest;
poly1305_statepoly;
unsigned charpolykey[32], tag[16];
unsigned longtaglen=16, msglen;
interr;
if (boxlen<16) returnCRYPT_ERROR;
msglen=boxlen-16;
if ((err=xsalsa20_setup(&st, key, 32, nonce, 24, 20)) !=CRYPT_OK) returnerr;
if ((err=salsa20_keystream(&st, polykey, 32)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
if ((err=poly1305_init(&poly, polykey, 32)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
if ((err=poly1305_process(&poly, box+16, msglen)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
if ((err=poly1305_done(&poly, tag, &taglen)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
if (memcmp(tag, box, 16) !=0) { salsa20_done(&st); returnCRYPT_ERROR; }
if ((err=salsa20_crypt(&st, box+16, msglen, msg)) !=CRYPT_OK) { salsa20_done(&st); returnerr; }
salsa20_done(&st);
returnCRYPT_OK;
}
staticintltc_cryptobox_create(unsigned char*out, constunsigned char*msg, unsigned longmsglen, constunsigned charnonce[24], constunsigned charrecipient_pk[32], constunsigned charsender_sk[32])
{
curve25519_keysk, pk;
unsigned charshared[32], symkey[32];
staticconstunsigned charzero16[16] = {0};
unsigned longshared_len=32;
interr;
if ((err=x25519_import_raw(sender_sk, 32, PK_PRIVATE, &sk)) !=CRYPT_OK) returnerr;
if ((err=x25519_import_raw(recipient_pk, 32, PK_PUBLIC, &pk)) !=CRYPT_OK) returnerr;
if ((err=x25519_shared_secret(&sk, &pk, shared, &shared_len)) !=CRYPT_OK) returnerr;
if ((err=xsalsa20_hsalsa20(symkey, 32, shared, 32, zero16, 16, 20)) !=CRYPT_OK) returnerr;
returnltc_secretbox_create(out, msg, msglen, nonce, symkey);
}
staticintltc_cryptobox_open(unsigned char*msg, constunsigned char*box, unsigned longboxlen, constunsigned charnonce[24], constunsigned charsender_pk[32], constunsigned charrecipient_sk[32])
{
curve25519_keysk, pk;
unsigned charshared[32], symkey[32];
staticconstunsigned charzero16[16] = {0};
unsigned longshared_len=32;
interr;
if ((err=x25519_import_raw(recipient_sk, 32, PK_PRIVATE, &sk)) !=CRYPT_OK) returnerr;
if ((err=x25519_import_raw(sender_pk, 32, PK_PUBLIC, &pk)) !=CRYPT_OK) returnerr;
if ((err=x25519_shared_secret(&sk, &pk, shared, &shared_len)) !=CRYPT_OK) returnerr;
if ((err=xsalsa20_hsalsa20(symkey, 32, shared, 32, zero16, 16, 20)) !=CRYPT_OK) returnerr;
returnltc_secretbox_open(msg, box, boxlen, nonce, symkey);
}
staticvoidltc_seal_nonce(unsigned charnonce[24], constunsigned chareph_pk[32], constunsigned charrecipient_pk[32])
{
blake2bmac_statebst;
unsigned longnoncelen=24;
blake2bmac_init(&bst, 24, NULL, 0);
blake2bmac_process(&bst, eph_pk, 32);
blake2bmac_process(&bst, recipient_pk, 32);
blake2bmac_done(&bst, nonce, &noncelen);
}
staticintltc_sealbox_create(unsigned char*out, constunsigned char*msg, unsigned longmsglen, constunsigned charrecipient_pk[32])
{
curve25519_keyeph;
prng_stateprng;
unsigned charnonce[24];
intprng_idx, err;
prng_idx=find_prng("sprng");
if (prng_idx==-1) returnCRYPT_ERROR;
if ((err=x25519_make_key(&prng, prng_idx, &eph)) !=CRYPT_OK) returnerr;
memcpy(out, eph.pub, 32);
ltc_seal_nonce(nonce, eph.pub, recipient_pk);
returnltc_cryptobox_create(out+32, msg, msglen, nonce, recipient_pk, eph.priv);
}
staticintltc_sealbox_open(unsigned char*msg, constunsigned char*box, unsigned longboxlen, constunsigned charrecipient_pk[32], constunsigned charrecipient_sk[32])
{
unsigned charnonce[24];
if (boxlen<48) returnCRYPT_ERROR;
ltc_seal_nonce(nonce, box, recipient_pk);
returnltc_cryptobox_open(msg, box+32, boxlen-32, nonce, box, recipient_sk);
}
staticinttest_secretbox(void)
{
constunsigned charkey[32] = {
0x1b,0x27,0x55,0x64,0x73,0xe9,0x85,0xd4,0x62,0xcd,0x51,0x19,0x7a,0x9a,0x46,0xc7,
0x60,0x09,0x54,0x9e,0xac,0x64,0x74,0xf2,0x06,0xc4,0xee,0x08,0x44,0xf6,0x83,0x89
};
constunsigned charnonce[24] = {
0x69,0x69,0x6e,0xe9,0x55,0xb6,0x2b,0x73,0xcd,0x62,0xbd,0xa8,0x75,0xfc,0x73,0xd6,
0x82,0x19,0xe0,0x03,0x6b,0x7a,0x0b,0x37
};
constchar*msg="Hello from the interop test!";
unsigned longmsglen=strlen(msg);
unsigned charsbox[256], lbox[256], dec[256];
printf(" sodium create -> ltc open ... ");
CHECK(crypto_secretbox_easy(sbox, (constunsigned char*)msg, msglen, nonce, key));
CHECK(ltc_secretbox_open(dec, sbox, msglen+16, nonce, key));
CHECK(memcmp(dec, msg, msglen));
printf("OK\n");
printf(" ltc create -> sodium open ... ");
CHECK(ltc_secretbox_create(lbox, (constunsigned char*)msg, msglen, nonce, key));
CHECK(crypto_secretbox_open_easy(dec, lbox, msglen+16, nonce, key));
CHECK(memcmp(dec, msg, msglen));
printf("OK\n");
printf(" identical ciphertext ... ");
CHECK(memcmp(sbox, lbox, msglen+16));
printf("OK\n");
return0;
}
staticinttest_cryptobox(void)
{
unsigned charalice_pk[32], alice_sk[32];
unsigned charbob_pk[32], bob_sk[32];
constunsigned charnonce[24] = {
0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,
0x0d,0x0e,0x0f,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18
};
constchar*msg="Testing crypto_box interop between libtomcrypt and libsodium.";
unsigned longmsglen=strlen(msg);
unsigned charsbox[256], lbox[256], dec[256];
crypto_box_keypair(alice_pk, alice_sk);
crypto_box_keypair(bob_pk, bob_sk);
printf(" sodium create -> ltc open ... ");
CHECK(crypto_box_easy(sbox, (constunsigned char*)msg, msglen, nonce, bob_pk, alice_sk));
CHECK(ltc_cryptobox_open(dec, sbox, msglen+16, nonce, alice_pk, bob_sk));
CHECK(memcmp(dec, msg, msglen));
printf("OK\n");
printf(" ltc create -> sodium open ... ");
CHECK(ltc_cryptobox_create(lbox, (constunsigned char*)msg, msglen, nonce, bob_pk, alice_sk));
CHECK(crypto_box_open_easy(dec, lbox, msglen+16, nonce, alice_pk, bob_sk));
CHECK(memcmp(dec, msg, msglen));
printf("OK\n");
printf(" identical ciphertext ... ");
CHECK(memcmp(sbox, lbox, msglen+16));
printf("OK\n");
return0;
}
staticinttest_sealbox(void)
{
unsigned charpk[32], sk[32];
constchar*msg="Sealed box: anonymous public-key authenticated encryption!";
unsigned longmsglen=strlen(msg);
unsigned longsealedlen=msglen+48; /* 32 eph_pk + 16 tag */unsigned charsealed[256], dec[256];
crypto_box_keypair(pk, sk);
printf(" sodium create -> ltc open ... ");
CHECK(crypto_box_seal(sealed, (constunsigned char*)msg, msglen, pk));
CHECK(ltc_sealbox_open(dec, sealed, sealedlen, pk, sk));
CHECK(memcmp(dec, msg, msglen));
printf("OK\n");
printf(" ltc create -> sodium open ... ");
CHECK(ltc_sealbox_create(sealed, (constunsigned char*)msg, msglen, pk));
CHECK(crypto_box_seal_open(dec, sealed, sealedlen, pk, sk));
CHECK(memcmp(dec, msg, msglen));
printf("OK\n");
return0;
}
intmain(void)
{
if (sodium_init() <0) {
fprintf(stderr, "sodium_init() failed\n");
return1;
}
if (register_prng(&sprng_desc) ==-1) {
fprintf(stderr, "register_prng(sprng) failed\n");
return1;
}
printf("Test 1: secretbox (XSalsa20-Poly1305)\n");
if (test_secretbox() !=0) return1;
printf("Test 2: cryptobox (X25519 + HSalsa20 + secretbox)\n");
if (test_cryptobox() !=0) return1;
printf("Test 3: sealbox (ephemeral X25519 + BLAKE2b + cryptobox)\n");
if (test_sealbox() !=0) return1;
printf("All tests passed.\n");
return0;
}

@sjaeckel

Copy link
Copy Markdown
Member

How about adding the libsodium API under an ltc_ prefix as suggested in your comment? I somehow like the idea.

@karel-m

karel-m commented Apr 28, 2026

Copy link
Copy Markdown
MemberAuthor

do you mean having something like this in our public API?

intltc_secretbox_create(constunsigned char*msg, unsigned longmsglen,
constunsigned char*nonce, unsigned longnoncelen,
constunsigned char*key, unsigned longkeylen,
unsigned char*out, unsigned long*outlen);
intltc_secretbox_open(constunsigned char*enc, unsigned longenclen,
constunsigned char*nonce, unsigned longnoncelen,
constunsigned char*key, unsigned longkeylen,
unsigned char*out, unsigned long*outlen);
intltc_cryptobox_create(constunsigned char*msg, unsigned longmsglen,
constunsigned char*nonce, unsigned longnoncelen,
constunsigned char*pk, unsigned longpklen,
constunsigned char*sk, unsigned longsklen,
unsigned char*out, unsigned long*outlen);
intltc_cryptobox_open(constunsigned char*enc, unsigned longenclen,
constunsigned char*nonce, unsigned longnoncelen,
constunsigned char*pk, unsigned longpklen,
constunsigned char*sk, unsigned longsklen,
unsigned char*out, unsigned long*outlen);
intltc_sealedbox_create(constunsigned char*msg, unsigned longmsglen,
constunsigned char*pk, unsigned longpklen,
unsigned char*out, unsigned long*outlen);
intltc_sealedbox_open(constunsigned char*enc, unsigned longenclen,
constunsigned char*pk, unsigned longpklen,
constunsigned char*sk, unsigned longsklen,
unsigned char*out, unsigned long*outlen);

FYI here is how original libsodium api looks like:

@sjaeckel

Copy link
Copy Markdown
Member

do you mean having something like this in our public API?

I like that API proposal.

Not sure though if we should add it as an official API or as an inofficial one in demos, like the gcm-file one ... if we add it, we should also have it tested. (Which I will add now for the gcm-file one.)

@karel-m

Copy link
Copy Markdown
MemberAuthor

for now, maybe demos/libsodium-boxes.c?

@sjaeckel

sjaeckel commented Apr 29, 2026

Copy link
Copy Markdown
Member

I'd prefer to directly separate the demo from the boxes and put the boxes to a separate folder ...
Maybe demos/sodium/boxes.c and demos/libsodium.c?

@karel-m
karel-mforce-pushed the pr/xsalsa20-hsalsa20 branch from 93fa641 to 0f65af2CompareMay 5, 2026 11:17
@karel-m
karel-mforce-pushed the pr/xsalsa20-hsalsa20 branch from 0f65af2 to 711a67dCompareMay 20, 2026 10:49
@karel-m
karel-mforce-pushed the pr/xsalsa20-hsalsa20 branch from 711a67d to 466a2c5CompareAugust 2, 2026 07:33

@sjaeckelsjaeckel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically LGTM

If we put the boxes into the library we should also have them in the docs. (That's also why I suggested to have the boxes in the demos instead of the library, but I'm fine with both.)

We should also run the demo in CI (or something like that ... maybe turn it into a testcase or so...) to ensure that this doesn't break, especially if the boxes API is provided as an official API.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@karel-m@sjaeckel