@@ -236,6 +236,287 @@ public void Api()
236236 Assert . False ( kp5 . Verify ( corrupt , signature ) ) ;
237237 }
238238
239+ [ Fact ]
240+ public void DecodePubCurveKey_returns_exactly_32_bytes ( )
241+ {
242+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
243+ var decoded = KeyPair . DecodePubCurveKey ( kp . GetPublicKey ( ) ) ;
244+ Assert . Equal ( 32 , decoded . Length ) ;
245+ }
246+
247+ [ Fact ]
248+ public void DecodePubCurveKey_returns_32_bytes_for_multiple_keys ( )
249+ {
250+ // Verify across several independently generated keys
251+ for ( var i = 0 ; i < 10 ; i ++ )
252+ {
253+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
254+ var decoded = KeyPair . DecodePubCurveKey ( kp . GetPublicKey ( ) ) ;
255+ Assert . Equal ( 32 , decoded . Length ) ;
256+ }
257+ }
258+
259+ [ Fact ]
260+ public void DecodePubCurveKey_deterministic_for_known_seed ( )
261+ {
262+ var kp = KeyPair . FromSeed ( "SXAD4F52S2XAJTJ3TGDJ4VXQVW7TU35XJUSVKF25ZRXIWCIUK6NLANRHVY" . ToCharArray ( ) ) ;
263+ var decoded1 = KeyPair . DecodePubCurveKey ( kp . GetPublicKey ( ) ) ;
264+ var decoded2 = KeyPair . DecodePubCurveKey ( kp . GetPublicKey ( ) ) ;
265+ Assert . Equal ( decoded1 , decoded2 ) ;
266+ Assert . Equal ( 32 , decoded1 . Length ) ;
267+ }
268+
269+ [ Fact ]
270+ public void DecodePubCurveKey_matches_known_public_key ( )
271+ {
272+ // Known seed/public key pair from existing tests
273+ var kp = KeyPair . FromSeed ( "SXANLPW5OPP62ISXMPTH26DBYM4BGT3U6P2FEALGW3BZAVXQRWCX3KH2ZM" . ToCharArray ( ) ) ;
274+ Assert . Equal ( "XBIGHMCJSHWYFW6ZY4WWONQ3FRIS5A3OQAUMPPKYBNKRYQTVX4PEAIZA" , kp . GetPublicKey ( ) ) ;
275+ var decoded = KeyPair . DecodePubCurveKey ( kp . GetPublicKey ( ) ) ;
276+ Assert . Equal ( 32 , decoded . Length ) ;
277+ }
278+
279+ [ Fact ]
280+ public void DecodePubCurveKey_does_not_include_prefix_byte ( )
281+ {
282+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
283+ var publicKey = kp . GetPublicKey ( ) ;
284+
285+ // Decode the full base32 to get raw bytes including prefix and CRC
286+ var rawLen = Base32 . GetDataLength ( publicKey . ToCharArray ( ) ) ;
287+ var raw = new byte [ rawLen ] ;
288+ Base32 . FromBase32 ( publicKey . ToCharArray ( ) , raw ) ;
289+
290+ // First byte is the prefix
291+ var prefixByte = raw [ 0 ] ;
292+ Assert . Equal ( ( byte ) PrefixByte . Curve , prefixByte ) ;
293+
294+ // DecodePubCurveKey should return bytes [1..33), not include prefix or CRC
295+ var decoded = KeyPair . DecodePubCurveKey ( publicKey ) ;
296+ Assert . Equal ( raw . AsSpan ( ) . Slice ( 1 , 32 ) . ToArray ( ) , decoded ) ;
297+ }
298+
299+ [ Fact ]
300+ public void DecodePubCurveKey_does_not_include_crc_bytes ( )
301+ {
302+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
303+ var publicKey = kp . GetPublicKey ( ) ;
304+
305+ // Get the raw decoded bytes
306+ var rawLen = Base32 . GetDataLength ( publicKey . ToCharArray ( ) ) ;
307+ var raw = new byte [ rawLen ] ;
308+ Base32 . FromBase32 ( publicKey . ToCharArray ( ) , raw ) ;
309+
310+ // Decoded key should be exactly raw[1..33) — the 32-byte key without prefix or CRC
311+ var decoded = KeyPair . DecodePubCurveKey ( publicKey ) ;
312+ Assert . Equal ( 32 , decoded . Length ) ;
313+ Assert . Equal ( raw . AsSpan ( ) . Slice ( 1 , 32 ) . ToArray ( ) , decoded ) ;
314+ }
315+
316+ [ Fact ]
317+ public void DecodePubCurveKey_round_trips_with_seal_open ( )
318+ {
319+ var kp1 = KeyPair . FromSeed ( "SXAD4F52S2XAJTJ3TGDJ4VXQVW7TU35XJUSVKF25ZRXIWCIUK6NLANRHVY" . ToCharArray ( ) ) ;
320+ var kp2 = KeyPair . CreatePair ( PrefixByte . Curve ) ;
321+
322+ var message = new byte [ ] { 1 , 2 , 3 , 4 , 5 } ;
323+ var sealed1 = kp1 . Seal ( message , kp2 . GetPublicKey ( ) ) ;
324+ var opened = kp2 . Open ( sealed1 , kp1 . GetPublicKey ( ) ) ;
325+ Assert . Equal ( message , opened ) ;
326+ }
327+
328+ [ Fact ]
329+ public void DecodePubCurveKey_rejects_non_curve_key ( )
330+ {
331+ // A valid User public key has the same decoded length but wrong prefix
332+ var kp = KeyPair . CreatePair ( PrefixByte . User ) ;
333+ var ex = Assert . Throws < NKeysException > ( ( ) => KeyPair . DecodePubCurveKey ( kp . GetPublicKey ( ) ) ) ;
334+ Assert . Equal ( "Not a valid curve key" , ex . Message ) ;
335+ }
336+
337+ [ Fact ]
338+ public void DecodePubCurveKey_rejects_too_short ( )
339+ {
340+ var ex = Assert . Throws < NKeysException > ( ( ) => KeyPair . DecodePubCurveKey ( "XAAA" ) ) ;
341+ Assert . Equal ( "Not a valid curve key" , ex . Message ) ;
342+ }
343+
344+ [ Fact ]
345+ public void DecodePubCurveKey_rejects_too_long ( )
346+ {
347+ // Take a valid key and append extra base32 characters
348+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
349+ var tooLong = kp . GetPublicKey ( ) + "AAAAAAAAAA" ;
350+ Assert . Throws < NKeysException > ( ( ) => KeyPair . DecodePubCurveKey ( tooLong ) ) ;
351+ }
352+
353+ [ Fact ]
354+ public void DecodePubCurveKey_rejects_empty_string ( )
355+ {
356+ Assert . ThrowsAny < Exception > ( ( ) => KeyPair . DecodePubCurveKey ( string . Empty ) ) ;
357+ }
358+
359+ [ Fact ]
360+ public void DecodePubCurveKey_rejects_corrupted_crc ( )
361+ {
362+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
363+ var publicKey = kp . GetPublicKey ( ) ;
364+
365+ // Corrupt a character in the middle of the key
366+ var chars = publicKey . ToCharArray ( ) ;
367+ chars [ 10 ] = chars [ 10 ] == 'A' ? 'B' : 'A' ;
368+ var corrupted = new string ( chars ) ;
369+
370+ var ex = Assert . Throws < NKeysException > ( ( ) => KeyPair . DecodePubCurveKey ( corrupted ) ) ;
371+ Assert . Equal ( "Invalid CRC" , ex . Message ) ;
372+ }
373+
374+ [ Fact ]
375+ public void DecodePubCurveKey_rejects_invalid_base32_characters ( )
376+ {
377+ var kp = KeyPair . CreatePair ( PrefixByte . Curve ) ;
378+ var publicKey = kp . GetPublicKey ( ) ;
379+
380+ // Replace with invalid base32 chars (lowercase, digits 0/1/8/9, symbols)
381+ var chars = publicKey . ToCharArray ( ) ;
382+ chars [ 5 ] = '!' ;
383+ Assert . Throws < ArgumentException > ( ( ) => KeyPair . DecodePubCurveKey ( new string ( chars ) ) ) ;
384+ }
385+
386+ [ Fact ]
387+ public void DecodePubCurveKey_different_keys_produce_different_results ( )
388+ {
389+ var kp1 = KeyPair . CreatePair ( PrefixByte . Curve ) ;
390+ var kp2 = KeyPair . CreatePair ( PrefixByte . Curve ) ;
391+
392+ var decoded1 = KeyPair . DecodePubCurveKey ( kp1 . GetPublicKey ( ) ) ;
393+ var decoded2 = KeyPair . DecodePubCurveKey ( kp2 . GetPublicKey ( ) ) ;
394+
395+ Assert . NotEqual ( decoded1 , decoded2 ) ;
396+ }
397+
398+ [ Fact ]
399+ public void DecodePubCurveKey_cross_validates_with_go_test_data ( )
400+ {
401+ // Use the Go-generated test data to verify decode produces
402+ // keys that work correctly in seal/open operations
403+ var json = JsonNode . Parse ( File . ReadAllText ( "test_data.json" ) ) ;
404+ foreach ( var data in json ! [ "xkeys" ] ! . AsArray ( ) )
405+ {
406+ var pk1 = data ! [ "pk1" ] ! . GetValue < string > ( ) ;
407+ var pk2 = data ! [ "pk2" ] ! . GetValue < string > ( ) ;
408+
409+ var decoded1 = KeyPair . DecodePubCurveKey ( pk1 ) ;
410+ var decoded2 = KeyPair . DecodePubCurveKey ( pk2 ) ;
411+
412+ Assert . Equal ( 32 , decoded1 . Length ) ;
413+ Assert . Equal ( 32 , decoded2 . Length ) ;
414+ }
415+ }
416+
417+ [ Fact ]
418+ public void DecodePubCurveKey_blackbox_seal_open_with_decoded_key ( )
419+ {
420+ // Black box: if DecodePubCurveKey returns the correct raw key,
421+ // then encrypting for that key and decrypting must round-trip
422+ var sender = KeyPair . CreatePair ( PrefixByte . Curve ) ;
423+ var receiver = KeyPair . CreatePair ( PrefixByte . Curve ) ;
424+
425+ var payloads = new byte [ ] [ ]
426+ {
427+ Array . Empty < byte > ( ) ,
428+ new byte [ ] { 0 } ,
429+ new byte [ ] { 0xFF } ,
430+ Encoding . UTF8 . GetBytes ( "hello world" ) ,
431+ new byte [ 1024 ] ,
432+ new byte [ 8192 ] ,
433+ } ;
434+
435+ foreach ( var payload in payloads )
436+ {
437+ var sealed1 = sender . Seal ( payload , receiver . GetPublicKey ( ) ) ;
438+ var opened = receiver . Open ( sealed1 , sender . GetPublicKey ( ) ) ;
439+ Assert . Equal ( payload , opened ) ;
440+ }
441+ }
442+
443+ [ Fact ]
444+ public void DecodePubCurveKey_blackbox_two_parties_cross_seal ( )
445+ {
446+ // Black box: both parties can seal for each other and open
447+ var alice = KeyPair . CreatePair ( PrefixByte . Curve ) ;
448+ var bob = KeyPair . CreatePair ( PrefixByte . Curve ) ;
449+ var message = Encoding . UTF8 . GetBytes ( "secret message" ) ;
450+
451+ // Alice seals for Bob
452+ var sealedForBob = alice . Seal ( message , bob . GetPublicKey ( ) ) ;
453+ Assert . Equal ( message , bob . Open ( sealedForBob , alice . GetPublicKey ( ) ) ) ;
454+
455+ // Bob seals for Alice
456+ var sealedForAlice = bob . Seal ( message , alice . GetPublicKey ( ) ) ;
457+ Assert . Equal ( message , alice . Open ( sealedForAlice , bob . GetPublicKey ( ) ) ) ;
458+ }
459+
460+ [ Fact ]
461+ public void DecodePubCurveKey_blackbox_wrong_sender_fails_open ( )
462+ {
463+ // Black box: opening with wrong sender key must fail
464+ var alice = KeyPair . CreatePair ( PrefixByte . Curve ) ;
465+ var bob = KeyPair . CreatePair ( PrefixByte . Curve ) ;
466+ var eve = KeyPair . CreatePair ( PrefixByte . Curve ) ;
467+
468+ var sealed1 = alice . Seal ( Encoding . UTF8 . GetBytes ( "secret" ) , bob . GetPublicKey ( ) ) ;
469+
470+ // Bob tries to open with Eve's public key as sender — must fail
471+ Assert . ThrowsAny < Exception > ( ( ) => bob . Open ( sealed1 , eve . GetPublicKey ( ) ) ) ;
472+ }
473+
474+ [ Fact ]
475+ public void DecodePubCurveKey_blackbox_wrong_receiver_fails_open ( )
476+ {
477+ // Black box: wrong receiver cannot open
478+ var alice = KeyPair . CreatePair ( PrefixByte . Curve ) ;
479+ var bob = KeyPair . CreatePair ( PrefixByte . Curve ) ;
480+ var eve = KeyPair . CreatePair ( PrefixByte . Curve ) ;
481+
482+ var sealed1 = alice . Seal ( Encoding . UTF8 . GetBytes ( "secret" ) , bob . GetPublicKey ( ) ) ;
483+
484+ // Eve tries to open something meant for Bob
485+ Assert . ThrowsAny < Exception > ( ( ) => eve . Open ( sealed1 , alice . GetPublicKey ( ) ) ) ;
486+ }
487+
488+ [ Fact ]
489+ public void DecodePubCurveKey_blackbox_tampered_ciphertext_fails ( )
490+ {
491+ var alice = KeyPair . CreatePair ( PrefixByte . Curve ) ;
492+ var bob = KeyPair . CreatePair ( PrefixByte . Curve ) ;
493+
494+ var sealed1 = alice . Seal ( Encoding . UTF8 . GetBytes ( "secret" ) , bob . GetPublicKey ( ) ) ;
495+
496+ // Flip a byte in the encrypted payload (after version + nonce header)
497+ sealed1 [ 30 ] ^= 0xFF ;
498+
499+ Assert . ThrowsAny < Exception > ( ( ) => bob . Open ( sealed1 , alice . GetPublicKey ( ) ) ) ;
500+ }
501+
502+ [ Fact ]
503+ public void DecodePubCurveKey_blackbox_each_seal_produces_different_ciphertext ( )
504+ {
505+ // Black box: nonce randomization means same plaintext encrypts differently
506+ var alice = KeyPair . CreatePair ( PrefixByte . Curve ) ;
507+ var bob = KeyPair . CreatePair ( PrefixByte . Curve ) ;
508+ var message = Encoding . UTF8 . GetBytes ( "hello" ) ;
509+
510+ var sealed1 = alice . Seal ( message , bob . GetPublicKey ( ) ) ;
511+ var sealed2 = alice . Seal ( message , bob . GetPublicKey ( ) ) ;
512+
513+ Assert . NotEqual ( sealed1 , sealed2 ) ;
514+
515+ // But both decrypt to the same message
516+ Assert . Equal ( message , bob . Open ( sealed1 , alice . GetPublicKey ( ) ) ) ;
517+ Assert . Equal ( message , bob . Open ( sealed2 , alice . GetPublicKey ( ) ) ) ;
518+ }
519+
239520 [ Fact ]
240521 public void Public_key_does_not_have_seed_nor_secret_key ( )
241522 {
0 commit comments