Skip to main content

dryoc/classic/
crypto_core.rs

1use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
2use subtle::ConstantTimeEq;
3
4use crate::constants::{
5    CRYPTO_CORE_ED25519_BYTES, CRYPTO_CORE_HCHACHA20_INPUTBYTES, CRYPTO_CORE_HCHACHA20_KEYBYTES,
6    CRYPTO_CORE_HCHACHA20_OUTPUTBYTES, CRYPTO_CORE_HSALSA20_INPUTBYTES,
7    CRYPTO_CORE_HSALSA20_KEYBYTES, CRYPTO_CORE_HSALSA20_OUTPUTBYTES, CRYPTO_SCALARMULT_BYTES,
8    CRYPTO_SCALARMULT_SCALARBYTES,
9};
10use crate::error::Error;
11use crate::scalarmult_curve25519::{
12    crypto_scalarmult_curve25519, crypto_scalarmult_curve25519_base,
13};
14use crate::types::*;
15use crate::utils::load_u32_le;
16
17/// Stack-allocated HChaCha20 input.
18pub type HChaCha20Input = [u8; CRYPTO_CORE_HCHACHA20_INPUTBYTES];
19/// Stack-allocated HChaCha20 key.
20pub type HChaCha20Key = [u8; CRYPTO_CORE_HCHACHA20_KEYBYTES];
21/// Stack-allocated HChaCha20 output.
22pub type HChaCha20Output = [u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
23/// Stack-allocated HSalsa20 input.
24pub type HSalsa20Input = [u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
25/// Stack-allocated HSalsa20 key.
26pub type HSalsa20Key = [u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
27/// Stack-allocated HSalsa20 output.
28pub type HSalsa20Output = [u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
29/// Stack-allocated Ed25519 point.
30pub type Ed25519Point = [u8; CRYPTO_CORE_ED25519_BYTES];
31
32/// Computes the public key for a previously generated secret key.
33///
34/// Compatible with libsodium's `crypto_scalarmult_base`.
35pub fn crypto_scalarmult_base(
36    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
37    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
38) {
39    crypto_scalarmult_curve25519_base(q, n)
40}
41
42/// Computes a shared secret `q`, given `n`, our secret key, and `p`, their
43/// public key, using a Diffie-Hellman key exchange.
44///
45/// Compatible with libsodium's `crypto_scalarmult`.
46///
47/// # Errors
48///
49/// Returns an error if `p` is an unacceptable low-order public key that
50/// produces an all-zero shared secret.
51pub fn crypto_scalarmult(
52    q: &mut [u8; CRYPTO_SCALARMULT_BYTES],
53    n: &[u8; CRYPTO_SCALARMULT_SCALARBYTES],
54    p: &[u8; CRYPTO_SCALARMULT_BYTES],
55) -> Result<(), Error> {
56    crypto_scalarmult_curve25519(q, n, p);
57
58    if q.ct_eq(&[0u8; CRYPTO_SCALARMULT_BYTES]).into() {
59        Err(Error::invalid_key(crate::ErrorContext::Curve25519PublicKey))
60    } else {
61        Ok(())
62    }
63}
64
65#[inline]
66fn chacha20_round(x: &mut u32, y: &u32, z: &mut u32, rot: u32) {
67    *x = x.wrapping_add(*y);
68    *z = (*z ^ *x).rotate_left(rot);
69}
70
71#[inline]
72fn chacha20_quarterround(a: &mut u32, b: &mut u32, c: &mut u32, d: &mut u32) {
73    chacha20_round(a, b, d, 16);
74    chacha20_round(c, d, b, 12);
75    chacha20_round(a, b, d, 8);
76    chacha20_round(c, d, b, 7);
77}
78
79/// Implements the HChaCha20 function.
80///
81/// Compatible with libsodium's `crypto_core_hchacha20`.
82pub fn crypto_core_hchacha20(
83    output: &mut HChaCha20Output,
84    input: &HChaCha20Input,
85    key: &HChaCha20Key,
86    constants: Option<(u32, u32, u32, u32)>,
87) {
88    let input = input.as_array();
89    let key = key.as_array();
90    let (mut x0, mut x1, mut x2, mut x3) =
91        constants.unwrap_or((0x61707865, 0x3320646e, 0x79622d32, 0x6b206574));
92    let (
93        mut x4,
94        mut x5,
95        mut x6,
96        mut x7,
97        mut x8,
98        mut x9,
99        mut x10,
100        mut x11,
101        mut x12,
102        mut x13,
103        mut x14,
104        mut x15,
105    ) = (
106        load_u32_le(&key[0..4]),
107        load_u32_le(&key[4..8]),
108        load_u32_le(&key[8..12]),
109        load_u32_le(&key[12..16]),
110        load_u32_le(&key[16..20]),
111        load_u32_le(&key[20..24]),
112        load_u32_le(&key[24..28]),
113        load_u32_le(&key[28..32]),
114        load_u32_le(&input[0..4]),
115        load_u32_le(&input[4..8]),
116        load_u32_le(&input[8..12]),
117        load_u32_le(&input[12..16]),
118    );
119
120    for _ in 0..10 {
121        chacha20_quarterround(&mut x0, &mut x4, &mut x8, &mut x12);
122        chacha20_quarterround(&mut x1, &mut x5, &mut x9, &mut x13);
123        chacha20_quarterround(&mut x2, &mut x6, &mut x10, &mut x14);
124        chacha20_quarterround(&mut x3, &mut x7, &mut x11, &mut x15);
125        chacha20_quarterround(&mut x0, &mut x5, &mut x10, &mut x15);
126        chacha20_quarterround(&mut x1, &mut x6, &mut x11, &mut x12);
127        chacha20_quarterround(&mut x2, &mut x7, &mut x8, &mut x13);
128        chacha20_quarterround(&mut x3, &mut x4, &mut x9, &mut x14);
129    }
130
131    output[0..4].copy_from_slice(&x0.to_le_bytes());
132    output[4..8].copy_from_slice(&x1.to_le_bytes());
133    output[8..12].copy_from_slice(&x2.to_le_bytes());
134    output[12..16].copy_from_slice(&x3.to_le_bytes());
135    output[16..20].copy_from_slice(&x12.to_le_bytes());
136    output[20..24].copy_from_slice(&x13.to_le_bytes());
137    output[24..28].copy_from_slice(&x14.to_le_bytes());
138    output[28..32].copy_from_slice(&x15.to_le_bytes());
139}
140
141/// Checks whether `p` is a valid prime-order Ed25519 point.
142///
143/// This validates the canonical compressed encoding, rejects points that are
144/// not on the curve or have small order, and requires membership in the main
145/// subgroup. The high bit is the sign of the x-coordinate and may legitimately
146/// be set.
147///
148/// # Example
149///
150/// ```
151/// use dryoc::classic::crypto_core::crypto_core_ed25519_is_valid_point;
152/// use dryoc::classic::crypto_sign::crypto_sign_keypair;
153///
154/// let (pk, _) = crypto_sign_keypair();
155/// assert!(crypto_core_ed25519_is_valid_point(&pk));
156/// ```
157///
158/// # Compatibility
159///
160/// This matches `crypto_core_ed25519_is_valid_point` in libsodium 1.0.21 and
161/// later. Libsodium versions through 1.0.20 incorrectly accepted some
162/// mixed-order points; this function rejects them.
163pub fn crypto_core_ed25519_is_valid_point(p: &Ed25519Point) -> bool {
164    let Some(point) = decompress_canonical_ed25519_point(p) else {
165        return false;
166    };
167
168    !point.is_small_order() && point.is_torsion_free()
169}
170
171/// Decompresses an Ed25519 point only if its encoding is canonical.
172///
173/// `curve25519-dalek` intentionally reduces the encoded y-coordinate modulo the
174/// field prime while decompressing. Recompressing and comparing prevents
175/// alternate encodings of the same point from being accepted.
176pub(crate) fn decompress_canonical_ed25519_point(p: &Ed25519Point) -> Option<EdwardsPoint> {
177    let compressed = CompressedEdwardsY(*p);
178    let point = compressed.decompress()?;
179
180    if point.compress() == compressed {
181        Some(point)
182    } else {
183        None
184    }
185}
186
187#[inline]
188fn salsa20_rotl32(x: u32, y: u32, rot: u32) -> u32 {
189    x.wrapping_add(y).rotate_left(rot)
190}
191
192/// Implements the HSalsa20 function.
193///
194/// Compatible with libsodium's `crypto_core_hsalsa20`.
195pub fn crypto_core_hsalsa20(
196    output: &mut HSalsa20Output,
197    input: &HSalsa20Input,
198    key: &HSalsa20Key,
199    constants: Option<(u32, u32, u32, u32)>,
200) {
201    let (mut x0, mut x5, mut x10, mut x15) =
202        constants.unwrap_or((0x61707865, 0x3320646e, 0x79622d32, 0x6b206574));
203    let (
204        mut x1,
205        mut x2,
206        mut x3,
207        mut x4,
208        mut x11,
209        mut x12,
210        mut x13,
211        mut x14,
212        mut x6,
213        mut x7,
214        mut x8,
215        mut x9,
216    ) = (
217        load_u32_le(&key[0..4]),
218        load_u32_le(&key[4..8]),
219        load_u32_le(&key[8..12]),
220        load_u32_le(&key[12..16]),
221        load_u32_le(&key[16..20]),
222        load_u32_le(&key[20..24]),
223        load_u32_le(&key[24..28]),
224        load_u32_le(&key[28..32]),
225        load_u32_le(&input[0..4]),
226        load_u32_le(&input[4..8]),
227        load_u32_le(&input[8..12]),
228        load_u32_le(&input[12..16]),
229    );
230
231    for _ in (0..20).step_by(2) {
232        x4 ^= salsa20_rotl32(x0, x12, 7);
233        x8 ^= salsa20_rotl32(x4, x0, 9);
234        x12 ^= salsa20_rotl32(x8, x4, 13);
235        x0 ^= salsa20_rotl32(x12, x8, 18);
236        x9 ^= salsa20_rotl32(x5, x1, 7);
237        x13 ^= salsa20_rotl32(x9, x5, 9);
238        x1 ^= salsa20_rotl32(x13, x9, 13);
239        x5 ^= salsa20_rotl32(x1, x13, 18);
240        x14 ^= salsa20_rotl32(x10, x6, 7);
241        x2 ^= salsa20_rotl32(x14, x10, 9);
242        x6 ^= salsa20_rotl32(x2, x14, 13);
243        x10 ^= salsa20_rotl32(x6, x2, 18);
244        x3 ^= salsa20_rotl32(x15, x11, 7);
245        x7 ^= salsa20_rotl32(x3, x15, 9);
246        x11 ^= salsa20_rotl32(x7, x3, 13);
247        x15 ^= salsa20_rotl32(x11, x7, 18);
248        x1 ^= salsa20_rotl32(x0, x3, 7);
249        x2 ^= salsa20_rotl32(x1, x0, 9);
250        x3 ^= salsa20_rotl32(x2, x1, 13);
251        x0 ^= salsa20_rotl32(x3, x2, 18);
252        x6 ^= salsa20_rotl32(x5, x4, 7);
253        x7 ^= salsa20_rotl32(x6, x5, 9);
254        x4 ^= salsa20_rotl32(x7, x6, 13);
255        x5 ^= salsa20_rotl32(x4, x7, 18);
256        x11 ^= salsa20_rotl32(x10, x9, 7);
257        x8 ^= salsa20_rotl32(x11, x10, 9);
258        x9 ^= salsa20_rotl32(x8, x11, 13);
259        x10 ^= salsa20_rotl32(x9, x8, 18);
260        x12 ^= salsa20_rotl32(x15, x14, 7);
261        x13 ^= salsa20_rotl32(x12, x15, 9);
262        x14 ^= salsa20_rotl32(x13, x12, 13);
263        x15 ^= salsa20_rotl32(x14, x13, 18);
264    }
265
266    output[0..4].copy_from_slice(&x0.to_le_bytes());
267    output[4..8].copy_from_slice(&x5.to_le_bytes());
268    output[8..12].copy_from_slice(&x10.to_le_bytes());
269    output[12..16].copy_from_slice(&x15.to_le_bytes());
270    output[16..20].copy_from_slice(&x6.to_le_bytes());
271    output[20..24].copy_from_slice(&x7.to_le_bytes());
272    output[24..28].copy_from_slice(&x8.to_le_bytes());
273    output[28..32].copy_from_slice(&x9.to_le_bytes());
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::classic::crypto_sign::crypto_sign_keypair;
280
281    #[test]
282    fn test_crypto_core_ed25519_is_valid_point() {
283        let basepoint = curve25519_dalek::constants::ED25519_BASEPOINT_COMPRESSED.to_bytes();
284        assert!(crypto_core_ed25519_is_valid_point(&basepoint));
285
286        let mut negative_basepoint = basepoint;
287        negative_basepoint[31] |= 0x80;
288        assert!(
289            crypto_core_ed25519_is_valid_point(&negative_basepoint),
290            "the high bit is a valid x-coordinate sign bit"
291        );
292
293        let identity = {
294            let mut point = [0u8; CRYPTO_CORE_ED25519_BYTES];
295            point[0] = 1;
296            point
297        };
298        assert!(!crypto_core_ed25519_is_valid_point(&identity));
299
300        let noncanonical_identity = {
301            let mut point = [0xff; CRYPTO_CORE_ED25519_BYTES];
302            point[0] = 0xee;
303            point[31] = 0x7f;
304            point
305        };
306        assert!(
307            decompress_canonical_ed25519_point(&noncanonical_identity).is_none(),
308            "p + 1 must not be accepted as an alternate encoding of the identity"
309        );
310        assert!(!crypto_core_ed25519_is_valid_point(&noncanonical_identity));
311
312        let torsion = curve25519_dalek::constants::EIGHT_TORSION[1];
313        assert!(torsion.is_small_order());
314        assert!(!crypto_core_ed25519_is_valid_point(
315            &torsion.compress().to_bytes()
316        ));
317
318        let mixed_order = curve25519_dalek::constants::ED25519_BASEPOINT_POINT + torsion;
319        assert!(!mixed_order.is_small_order());
320        assert!(!mixed_order.is_torsion_free());
321        assert!(!crypto_core_ed25519_is_valid_point(
322            &mixed_order.compress().to_bytes()
323        ));
324
325        let mut point_not_on_curve = [0u8; CRYPTO_CORE_ED25519_BYTES];
326        point_not_on_curve[0] = 2;
327        assert!(!crypto_core_ed25519_is_valid_point(&point_not_on_curve));
328        assert!(!crypto_core_ed25519_is_valid_point(
329            &[0u8; CRYPTO_CORE_ED25519_BYTES]
330        ));
331    }
332
333    #[test]
334    fn test_generated_ed25519_keys_are_valid_points() {
335        for _ in 0..25 {
336            let (ed25519_pk, _) = crypto_sign_keypair();
337            assert!(crypto_core_ed25519_is_valid_point(&ed25519_pk));
338        }
339    }
340
341    #[test]
342    fn test_crypto_core_ed25519_rejects_legacy_libsodium_mixed_order_points() {
343        let mut y_is_nine = [0u8; CRYPTO_CORE_ED25519_BYTES];
344        y_is_nine[0] = 9;
345
346        // This is the regression vector added when libsodium fixed its main
347        // subgroup check. It is a prime-order point plus order-two torsion.
348        let mut order_two_coset = [0x99; CRYPTO_CORE_ED25519_BYTES];
349        order_two_coset[0] = 0x95;
350
351        for point in [y_is_nine, order_two_coset] {
352            let decoded = decompress_canonical_ed25519_point(&point)
353                .expect("regression vector must be a canonical curve point");
354            assert!(!decoded.is_small_order());
355            assert!(!decoded.is_torsion_free());
356            assert!(!crypto_core_ed25519_is_valid_point(&point));
357        }
358    }
359
360    #[test]
361    fn test_crypto_scalarmult_rejects_low_order_points() {
362        let scalar = [0x42; CRYPTO_SCALARMULT_SCALARBYTES];
363        let mut one = [0u8; CRYPTO_SCALARMULT_BYTES];
364        one[0] = 1;
365
366        for public_key in [[0u8; CRYPTO_SCALARMULT_BYTES], one] {
367            let mut shared_secret = [0xa5; CRYPTO_SCALARMULT_BYTES];
368            crypto_scalarmult(&mut shared_secret, &scalar, &public_key)
369                .expect_err("low-order public key must be rejected");
370            assert_eq!(shared_secret, [0u8; CRYPTO_SCALARMULT_BYTES]);
371        }
372    }
373
374    #[test]
375    fn test_crypto_scalarmult_ignores_public_key_high_bit() {
376        let scalar = [0x42; CRYPTO_SCALARMULT_SCALARBYTES];
377        let mut canonical = [0u8; CRYPTO_SCALARMULT_BYTES];
378        canonical[0] = 9;
379        let mut high_bit_set = canonical;
380        high_bit_set[CRYPTO_SCALARMULT_BYTES - 1] = 0x80;
381        let mut canonical_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
382        let mut high_bit_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
383
384        crypto_scalarmult(&mut canonical_secret, &scalar, &canonical).unwrap();
385        crypto_scalarmult(&mut high_bit_secret, &scalar, &high_bit_set).unwrap();
386
387        assert_eq!(canonical_secret, high_bit_secret);
388    }
389
390    #[cfg(dryoc_native_tests)]
391    mod native_tests {
392        use super::*;
393        use crate::classic::crypto_box::*;
394
395        #[test]
396        fn test_crypto_core_ed25519_compatibility_for_version_stable_points() {
397            use libsodium_sys::crypto_core_ed25519_is_valid_point as sodium_is_valid_point;
398
399            // libsodium-sys 0.2.7 normally embeds libsodium 1.0.18, whose main
400            // subgroup check has a known mixed-order bug. Use it as an oracle
401            // only for cases whose behavior is stable across versions. The
402            // affected vectors are tested directly above.
403
404            let basepoint = curve25519_dalek::constants::ED25519_BASEPOINT_COMPRESSED.to_bytes();
405            let mut negative_basepoint = basepoint;
406            negative_basepoint[31] |= 0x80;
407            let identity = {
408                let mut point = [0u8; CRYPTO_CORE_ED25519_BYTES];
409                point[0] = 1;
410                point
411            };
412            let noncanonical_identity = {
413                let mut point = [0xff; CRYPTO_CORE_ED25519_BYTES];
414                point[0] = 0xee;
415                point[31] = 0x7f;
416                point
417            };
418            let torsion = curve25519_dalek::constants::EIGHT_TORSION[1];
419            let mixed_order = (curve25519_dalek::constants::ED25519_BASEPOINT_POINT + torsion)
420                .compress()
421                .to_bytes();
422
423            for point in [
424                basepoint,
425                negative_basepoint,
426                identity,
427                noncanonical_identity,
428                torsion.compress().to_bytes(),
429                mixed_order,
430                [0u8; CRYPTO_CORE_ED25519_BYTES],
431            ] {
432                let dryoc_result = crypto_core_ed25519_is_valid_point(&point);
433                let sodium_result = unsafe { sodium_is_valid_point(point.as_ptr()) } == 1;
434                assert_eq!(dryoc_result, sodium_result, "point: {point:02x?}");
435            }
436
437            for _ in 0..20 {
438                let (public_key, _) = crypto_sign_keypair();
439                let sodium_result = unsafe { sodium_is_valid_point(public_key.as_ptr()) } == 1;
440                assert!(sodium_result);
441                assert_eq!(
442                    crypto_core_ed25519_is_valid_point(&public_key),
443                    sodium_result
444                );
445            }
446        }
447
448        #[test]
449        fn test_crypto_scalarmult_base() {
450            use base64::Engine as _;
451            use base64::engine::general_purpose;
452            for _ in 0..20 {
453                use sodiumoxide::crypto::scalarmult::curve25519::{Scalar, scalarmult_base};
454
455                let (pk, sk) = crypto_box_keypair();
456
457                let mut public_key = [0u8; CRYPTO_SCALARMULT_BYTES];
458                crypto_scalarmult_base(&mut public_key, &sk);
459
460                assert_eq!(&pk, &public_key);
461
462                let ge = scalarmult_base(&Scalar::from_slice(&sk).unwrap());
463
464                assert_eq!(
465                    general_purpose::STANDARD.encode(ge.as_ref()),
466                    general_purpose::STANDARD.encode(public_key)
467                );
468            }
469        }
470
471        #[test]
472        fn test_crypto_scalarmult() {
473            use base64::Engine as _;
474            use base64::engine::general_purpose;
475            for _ in 0..20 {
476                use sodiumoxide::crypto::scalarmult::curve25519::{
477                    GroupElement, Scalar, scalarmult,
478                };
479
480                let (_our_pk, our_sk) = crypto_box_keypair();
481                let (their_pk, _their_sk) = crypto_box_keypair();
482
483                let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
484                crypto_scalarmult(&mut shared_secret, &our_sk, &their_pk)
485                    .expect("scalarmult failed");
486
487                let ge = scalarmult(
488                    &Scalar::from_slice(&our_sk).unwrap(),
489                    &GroupElement::from_slice(&their_pk).unwrap(),
490                )
491                .expect("scalarmult failed");
492
493                assert_eq!(
494                    general_purpose::STANDARD.encode(ge.as_ref()),
495                    general_purpose::STANDARD.encode(shared_secret)
496                );
497            }
498        }
499
500        #[test]
501        fn test_crypto_scalarmult_low_order_compatibility() {
502            use sodiumoxide::crypto::scalarmult::curve25519::{GroupElement, Scalar, scalarmult};
503
504            let scalar = [0x42; CRYPTO_SCALARMULT_SCALARBYTES];
505            let mut one = [0u8; CRYPTO_SCALARMULT_BYTES];
506            one[0] = 1;
507
508            for public_key in [[0u8; CRYPTO_SCALARMULT_BYTES], one] {
509                let mut shared_secret = [0u8; CRYPTO_SCALARMULT_BYTES];
510                assert!(crypto_scalarmult(&mut shared_secret, &scalar, &public_key).is_err());
511                assert!(
512                    scalarmult(
513                        &Scalar::from_slice(&scalar).unwrap(),
514                        &GroupElement::from_slice(&public_key).unwrap(),
515                    )
516                    .is_err()
517                );
518            }
519        }
520
521        #[test]
522        fn test_crypto_core_hchacha20() {
523            use base64::Engine as _;
524            use base64::engine::general_purpose;
525            use libsodium_sys::crypto_core_hchacha20 as so_crypto_core_hchacha20;
526
527            use crate::rng::copy_randombytes;
528
529            for _ in 0..10 {
530                let mut key = [0u8; 32];
531                let mut data = [0u8; 16];
532                copy_randombytes(&mut key);
533                copy_randombytes(&mut data);
534
535                let mut out = [0u8; CRYPTO_CORE_HCHACHA20_OUTPUTBYTES];
536                crypto_core_hchacha20(&mut out, &data, &key, None);
537
538                let mut so_out = [0u8; 32];
539                unsafe {
540                    let ret = so_crypto_core_hchacha20(
541                        so_out.as_mut_ptr(),
542                        data.as_ptr(),
543                        key.as_ptr(),
544                        std::ptr::null(),
545                    );
546                    assert_eq!(ret, 0);
547                }
548                assert_eq!(
549                    general_purpose::STANDARD.encode(out),
550                    general_purpose::STANDARD.encode(so_out)
551                );
552            }
553        }
554
555        #[test]
556        fn test_crypto_core_hsalsa20() {
557            use base64::Engine as _;
558            use base64::engine::general_purpose;
559            use libsodium_sys::crypto_core_hsalsa20 as so_crypto_core_hsalsa20;
560
561            use crate::rng::copy_randombytes;
562
563            for _ in 0..10 {
564                let mut key = [0u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
565                let mut data = [0u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
566                copy_randombytes(&mut key);
567                copy_randombytes(&mut data);
568
569                let mut out = [0u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
570                crypto_core_hsalsa20(&mut out, &data, &key, None);
571
572                let mut so_out = [0u8; 32];
573                unsafe {
574                    let ret = so_crypto_core_hsalsa20(
575                        so_out.as_mut_ptr(),
576                        data.as_ptr(),
577                        key.as_ptr(),
578                        std::ptr::null(),
579                    );
580                    assert_eq!(ret, 0);
581                }
582                assert_eq!(
583                    general_purpose::STANDARD.encode(out),
584                    general_purpose::STANDARD.encode(so_out)
585                );
586            }
587        }
588    }
589}