diff --git a/go.mod b/go.mod index e77866d..d3250a1 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b go-simpler.org/env v0.12.0 go.uber.org/atomic v1.11.0 + golang.org/x/crypto v0.41.0 golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/lint v0.0.0-20241112194109-818c5a804067 golang.org/x/net v0.43.0 diff --git a/go.sum b/go.sum index 18b79c9..6f23685 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= diff --git a/pkg/crypto/encryption/README.md b/pkg/crypto/encryption/README.md new file mode 100644 index 0000000..2ca8f06 --- /dev/null +++ b/pkg/crypto/encryption/README.md @@ -0,0 +1 @@ +Code copied from https://github.com/paulmillr/nip44/tree/e7aed61aaf77240ac10c325683eed14b22e7950f/go. diff --git a/pkg/crypto/encryption/doc.go b/pkg/crypto/encryption/doc.go new file mode 100644 index 0000000..07bdbbb --- /dev/null +++ b/pkg/crypto/encryption/doc.go @@ -0,0 +1,3 @@ +// Package encryption contains the message encryption schemes defined in NIP-04 +// and NIP-44, used for encrypting the content of nostr messages. +package encryption diff --git a/pkg/crypto/encryption/nip4.go b/pkg/crypto/encryption/nip4.go new file mode 100644 index 0000000..3507f65 --- /dev/null +++ b/pkg/crypto/encryption/nip4.go @@ -0,0 +1,88 @@ +package encryption + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + + "lol.mleku.dev/chk" + "lol.mleku.dev/errorf" + "lukechampine.com/frand" +) + +// EncryptNip4 encrypts message with key using aes-256-cbc. key should be the shared secret generated by +// ComputeSharedSecret. +// +// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector). +func EncryptNip4(msg, key []byte) (ct []byte, err error) { + // block size is 16 bytes + iv := make([]byte, 16) + if _, err = frand.Read(iv); chk.E(err) { + err = errorf.E("error creating initialization vector: %w", err) + return + } + // automatically picks aes-256 based on key length (32 bytes) + var block cipher.Block + if block, err = aes.NewCipher(key); chk.E(err) { + err = errorf.E("error creating block cipher: %w", err) + return + } + mode := cipher.NewCBCEncrypter(block, iv) + plaintext := []byte(msg) + // add padding + base := len(plaintext) + // this will be a number between 1 and 16 (inclusive), never 0 + bs := block.BlockSize() + padding := bs - base%bs + // encode the padding in all the padding bytes themselves + padText := bytes.Repeat([]byte{byte(padding)}, padding) + paddedMsgBytes := append(plaintext, padText...) + ciphertext := make([]byte, len(paddedMsgBytes)) + mode.CryptBlocks(ciphertext, paddedMsgBytes) + return []byte(base64.StdEncoding.EncodeToString(ciphertext) + "?iv=" + + base64.StdEncoding.EncodeToString(iv)), nil +} + +// DecryptNip4 decrypts a content string using the shared secret key. The inverse operation to message -> +// EncryptNip4(message, key). +func DecryptNip4(content, key []byte) (msg []byte, err error) { + parts := bytes.Split(content, []byte("?iv=")) + if len(parts) < 2 { + return nil, errorf.E( + "error parsing encrypted message: no initialization vector", + ) + } + ciphertext := make([]byte, base64.StdEncoding.EncodedLen(len(parts[0]))) + if _, err = base64.StdEncoding.Decode(ciphertext, parts[0]); chk.E(err) { + err = errorf.E("error decoding ciphertext from base64: %w", err) + return + } + iv := make([]byte, base64.StdEncoding.EncodedLen(len(parts[1]))) + if _, err = base64.StdEncoding.Decode(iv, parts[1]); chk.E(err) { + err = errorf.E("error decoding iv from base64: %w", err) + return + } + var block cipher.Block + if block, err = aes.NewCipher(key); chk.E(err) { + err = errorf.E("error creating block cipher: %w", err) + return + } + mode := cipher.NewCBCDecrypter(block, iv) + msg = make([]byte, len(ciphertext)) + mode.CryptBlocks(msg, ciphertext) + // remove padding + var ( + plaintextLen = len(msg) + ) + if plaintextLen > 0 { + // the padding amount is encoded in the padding bytes themselves + padding := int(msg[plaintextLen-1]) + if padding > plaintextLen { + err = errorf.E("invalid padding amount: %d", padding) + return + } + msg = msg[0 : plaintextLen-padding] + } + return msg, nil +} diff --git a/pkg/crypto/encryption/nip44.go b/pkg/crypto/encryption/nip44.go new file mode 100644 index 0000000..c64298e --- /dev/null +++ b/pkg/crypto/encryption/nip44.go @@ -0,0 +1,260 @@ +package encryption + +import ( + "crypto/hmac" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "io" + "math" + + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/hkdf" + "lol.mleku.dev/chk" + "lol.mleku.dev/errorf" + "next.orly.dev/pkg/crypto/p256k" + "next.orly.dev/pkg/crypto/sha256" + "next.orly.dev/pkg/interfaces/signer" + "next.orly.dev/pkg/utils" +) + +const ( + version byte = 2 + MinPlaintextSize = 0x0001 // 1b msg => padded to 32b + MaxPlaintextSize = 0xffff // 65535 (64kb-1) => padded to 64kb +) + +type Opts struct { + err error + nonce []byte +} + +// Deprecated: use WithCustomNonce instead of WithCustomSalt, so the naming is less confusing +var WithCustomSalt = WithCustomNonce + +// WithCustomNonce enables using a custom nonce (salt) instead of using the +// system crypto/rand entropy source. +func WithCustomNonce(salt []byte) func(opts *Opts) { + return func(opts *Opts) { + if len(salt) != 32 { + opts.err = errorf.E("salt must be 32 bytes, got %d", len(salt)) + } + opts.nonce = salt + } +} + +// Encrypt data using a provided symmetric conversation key using NIP-44 +// encryption (chacha20 cipher stream and sha256 HMAC). +func Encrypt( + plaintext, conversationKey []byte, applyOptions ...func(opts *Opts), +) ( + cipherString []byte, err error, +) { + + var o Opts + for _, apply := range applyOptions { + apply(&o) + } + if chk.E(o.err) { + err = o.err + return + } + if o.nonce == nil { + o.nonce = make([]byte, 32) + if _, err = rand.Read(o.nonce); chk.E(err) { + return + } + } + var enc, cc20nonce, auth []byte + if enc, cc20nonce, auth, err = getKeys( + conversationKey, o.nonce, + ); chk.E(err) { + return + } + plain := plaintext + size := len(plain) + if size < MinPlaintextSize || size > MaxPlaintextSize { + err = errorf.E("plaintext should be between 1b and 64kB") + return + } + padding := CalcPadding(size) + padded := make([]byte, 2+padding) + binary.BigEndian.PutUint16(padded, uint16(size)) + copy(padded[2:], plain) + var cipher []byte + if cipher, err = encrypt(enc, cc20nonce, padded); chk.E(err) { + return + } + var mac []byte + if mac, err = sha256Hmac(auth, cipher, o.nonce); chk.E(err) { + return + } + ct := make([]byte, 0, 1+32+len(cipher)+32) + ct = append(ct, version) + ct = append(ct, o.nonce...) + ct = append(ct, cipher...) + ct = append(ct, mac...) + cipherString = make([]byte, base64.StdEncoding.EncodedLen(len(ct))) + base64.StdEncoding.Encode(cipherString, ct) + return +} + +// Decrypt data that has been encoded using a provided symmetric conversation +// key using NIP-44 encryption (chacha20 cipher stream and sha256 HMAC). +func Decrypt(b64ciphertextWrapped, conversationKey []byte) ( + plaintext []byte, + err error, +) { + cLen := len(b64ciphertextWrapped) + if cLen < 132 || cLen > 87472 { + err = errorf.E("invalid payload length: %d", cLen) + return + } + if len(b64ciphertextWrapped) > 0 && b64ciphertextWrapped[0] == '#' { + err = errorf.E("unknown version") + return + } + var decoded []byte + if decoded, err = base64.StdEncoding.DecodeString(string(b64ciphertextWrapped)); chk.E(err) { + return + } + if decoded[0] != version { + err = errorf.E("unknown version %d", decoded[0]) + return + } + dLen := len(decoded) + if dLen < 99 || dLen > 65603 { + err = errorf.E("invalid data length: %d", dLen) + return + } + nonce, ciphertext, givenMac := decoded[1:33], decoded[33:dLen-32], decoded[dLen-32:] + var enc, cc20nonce, auth []byte + if enc, cc20nonce, auth, err = getKeys(conversationKey, nonce); chk.E(err) { + return + } + var expectedMac []byte + if expectedMac, err = sha256Hmac(auth, ciphertext, nonce); chk.E(err) { + return + } + if !utils.FastEqual(givenMac, expectedMac) { + err = errorf.E("invalid hmac") + return + } + var padded []byte + if padded, err = encrypt(enc, cc20nonce, ciphertext); chk.E(err) { + return + } + unpaddedLen := binary.BigEndian.Uint16(padded[0:2]) + if unpaddedLen < uint16(MinPlaintextSize) || unpaddedLen > uint16(MaxPlaintextSize) || + len(padded) != 2+CalcPadding(int(unpaddedLen)) { + err = errorf.E("invalid padding") + return + } + unpadded := padded[2:][:unpaddedLen] + if len(unpadded) == 0 || len(unpadded) != int(unpaddedLen) { + err = errorf.E("invalid padding") + return + } + plaintext = unpadded + return +} + +// GenerateConversationKeyFromHex performs an ECDH key generation hashed with the nip-44-v2 using hkdf. +func GenerateConversationKeyFromHex(pkh, skh string) (ck []byte, err error) { + if skh >= "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" || + skh == "0000000000000000000000000000000000000000000000000000000000000000" { + err = errorf.E( + "invalid private key: x coordinate %s is not on the secp256k1 curve", + skh, + ) + return + } + var sign signer.I + if sign, err = p256k.NewSecFromHex(skh); chk.E(err) { + return + } + var pk []byte + if pk, err = p256k.HexToBin(pkh); chk.E(err) { + return + } + var shared []byte + if shared, err = sign.ECDH(pk); chk.E(err) { + return + } + ck = hkdf.Extract(sha256.New, shared, []byte("nip44-v2")) + return +} + +func GenerateConversationKeyWithSigner(sign signer.I, pk []byte) ( + ck []byte, err error, +) { + var shared []byte + if shared, err = sign.ECDH(pk); chk.E(err) { + return + } + ck = hkdf.Extract(sha256.New, shared, []byte("nip44-v2")) + return +} + +func encrypt(key, nonce, message []byte) (dst []byte, err error) { + var cipher *chacha20.Cipher + if cipher, err = chacha20.NewUnauthenticatedCipher(key, nonce); chk.E(err) { + return + } + dst = make([]byte, len(message)) + cipher.XORKeyStream(dst, message) + return +} + +func sha256Hmac(key, ciphertext, nonce []byte) (h []byte, err error) { + if len(nonce) != sha256.Size { + err = errorf.E("nonce aad must be 32 bytes") + return + } + hm := hmac.New(sha256.New, key) + hm.Write(nonce) + hm.Write(ciphertext) + h = hm.Sum(nil) + return +} + +func getKeys(conversationKey, nonce []byte) ( + enc, cc20nonce, auth []byte, err error, +) { + if len(conversationKey) != 32 { + err = errorf.E("conversation key must be 32 bytes") + return + } + if len(nonce) != 32 { + err = errorf.E("nonce must be 32 bytes") + return + } + r := hkdf.Expand(sha256.New, conversationKey, nonce) + enc = make([]byte, 32) + if _, err = io.ReadFull(r, enc); chk.E(err) { + return + } + cc20nonce = make([]byte, 12) + if _, err = io.ReadFull(r, cc20nonce); chk.E(err) { + return + } + auth = make([]byte, 32) + if _, err = io.ReadFull(r, auth); chk.E(err) { + return + } + return +} + +// CalcPadding creates padding for the message payload that is precisely a power +// of two in order to reduce the chances of plaintext attack. This is plainly +// retarded because it could blow out the message size a lot when just a random few +// dozen bytes and a length prefix would achieve the same result. +func CalcPadding(sLen int) (l int) { + if sLen <= 32 { + return 32 + } + nextPower := 1 << int(math.Floor(math.Log2(float64(sLen-1)))+1) + chunk := int(math.Max(32, float64(nextPower/8))) + l = chunk * int(math.Floor(float64((sLen-1)/chunk))+1) + return +} diff --git a/pkg/crypto/encryption/nip44_test.go b/pkg/crypto/encryption/nip44_test.go new file mode 100644 index 0000000..9380888 --- /dev/null +++ b/pkg/crypto/encryption/nip44_test.go @@ -0,0 +1,1381 @@ +package encryption + +import ( + "crypto/rand" + "fmt" + "hash" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "lol.mleku.dev/chk" + "next.orly.dev/pkg/crypto/keys" + "next.orly.dev/pkg/crypto/sha256" + "next.orly.dev/pkg/encoders/hex" +) + +func assertCryptPriv( + t *testing.T, + sk1, sk2, conversationKey, salt, plaintext, expected string, +) { + var ( + k1, s, plaintextBytes, actualBytes, + expectedBytes, decrypted []byte + ok bool + err error + ) + k1, err = hex.Dec(conversationKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for conversation key: %v", err, + ); !ok { + return + } + if ok = assertConversationKeyGenerationSec( + t, sk1, sk2, conversationKey, + ); !ok { + return + } + s, err = hex.Dec(salt) + if ok = assert.NoErrorf( + t, err, "hex decode failed for salt: %v", err, + ); !ok { + return + } + plaintextBytes = []byte(plaintext) + actualBytes, err = Encrypt(plaintextBytes, k1, WithCustomNonce(s)) + if ok = assert.NoError(t, err, "encryption failed: %v", err); !ok { + return + } + expectedBytes = []byte(expected) + if ok = assert.Equalf( + t, string(expectedBytes), string(actualBytes), "wrong encryption", + ); !ok { + return + } + decrypted, err = Decrypt(expectedBytes, k1) + if ok = assert.NoErrorf(t, err, "decryption failed: %v", err); !ok { + return + } + assert.Equal(t, decrypted, plaintextBytes, "wrong decryption") +} + +func assertDecryptFail( + t *testing.T, conversationKey, plaintext, ciphertext, msg string, +) { + var ( + k1, ciphertextBytes []byte + ok bool + err error + ) + k1, err = hex.Dec(conversationKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for conversation key: %v", err, + ); !ok { + return + } + ciphertextBytes = []byte(ciphertext) + _, err = Decrypt(ciphertextBytes, k1) + assert.ErrorContains(t, err, msg) +} + +func assertConversationKeyFail( + t *testing.T, priv string, pub string, msg string, +) { + _, err := GenerateConversationKeyFromHex(pub, priv) + assert.ErrorContains(t, err, msg) +} + +func assertConversationKeyGeneration( + t *testing.T, priv, pub, conversationKey string, +) bool { + var ( + actualConversationKey, + expectedConversationKey []byte + ok bool + err error + ) + expectedConversationKey, err = hex.Dec(conversationKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for conversation key: %v", err, + ); !ok { + return false + } + actualConversationKey, err = GenerateConversationKeyFromHex(pub, priv) + if ok = assert.NoErrorf( + t, err, "conversation key generation failed: %v", err, + ); !ok { + return false + } + if ok = assert.Equalf( + t, expectedConversationKey, actualConversationKey, + "wrong conversation key", + ); !ok { + return false + } + return true +} + +func assertConversationKeyGenerationSec( + t *testing.T, sk1, sk2, conversationKey string, +) bool { + pub2, err := keys.GetPublicKeyHex(sk2) + if ok := assert.NoErrorf( + t, err, "failed to derive pubkey from sk2: %v", err, + ); !ok { + return false + } + return assertConversationKeyGeneration(t, sk1, pub2, conversationKey) +} + +func assertConversationKeyGenerationPub( + t *testing.T, sk, pub, conversationKey string, +) bool { + return assertConversationKeyGeneration(t, sk, pub, conversationKey) +} + +func assertMessageKeyGeneration( + t *testing.T, + conversationKey, salt, chachaKey, chachaSalt, hmacKey string, +) bool { + var ( + convKey, convSalt, actualChaChaKey, expectedChaChaKey, actualChaChaNonce, + expectedChaChaNonce, actualHmacKey, expectedHmacKey []byte + ok bool + err error + ) + convKey, err = hex.Dec(conversationKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for convKey: %v", err, + ); !ok { + return false + } + convSalt, err = hex.Dec(salt) + if ok = assert.NoErrorf( + t, err, "hex decode failed for salt: %v", err, + ); !ok { + return false + } + expectedChaChaKey, err = hex.Dec(chachaKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for encrypt key: %v", err, + ); !ok { + return false + } + expectedChaChaNonce, err = hex.Dec(chachaSalt) + if ok = assert.NoErrorf( + t, err, "hex decode failed for encrypt nonce: %v", err, + ); !ok { + return false + } + expectedHmacKey, err = hex.Dec(hmacKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for hmac key: %v", err, + ); !ok { + return false + } + actualChaChaKey, actualChaChaNonce, actualHmacKey, err = getKeys( + convKey, convSalt, + ) + if ok = assert.NoErrorf( + t, err, "message key generation failed: %v", err, + ); !ok { + return false + } + if ok = assert.Equalf( + t, expectedChaChaKey, actualChaChaKey, "wrong encrypt key", + ); !ok { + return false + } + if ok = assert.Equalf( + t, expectedChaChaNonce, actualChaChaNonce, + "wrong encrypt nonce", + ); !ok { + return false + } + if ok = assert.Equalf( + t, expectedHmacKey, actualHmacKey, "wrong hmac key", + ); !ok { + return false + } + return true +} + +func assertCryptLong( + t *testing.T, conversationKey, salt string, pattern []byte, repeat int, + plaintextSha256, payloadSha256 string, +) { + var ( + convKey, convSalt, plaintext, payloadBytes []byte + actualPlaintextSha256, actualPayloadSha256 string + h hash.Hash + ok bool + err error + ) + convKey, err = hex.Dec(conversationKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for convKey: %v", err, + ); !ok { + return + } + convSalt, err = hex.Dec(salt) + if ok = assert.NoErrorf( + t, err, "hex decode failed for salt: %v", err, + ); !ok { + return + } + plaintext = make([]byte, 0, len(pattern)*repeat) + for i := 0; i < repeat; i++ { + plaintext = append(plaintext, pattern...) + } + h = sha256.New() + h.Write(plaintext) + actualPlaintextSha256 = hex.Enc(h.Sum(nil)) + if ok = assert.Equalf( + t, plaintextSha256, actualPlaintextSha256, + "invalid plaintext sha256 hash: %v", err, + ); !ok { + return + } + payloadBytes, err = Encrypt( + plaintext, convKey, WithCustomNonce(convSalt), + ) + if ok = assert.NoErrorf(t, err, "encryption failed: %v", err); !ok { + return + } + h.Reset() + h.Write(payloadBytes) + actualPayloadSha256 = hex.Enc(h.Sum(nil)) + if ok = assert.Equalf( + t, payloadSha256, actualPayloadSha256, + "invalid payload sha256 hash: %v", err, + ); !ok { + return + } +} + +func TestCryptPriv001(t *testing.T) { + assertCryptPriv( + t, + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "0000000000000000000000000000000000000000000000000000000000000001", + "a", + "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb", + ) +} + +func TestCryptPriv002(t *testing.T) { + assertCryptPriv( + t, + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000001", + "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "f00000000000000000000000000000f00000000000000000000000000000000f", + "๐Ÿ•๐Ÿซƒ", + "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj", + ) +} + +func TestCryptPriv003(t *testing.T) { + assertCryptPriv( + t, + "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", + "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", + "3e2b52a63be47d34fe0a80e34e73d436d6963bc8f39827f327057a9986c20a45", + "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b", + "่กจใƒใ‚A้ท—ล’รฉ๏ผข้€รœรŸยชฤ…รฑไธ‚ใ€๐ €€", + "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs=", + ) +} + +func TestCryptPriv004(t *testing.T) { + assertCryptPriv( + t, + "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c", + "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba", + "d5a2f879123145a4b291d767428870f5a8d9e5007193321795b40183d4ab8c2b", + "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf", + "ability๐Ÿค็š„ ศบศพ", + "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvbG2S0x4Yu86QGwPTy7mP3961I1XqB6SFFTzqDZZavhxoWMj7mEVGMQIsh2RLWI5EYQaQDIePSnXPlzf7CIt+voTD", + ) +} + +func TestCryptPriv005(t *testing.T) { + assertCryptPriv( + t, + "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c", + "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae", + "3b15c977e20bfe4b8482991274635edd94f366595b1a3d2993515705ca3cedb8", + "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1", + "pepper๐Ÿ‘€ั—ะถะฐะบ", + "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGxY3AduCS+jTUgAAnfvKafkmpy15+i9YMwCdccisRa8SvzW671T2JO4LFSPX31K4kYUKelSAdSPwe9NwO6LhOsnoJ+", + ) +} + +func TestCryptPriv006(t *testing.T) { + assertCryptPriv( + t, + "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f", + "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3", + "4f1538411098cf11c8af216836444787c462d47f97287f46cf7edb2c4915b8a5", + "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407", + "( อกยฐ อœส– อกยฐ)", + "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHv4qhKQwnFQ54OjVMgqCea/Vj0YqBSdhqNR777TJ4zIUk7R0fnizp6l1zwgzWv7+ee6u+0/89KIjY5q1wu6inyuiv", + ) +} + +func TestCryptPriv007(t *testing.T) { + assertCryptPriv( + t, + "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68", + "ู…ูู†ูŽุงู‚ูŽุดูŽุฉู ุณูุจูู„ู ุงูุณู’ุชูุฎู’ุฏูŽุงู…ู ุงู„ู„ูู‘ุบูŽุฉู ูููŠ ุงู„ู†ูู‘ุธูู…ู ุงู„ู’ู‚ูŽุงุฆูู…ูŽุฉู ูˆูŽูููŠู… ูŠูŽุฎูุตูŽู‘ ุงู„ุชูŽู‘ุทู’ุจููŠู‚ูŽุงุชู ุงู„ู’ุญุงุณููˆุจููŠูŽู‘ุฉูุŒ", + "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH", + ) +} + +func TestCryptPriv008(t *testing.T) { + assertCryptPriv( + t, + "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68", + "ู…ูู†ูŽุงู‚ูŽุดูŽุฉู ุณูุจูู„ู ุงูุณู’ุชูุฎู’ุฏูŽุงู…ู ุงู„ู„ูู‘ุบูŽุฉู ูููŠ ุงู„ู†ูู‘ุธูู…ู ุงู„ู’ู‚ูŽุงุฆูู…ูŽุฉู ูˆูŽูููŠู… ูŠูŽุฎูุตูŽู‘ ุงู„ุชูŽู‘ุทู’ุจููŠู‚ูŽุงุชู ุงู„ู’ุญุงุณููˆุจููŠูŽู‘ุฉูุŒ", + "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH", + ) +} + +func TestCryptPriv009X(t *testing.T) { + assertCryptPriv( + t, + "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023", + "ุงู„ูƒู„ ููŠ ุงู„ู…ุฌู…ูˆ ุนุฉ (5)", + "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjbOTrlfrxak5Lki42v2jMPpLSicy8eHjsWkkMtF0i925vOaKG/ZkMHh9ccQBdfTvgEGKzztedqDCAWb5TP1YwU1PsWaiiqG3+WgVvJiO4lUdMHXL7+zKKx8bgDtowzz4QAwI=", + ) +} + +func TestCryptPriv010(t *testing.T) { + assertCryptPriv( + t, + "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0", + "๐–‘๐–†๐–Ÿ๐–ž ็คพๆœƒ็ง‘ๅญธ้™ข่ชžๅญธ็ ”็ฉถๆ‰€", + "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLgh/TsxPLFSANcT67EC1t/qxjru5ZoADjKVEt2ejdx+xGvH49mcdfbc+l+L7gJtkH7GLKpE9pQNQWNHMAmj043PAXJZ++fiJObMRR2mye5VHEANzZWkZXMrXF7YjuG10S1pOU=", + ) +} + +func TestCryptPriv011(t *testing.T) { + assertCryptPriv( + t, + "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54", + "๐Ÿ™ˆ ๐Ÿ™‰ ๐Ÿ™Š 0๏ธโƒฃ 1๏ธโƒฃ 2๏ธโƒฃ 3๏ธโƒฃ 4๏ธโƒฃ 5๏ธโƒฃ 6๏ธโƒฃ 7๏ธโƒฃ 8๏ธโƒฃ 9๏ธโƒฃ ๐Ÿ”Ÿ Powerู„ูู„ูุตู‘ุจูู„ูู„ุตู‘ุจูุฑุฑู‹ เฅฃ เฅฃh เฅฃ เฅฃๅ†—", + "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtUf05aMF89q1FLwJvaFJYICZoMYgRJHFLwPiOHce7fuAc40kX0wXJvipyBJ9HzCOj7CgtnC1/cmPCHR3s5AIORmroBWglm1LiFMohv1FSPEbaBD51VXxJa4JyWpYhreSOEjn1wd0lMKC9b+osV2N2tpbs+rbpQem2tRen3sWflmCqjkG5VOVwRErCuXuPb5+hYwd8BoZbfCrsiAVLd7YT44dRtKNBx6rkabWfddKSLtreHLDysOhQUVOp/XkE7OzSkWl6sky0Hva6qJJ/V726hMlomvcLHjE41iKmW2CpcZfOedg==", + ) +} + +func TestCryptLong001(t *testing.T) { + assertCryptLong( + t, + "8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed", + "326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7", + []byte("x"), + 65535, + "09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3", + "90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a", + ) +} + +func TestCryptLong002(t *testing.T) { + assertCryptLong( + t, + "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0", + "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0", + []byte("!"), + 65535, + "6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e", + "8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844", + ) +} + +func TestCryptLong003(t *testing.T) { + assertCryptLong( + t, + "7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be", + "34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033", + []byte("๐Ÿฆ„"), + 16383, + "a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f", + "b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15", + ) +} + +func TestConversationKeyFail001(t *testing.T) { + // sec1 higher than curve.n + assertConversationKeyFail( + t, + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "invalid private key: x coordinate ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff is not on the secp256k1 curve", + ) +} + +func TestConversationKeyFail002(t *testing.T) { + // sec1 is 0 + assertConversationKeyFail( + t, + "0000000000000000000000000000000000000000000000000000000000000000", + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "invalid private key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve", + ) +} + +func TestConversationKeyFail003(t *testing.T) { + // pub2 is invalid, no sqrt, all-ff + assertConversationKeyFail( + t, + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "invalid public key: x >= field prime", + // "invalid public key: x >= field prime", + ) +} + +func TestConversationKeyFail004(t *testing.T) { + // sec1 == curve.n + assertConversationKeyFail( + t, + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "invalid private key: x coordinate fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 is not on the secp256k1 curve", + ) +} + +func TestConversationKeyFail005(t *testing.T) { + // pub2 is invalid, no sqrt + assertConversationKeyFail( + t, + "0000000000000000000000000000000000000000000000000000000000000002", + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "invalid public key: x coordinate 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef is not on the secp256k1 curve", + // "invalid public key: x coordinate 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef is not on the secp256k1 curve", + ) +} + +func TestConversationKeyFail006(t *testing.T) { + // pub2 is point of order 3 on twist + assertConversationKeyFail( + t, + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "0000000000000000000000000000000000000000000000000000000000000000", + "invalid public key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve", + // "invalid public key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve", + ) +} + +func TestConversationKeyFail007(t *testing.T) { + // pub2 is point of order 13 on twist + assertConversationKeyFail( + t, + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d", + "invalid public key: x coordinate eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d is not on the secp256k1 curve", + // "invalid public key: x coordinate eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d is not on the secp256k1 curve", + ) +} + +func TestConversationKeyFail008(t *testing.T) { + // pub2 is point of order 3319 on twist + assertConversationKeyFail( + t, + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f", + "invalid public key: x coordinate 709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f is not on the secp256k1 curve", + // "invalid public key: x coordinate 709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f is not on the secp256k1 curve", + ) +} + +func TestDecryptFail001(t *testing.T) { + assertDecryptFail( + t, + "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642", + // "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "n o b l e", + "#Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJdU0MIDf06CUvEvdnr1cp1fiMtlM/GrE92xAc1K5odTpCzUB+mjXgbaqtntBUbTToSUoT0ovrlPwzGjyp", + "unknown version", + ) +} + +func TestDecryptFail002(t *testing.T) { + assertDecryptFail( + t, + "36f04e558af246352dcf73b692fbd3646a2207bd8abd4b1cd26b234db84d9481", + // "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781", + "โš ๏ธ", + "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBZFaha6EAIRueE9D1B1RuoiuFScC0Q94yjIuxZD3JStQtE8JMNacWFs9rlYP+ZydtHhRucp+lxfdvFlaGV/sQlqZz", + "unknown version 0", + ) +} + +func TestDecryptFail003(t *testing.T) { + assertDecryptFail( + t, + "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642", + // "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "n o s t r", + "Atั„upco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJZE0UICD06CUvEvdnr1cp1fiMtlM/GrE92xAc1EwsVCQEgWEu2gsHUVf4JAa3TpgkmFc3TWsax0v6n/Wq", + "illegal base64 data at input byte 2", + ) +} + +func TestDecryptFail004(t *testing.T) { + assertDecryptFail( + t, + "cff7bd6a3e29a450fd27f6c125d5edeb0987c475fd1e8d97591e0d4d8a89763c", + // "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25", + "ยฏ\\_(ใƒ„)_/ยฏ", + "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholyySBfeh+EN8wNB9gaLlg4j6wdBYh+3oK+mnxWu3NKRbSvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "invalid hmac", + ) +} + +func TestDecryptFail005(t *testing.T) { + assertDecryptFail( + t, + "cfcc9cf682dfb00b11357f65bdc45e29156b69db424d20b3596919074f5bf957", + // "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4", + "๐ŸฅŽ", + "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0oRWQ8avl4OLqWZiTJ10vIgKrNqjoaX+fNhE9RqmR5g0f6BtUg1ijFMz71MO1D4lQLQfW7+UHva8PGYgQ1QpHlKgR", + "invalid hmac", + ) +} + +func TestDecryptFail006(t *testing.T) { + assertDecryptFail( + t, + "5254827d29177622d40a7b67cad014fe7137700c3c523903ebbe3e1b74d40214", + // "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1", + "elliptic-curve cryptography", + "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHB573MI7R7rrTYftpqmvUpahmBC2sngmI14/L0HjOZ7lWGJlzdh6luiOnGPc46cGxf08MRC4CIuxx3i2Lm0KqgJ7vA", + "invalid padding", + ) +} + +func TestDecryptFail007(t *testing.T) { + assertDecryptFail( + t, + "fea39aca9aa8340c3a78ae1f0902aa7e726946e4efcd7783379df8096029c496", + // "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330", + "noble", + "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwRrrPJFyAQYZh5VpjC2QYzny5LIQ9v9lhqmZR4WBYRNJ0ognHVNMwiFV1SHpvUFT8HHZN/m/QarflbvDHAtO6pY16", + "invalid padding", + ) +} + +func TestDecryptFail008(t *testing.T) { + assertDecryptFail( + t, + "0c4cffb7a6f7e706ec94b2e879f1fc54ff8de38d8db87e11787694d5392d5b3f", + // "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b", + "censorship-resistant and global social network", + "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6b2NZDaNm/TPkZGr75kbB6tCSoq7YRcbPiNfJXNch3Tf+o9+zZTMxwjgX/nm3yDKR2kHQMBhVleCB9uPuljl40AJ8kXRD0gjw+aYRJFUMK9gCETZAjjmrsCM+nGRZ1FfNsHr6Z", + "invalid padding", + ) +} + +func TestDecryptFail009(t *testing.T) { + assertDecryptFail( + t, + "5cd2d13b9e355aeb2452afbd3786870dbeecb9d355b12cb0a3b6e9da5744cd35", + // "b60036976a1ada277b948fd4caa065304b96964742b89d26f26a25263a5060bd", + "0", + "", + "invalid payload length: 0", + ) +} + +func TestDecryptFail010(t *testing.T) { + assertDecryptFail( + t, + "d61d3f09c7dfe1c0be91af7109b60a7d9d498920c90cbba1e137320fdd938853", + // "1a29d02c8b4527745a2ccb38bfa45655deb37bc338ab9289d756354cea1fd07c", + "1", + "Ag==", + "invalid payload length: 4", + ) +} + +func TestDecryptFail011(t *testing.T) { + assertDecryptFail( + t, + "873bb0fc665eb950a8e7d5971965539f6ebd645c83c08cd6a85aafbad0f0bc47", + // "c826d3c38e765ab8cc42060116cd1464b2a6ce01d33deba5dedfb48615306d4a", + "2", + "AqxgToSh3H7iLYRJjoWAM+vSv/Y1mgNlm6OWWjOYUClrFF8=", + "invalid payload length: 48", + ) +} + +func TestDecryptFail012(t *testing.T) { + assertDecryptFail( + t, + "9f2fef8f5401ac33f74641b568a7a30bb19409c76ffdc5eae2db6b39d2617fbe", + // "9ff6484642545221624eaac7b9ea27133a4cc2356682a6033aceeef043549861", + "3", + "Ap/2SEZCVFIhYk6qx7nqJxM6TMI1ZoKmAzrO7vBDVJhhuZXWiM20i/tIsbjT0KxkJs2MZjh1oXNYMO9ggfk7i47WQA==", + "invalid payload length: 92", + ) +} + +func TestConversationKey001(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268", + "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133", + "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1", + ) +} + +func TestConversationKey002(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e", + "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800", + "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b", + ) +} + +func TestConversationKey003(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311", + "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1", + "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7", + ) +} + +func TestConversationKey004(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "86ae5ac8034eb2542ce23ec2f84375655dab7f836836bbd3c54cefe9fdc9c19f", + "59f90272378089d73f1339710c02e2be6db584e9cdbe86eed3578f0c67c23585", + "19f934aafd3324e8415299b64df42049afaa051c71c98d0aa10e1081f2e3e2ba", + ) +} + +func TestConversationKey005(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "2528c287fe822421bc0dc4c3615878eb98e8a8c31657616d08b29c00ce209e34", + "f66ea16104c01a1c532e03f166c5370a22a5505753005a566366097150c6df60", + "c833bbb292956c43366145326d53b955ffb5da4e4998a2d853611841903f5442", + ) +} + +func TestConversationKey006(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "49808637b2d21129478041813aceb6f2c9d4929cd1303cdaf4fbdbd690905ff2", + "74d2aab13e97827ea21baf253ad7e39b974bb2498cc747cdb168582a11847b65", + "4bf304d3c8c4608864c0fe03890b90279328cd24a018ffa9eb8f8ccec06b505d", + ) +} + +func TestConversationKey007(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "af67c382106242c5baabf856efdc0629cc1c5b4061f85b8ceaba52aa7e4b4082", + "bdaf0001d63e7ec994fad736eab178ee3c2d7cfc925ae29f37d19224486db57b", + "a3a575dd66d45e9379904047ebfb9a7873c471687d0535db00ef2daa24b391db", + ) +} + +func TestConversationKey008(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "0e44e2d1db3c1717b05ffa0f08d102a09c554a1cbbf678ab158b259a44e682f1", + "1ffa76c5cc7a836af6914b840483726207cb750889753d7499fb8b76aa8fe0de", + "a39970a667b7f861f100e3827f4adbf6f464e2697686fe1a81aeda817d6b8bdf", + ) +} + +func TestConversationKey009(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "5fc0070dbd0666dbddc21d788db04050b86ed8b456b080794c2a0c8e33287bb6", + "31990752f296dd22e146c9e6f152a269d84b241cc95bb3ff8ec341628a54caf0", + "72c21075f4b2349ce01a3e604e02a9ab9f07e35dd07eff746de348b4f3c6365e", + ) +} + +func TestConversationKey010(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "1b7de0d64d9b12ddbb52ef217a3a7c47c4362ce7ea837d760dad58ab313cba64", + "24383541dd8083b93d144b431679d70ef4eec10c98fceef1eff08b1d81d4b065", + "dd152a76b44e63d1afd4dfff0785fa07b3e494a9e8401aba31ff925caeb8f5b1", + ) +} + +func TestConversationKey011(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "df2f560e213ca5fb33b9ecde771c7c0cbd30f1cf43c2c24de54480069d9ab0af", + "eeea26e552fc8b5e377acaa03e47daa2d7b0c787fac1e0774c9504d9094c430e", + "770519e803b80f411c34aef59c3ca018608842ebf53909c48d35250bd9323af6", + ) +} + +func TestConversationKey012(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "cffff919fcc07b8003fdc63bc8a00c0f5dc81022c1c927c62c597352190d95b9", + "eb5c3cca1a968e26684e5b0eb733aecfc844f95a09ac4e126a9e58a4e4902f92", + "46a14ee7e80e439ec75c66f04ad824b53a632b8409a29bbb7c192e43c00bb795", + ) +} + +func TestConversationKey013(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "64ba5a685e443e881e9094647ddd32db14444bb21aa7986beeba3d1c4673ba0a", + "50e6a4339fac1f3bf86f2401dd797af43ad45bbf58e0801a7877a3984c77c3c4", + "968b9dbbfcede1664a4ca35a5d3379c064736e87aafbf0b5d114dff710b8a946", + ) +} + +func TestConversationKey014(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "dd0c31ccce4ec8083f9b75dbf23cc2878e6d1b6baa17713841a2428f69dee91a", + "b483e84c1339812bed25be55cff959778dfc6edde97ccd9e3649f442472c091b", + "09024503c7bde07eb7865505891c1ea672bf2d9e25e18dd7a7cea6c69bf44b5d", + ) +} + +func TestConversationKey015(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "af71313b0d95c41e968a172b33ba5ebd19d06cdf8a7a98df80ecf7af4f6f0358", + "2a5c25266695b461ee2af927a6c44a3c598b8095b0557e9bd7f787067435bc7c", + "fe5155b27c1c4b4e92a933edae23726a04802a7cc354a77ac273c85aa3c97a92", + ) +} + +func TestConversationKey016(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "6636e8a389f75fe068a03b3edb3ea4a785e2768e3f73f48ffb1fc5e7cb7289dc", + "514eb2064224b6a5829ea21b6e8f7d3ea15ff8e70e8555010f649eb6e09aec70", + "ff7afacd4d1a6856d37ca5b546890e46e922b508639214991cf8048ddbe9745c", + ) +} + +func TestConversationKey017(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "94b212f02a3cfb8ad147d52941d3f1dbe1753804458e6645af92c7b2ea791caa", + "f0cac333231367a04b652a77ab4f8d658b94e86b5a8a0c472c5c7b0d4c6a40cc", + "e292eaf873addfed0a457c6bd16c8effde33d6664265697f69f420ab16f6669b", + ) +} + +func TestConversationKey018(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "aa61f9734e69ae88e5d4ced5aae881c96f0d7f16cca603d3bed9eec391136da6", + "4303e5360a884c360221de8606b72dd316da49a37fe51e17ada4f35f671620a6", + "8e7d44fd4767456df1fb61f134092a52fcd6836ebab3b00766e16732683ed848", + ) +} + +func TestConversationKey019(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "5e914bdac54f3f8e2cba94ee898b33240019297b69e96e70c8a495943a72fc98", + "5bd097924f606695c59f18ff8fd53c174adbafaaa71b3c0b4144a3e0a474b198", + "f5a0aecf2984bf923c8cd5e7bb8be262d1a8353cb93959434b943a07cf5644bc", + ) +} + +func TestConversationKey020(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "8b275067add6312ddee064bcdbeb9d17e88aa1df36f430b2cea5cc0413d8278a", + "65bbbfca819c90c7579f7a82b750a18c858db1afbec8f35b3c1e0e7b5588e9b8", + "2c565e7027eb46038c2263563d7af681697107e975e9914b799d425effd248d6", + ) +} + +func TestConversationKey021(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "1ac848de312285f85e0f7ec208aac20142a1f453402af9b34ec2ec7a1f9c96fc", + "45f7318fe96034d23ee3ddc25b77f275cc1dd329664dd51b89f89c4963868e41", + "b56e970e5057a8fd929f8aad9248176b9af87819a708d9ddd56e41d1aec74088", + ) +} + +func TestConversationKey022(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "295a1cf621de401783d29d0e89036aa1c62d13d9ad307161b4ceb535ba1b40e6", + "840115ddc7f1034d3b21d8e2103f6cb5ab0b63cf613f4ea6e61ae3d016715cdd", + "b4ee9c0b9b9fef88975773394f0a6f981ca016076143a1bb575b9ff46e804753", + ) +} + +func TestConversationKey023(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "a28eed0fe977893856ab9667e06ace39f03abbcdb845c329a1981be438ba565d", + "b0f38b950a5013eba5ab4237f9ed29204a59f3625c71b7e210fec565edfa288c", + "9d3a802b45bc5aeeb3b303e8e18a92ddd353375710a31600d7f5fff8f3a7285b", + ) +} + +func TestConversationKey024(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "7ab65af72a478c05f5c651bdc4876c74b63d20d04cdbf71741e46978797cd5a4", + "f1112159161b568a9cb8c9dd6430b526c4204bcc8ce07464b0845b04c041beda", + "943884cddaca5a3fef355e9e7f08a3019b0b66aa63ec90278b0f9fdb64821e79", + ) +} + +func TestConversationKey025(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "95c79a7b75ba40f2229e85756884c138916f9d103fc8f18acc0877a7cceac9fe", + "cad76bcbd31ca7bbda184d20cc42f725ed0bb105b13580c41330e03023f0ffb3", + "81c0832a669eea13b4247c40be51ccfd15bb63fcd1bba5b4530ce0e2632f301b", + ) +} + +func TestConversationKey026(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "baf55cc2febd4d980b4b393972dfc1acf49541e336b56d33d429bce44fa12ec9", + "0c31cf87fe565766089b64b39460ebbfdedd4a2bc8379be73ad3c0718c912e18", + "37e2344da9ecdf60ae2205d81e89d34b280b0a3f111171af7e4391ded93b8ea6", + ) +} + +func TestConversationKey027(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "6eeec45acd2ed31693c5256026abf9f072f01c4abb61f51cf64e6956b6dc8907", + "e501b34ed11f13d816748c0369b0c728e540df3755bab59ed3327339e16ff828", + "afaa141b522ddb27bb880d768903a7f618bb8b6357728cae7fb03af639b946e6", + ) +} + +func TestConversationKey028(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "261a076a9702af1647fb343c55b3f9a4f1096273002287df0015ba81ce5294df", + "b2777c863878893ae100fb740c8fab4bebd2bf7be78c761a75593670380a6112", + "76f8d2853de0734e51189ced523c09427c3e46338b9522cd6f74ef5e5b475c74", + ) +} + +func TestConversationKey029(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "ed3ec71ca406552ea41faec53e19f44b8f90575eda4b7e96380f9cc73c26d6f3", + "86425951e61f94b62e20cae24184b42e8e17afcf55bafa58645efd0172624fae", + "f7ffc520a3a0e9e9b3c0967325c9bf12707f8e7a03f28b6cd69ae92cf33f7036", + ) +} + +func TestConversationKey030(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "5a788fc43378d1303ac78639c59a58cb88b08b3859df33193e63a5a3801c722e", + "a8cba2f87657d229db69bee07850fd6f7a2ed070171a06d006ec3a8ac562cf70", + "7d705a27feeedf78b5c07283362f8e361760d3e9f78adab83e3ae5ce7aeb6409", + ) +} + +func TestConversationKey031(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "63bffa986e382b0ac8ccc1aa93d18a7aa445116478be6f2453bad1f2d3af2344", + "b895c70a83e782c1cf84af558d1038e6b211c6f84ede60408f519a293201031d", + "3a3b8f00d4987fc6711d9be64d9c59cf9a709c6c6481c2cde404bcc7a28f174e", + ) +} + +func TestConversationKey032(t *testing.T) { + assertConversationKeyGenerationPub( + t, + "e4a8bcacbf445fd3721792b939ff58e691cdcba6a8ba67ac3467b45567a03e5c", + "b54053189e8c9252c6950059c783edb10675d06d20c7b342f73ec9fa6ed39c9d", + "7b3933b4ef8189d347169c7955589fc1cfc01da5239591a08a183ff6694c44ad", + ) +} + +func TestConversationKey033(t *testing.T) { + // sec1 = n-2, pub2: random, 0x02 + assertConversationKeyGenerationPub( + t, + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "0000000000000000000000000000000000000000000000000000000000000002", + "8b6392dbf2ec6a2b2d5b1477fc2be84d63ef254b667cadd31bd3f444c44ae6ba", + ) +} + +func TestConversationKey034(t *testing.T) { + // sec1 = 2, pub2: rand + assertConversationKeyGenerationPub( + t, + "0000000000000000000000000000000000000000000000000000000000000002", + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", + "be234f46f60a250bef52a5ee34c758800c4ca8e5030bf4cc1a31d37ba2104d43", + ) +} + +func TestConversationKey035(t *testing.T) { + // sec1 == pub2 + assertConversationKeyGenerationPub( + t, + "0000000000000000000000000000000000000000000000000000000000000001", + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "3b4610cb7189beb9cc29eb3716ecc6102f1247e8f3101a03a1787d8908aeb54e", + ) +} + +func TestMessageKeyGeneration001(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72", + "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76", + "c4ad129bb01180c0933a160c", + "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4", + ) +} + +func TestMessageKeyGeneration002(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "e1d6d28c46de60168b43d79dacc519698512ec35e8ccb12640fc8e9f26121101", + "e35b88f8d4a8f1606c5082f7a64b100e5d85fcdb2e62aeafbec03fb9e860ad92", + "22925e920cee4a50a478be90", + "46a7c55d4283cb0df1d5e29540be67abfe709e3b2e14b7bf9976e6df994ded30", + ) +} + +func TestMessageKeyGeneration003(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "cfc13bef512ac9c15951ab00030dfaf2626fdca638dedb35f2993a9eeb85d650", + "020783eb35fdf5b80ef8c75377f4e937efb26bcbad0e61b4190e39939860c4bf", + "d3594987af769a52904656ac", + "237ec0ccb6ebd53d179fa8fd319e092acff599ef174c1fdafd499ef2b8dee745", + ) +} + +func TestMessageKeyGeneration004(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "ea6eb84cac23c5c1607c334e8bdf66f7977a7e374052327ec28c6906cbe25967", + "ff68db24b34fa62c78ac5ffeeaf19533afaedf651fb6a08384e46787f6ce94be", + "50bb859aa2dde938cc49ec7a", + "06ff32e1f7b29753a727d7927b25c2dd175aca47751462d37a2039023ec6b5a6", + ) +} + +func TestMessageKeyGeneration005(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "8c2e1dd3792802f1f9f7842e0323e5d52ad7472daf360f26e15f97290173605d", + "2f9daeda8683fdeede81adac247c63cc7671fa817a1fd47352e95d9487989d8b", + "400224ba67fc2f1b76736916", + "465c05302aeeb514e41c13ed6405297e261048cfb75a6f851ffa5b445b746e4b", + ) +} + +func TestMessageKeyGeneration006(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "05c28bf3d834fa4af8143bf5201a856fa5fac1a3aee58f4c93a764fc2f722367", + "1e3d45777025a035be566d80fd580def73ed6f7c043faec2c8c1c690ad31c110", + "021905b1ea3afc17cb9bf96f", + "74a6e481a89dcd130aaeb21060d7ec97ad30f0007d2cae7b1b11256cc70dfb81", + ) +} + +func TestMessageKeyGeneration007(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "5e043fb153227866e75a06d60185851bc90273bfb93342f6632a728e18a07a17", + "1ea72c9293841e7737c71567d8120145a58991aaa1c436ef77bf7adb83f882f1", + "72f69a5a5f795465cee59da8", + "e9daa1a1e9a266ecaa14e970a84bce3fbbf329079bbccda626582b4e66a0d4c9", + ) +} + +func TestMessageKeyGeneration009(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "7be7338eaf06a87e274244847fe7a97f5c6a91f44adc18fcc3e411ad6f786dbf", + "881e7968a1f0c2c80742ee03cd49ea587e13f22699730f1075ade01931582bf6", + "6e69be92d61c04a276021565", + "901afe79e74b19967c8829af23617d7d0ffbf1b57190c096855c6a03523a971b", + ) +} + +func TestMessageKeyGeneration010(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "94571c8d590905bad7becd892832b472f2aa5212894b6ce96e5ba719c178d976", + "f80873dd48466cb12d46364a97b8705c01b9b4230cb3ec3415a6b9551dc42eef", + "3dda53569cfcb7fac1805c35", + "e9fc264345e2839a181affebc27d2f528756e66a5f87b04bf6c5f1997047051e", + ) +} + +func TestMessageKeyGeneration011(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "13a6ee974b1fd759135a2c2010e3cdda47081c78e771125e4f0c382f0284a8cb", + "bc5fb403b0bed0d84cf1db872b6522072aece00363178c98ad52178d805fca85", + "65064239186e50304cc0f156", + "e872d320dde4ed3487958a8e43b48aabd3ced92bc24bb8ff1ccb57b590d9701a", + ) +} + +func TestMessageKeyGeneration012(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "082fecdb85f358367b049b08be0e82627ae1d8edb0f27327ccb593aa2613b814", + "1fbdb1cf6f6ea816349baf697932b36107803de98fcd805ebe9849b8ad0e6a45", + "2e605e1d825a3eaeb613db9c", + "fae910f591cf3c7eb538c598583abad33bc0a03085a96ca4ea3a08baf17c0eec", + ) +} + +func TestMessageKeyGeneration013(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "4c19020c74932c30ec6b2d8cd0d5bb80bd0fc87da3d8b4859d2fb003810afd03", + "1ab9905a0189e01cda82f843d226a82a03c4f5b6dbea9b22eb9bc953ba1370d4", + "cbb2530ea653766e5a37a83a", + "267f68acac01ac7b34b675e36c2cef5e7b7a6b697214add62a491bedd6efc178", + ) +} + +func TestMessageKeyGeneration014(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "67723a3381497b149ce24814eddd10c4c41a1e37e75af161930e6b9601afd0ff", + "9ecbd25e7e2e6c97b8c27d376dcc8c5679da96578557e4e21dba3a7ef4e4ac07", + "ef649fcf335583e8d45e3c2e", + "04dbbd812fa8226fdb45924c521a62e3d40a9e2b5806c1501efdeba75b006bf1", + ) +} + +func TestMessageKeyGeneration015(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "42063fe80b093e8619b1610972b4c3ab9e76c14fd908e642cd4997cafb30f36c", + "211c66531bbcc0efcdd0130f9f1ebc12a769105eb39608994bcb188fa6a73a4a", + "67803605a7e5010d0f63f8c8", + "e840e4e8921b57647369d121c5a19310648105dbdd008200ebf0d3b668704ff8", + ) +} + +func TestMessageKeyGeneration016(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "b5ac382a4be7ac03b554fe5f3043577b47ea2cd7cfc7e9ca010b1ffbb5cf1a58", + "b3b5f14f10074244ee42a3837a54309f33981c7232a8b16921e815e1f7d1bb77", + "4e62a0073087ed808be62469", + "c8efa10230b5ea11633816c1230ca05fa602ace80a7598916d83bae3d3d2ccd7", + ) +} + +func TestMessageKeyGeneration017(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "e9d1eba47dd7e6c1532dc782ff63125db83042bb32841db7eeafd528f3ea7af9", + "54241f68dc2e50e1db79e892c7c7a471856beeb8d51b7f4d16f16ab0645d2f1a", + "a963ed7dc29b7b1046820a1d", + "aba215c8634530dc21c70ddb3b3ee4291e0fa5fa79be0f85863747bde281c8b2", + ) +} + +func TestMessageKeyGeneration018(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "a94ecf8efeee9d7068de730fad8daf96694acb70901d762de39fa8a5039c3c49", + "c0565e9e201d2381a2368d7ffe60f555223874610d3d91fbbdf3076f7b1374dd", + "329bb3024461e84b2e1c489b", + "ac42445491f092481ce4fa33b1f2274700032db64e3a15014fbe8c28550f2fec", + ) +} + +func TestMessageKeyGeneration019(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "533605ea214e70c25e9a22f792f4b78b9f83a18ab2103687c8a0075919eaaa53", + "ab35a5e1e54d693ff023db8500d8d4e79ad8878c744e0eaec691e96e141d2325", + "653d759042b85194d4d8c0a7", + "b43628e37ba3c31ce80576f0a1f26d3a7c9361d29bb227433b66f49d44f167ba", + ) +} + +func TestMessageKeyGeneration020(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "7f38df30ceea1577cb60b355b4f5567ff4130c49e84fed34d779b764a9cc184c", + "a37d7f211b84a551a127ff40908974eb78415395d4f6f40324428e850e8c42a3", + "b822e2c959df32b3cb772a7c", + "1ba31764f01f69b5c89ded2d7c95828e8052c55f5d36f1cd535510d61ba77420", + ) +} + +func TestMessageKeyGeneration021(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "11b37f9dbc4d0185d1c26d5f4ed98637d7c9701fffa65a65839fa4126573a4e5", + "964f38d3a31158a5bfd28481247b18dd6e44d69f30ba2a40f6120c6d21d8a6ba", + "5f72c5b87c590bcd0f93b305", + "2fc4553e7cedc47f29690439890f9f19c1077ef3e9eaeef473d0711e04448918", + ) +} + +func TestMessageKeyGeneration022(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "8be790aa483d4cdd843189f71f135b3ec7e31f381312c8fe9f177aab2a48eafa", + "95c8c74d633721a131316309cf6daf0804d59eaa90ea998fc35bac3d2fbb7a94", + "409a7654c0e4bf8c2c6489be", + "21bb0b06eb2b460f8ab075f497efa9a01c9cf9146f1e3986c3bf9da5689b6dc4", + ) +} + +func TestMessageKeyGeneration023(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "19fd2a718ea084827d6bd73f509229ddf856732108b59fc01819f611419fd140", + "cc6714b9f5616c66143424e1413d520dae03b1a4bd202b82b0a89b0727f5cdc8", + "1b7fd2534f015a8f795d8f32", + "2bef39c4ce5c3c59b817e86351373d1554c98bc131c7e461ed19d96cfd6399a0", + ) +} + +func TestMessageKeyGeneration024(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "3c2acd893952b2f6d07d8aea76f545ca45961a93fe5757f6a5a80811d5e0255d", + "c8de6c878cb469278d0af894bc181deb6194053f73da5014c2b5d2c8db6f2056", + "6ffe4f1971b904a1b1a81b99", + "df1cd69dd3646fca15594284744d4211d70e7d8472e545d276421fbb79559fd4", + ) +} + +func TestMessageKeyGeneration025(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "7dbea4cead9ac91d4137f1c0a6eebb6ba0d1fb2cc46d829fbc75f8d86aca6301", + "c8e030f6aa680c3d0b597da9c92bb77c21c4285dd620c5889f9beba7446446b0", + "a9b5a67d081d3b42e737d16f", + "355a85f551bc3cce9a14461aa60994742c9bbb1c81a59ca102dc64e61726ab8e", + ) +} + +func TestMessageKeyGeneration026(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "45422e676cdae5f1071d3647d7a5f1f5adafb832668a578228aa1155a491f2f3", + "758437245f03a88e2c6a32807edfabff51a91c81ca2f389b0b46f2c97119ea90", + "263830a065af33d9c6c5aa1f", + "7c581cf3489e2de203a95106bfc0de3d4032e9d5b92b2b61fb444acd99037e17", + ) +} + +func TestMessageKeyGeneration027(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "babc0c03fad24107ad60678751f5db2678041ff0d28671ede8d65bdf7aa407e9", + "bd68a28bd48d9ffa3602db72c75662ac2848a0047a313d2ae2d6bc1ac153d7e9", + "d0f9d2a1ace6c758f594ffdd", + "eb435e3a642adfc9d59813051606fc21f81641afd58ea6641e2f5a9f123bb50a", + ) +} + +func TestMessageKeyGeneration028(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "7a1b8aac37d0d20b160291fad124ab697cfca53f82e326d78fef89b4b0ea8f83", + "9e97875b651a1d30d17d086d1e846778b7faad6fcbc12e08b3365d700f62e4fe", + "ccdaad5b3b7645be430992eb", + "6f2f55cf35174d75752f63c06cc7cbc8441759b142999ed2d5a6d09d263e1fc4", + ) +} + +func TestMessageKeyGeneration029(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "8370e4e32d7e680a83862cab0da6136ef607014d043e64cdf5ecc0c4e20b3d9a", + "1472bed5d19db9c546106de946e0649cd83cc9d4a66b087a65906e348dcf92e2", + "ed02dece5fc3a186f123420b", + "7b3f7739f49d30c6205a46b174f984bb6a9fc38e5ccfacef2dac04fcbd3b184e", + ) +} + +func TestMessageKeyGeneration030(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "9f1c5e8a29cd5677513c2e3a816551d6833ee54991eb3f00d5b68096fc8f0183", + "5e1a7544e4d4dafe55941fcbdf326f19b0ca37fc49c4d47e9eec7fb68cde4975", + "7d9acb0fdc174e3c220f40de", + "e265ab116fbbb86b2aefc089a0986a0f5b77eda50c7410404ad3b4f3f385c7a7", + ) +} + +func TestMessageKeyGeneration031(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "c385aa1c37c2bfd5cc35fcdbdf601034d39195e1cabff664ceb2b787c15d0225", + "06bf4e60677a13e54c4a38ab824d2ef79da22b690da2b82d0aa3e39a14ca7bdd", + "26b450612ca5e905b937e147", + "22208152be2b1f5f75e6bfcc1f87763d48bb7a74da1be3d102096f257207f8b3", + ) +} + +func TestMessageKeyGeneration032(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "3ff73528f88a50f9d35c0ddba4560bacee5b0462d0f4cb6e91caf41847040ce4", + "850c8a17a23aa761d279d9901015b2bbdfdff00adbf6bc5cf22bd44d24ecabc9", + "4a296a1fb0048e5020d3b129", + "b1bf49a533c4da9b1d629b7ff30882e12d37d49c19abd7b01b7807d75ee13806", + ) +} + +func TestMessageKeyGeneration033(t *testing.T) { + assertMessageKeyGeneration( + t, + "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "2dcf39b9d4c52f1cb9db2d516c43a7c6c3b8c401f6a4ac8f131a9e1059957036", + "17f8057e6156ba7cc5310d01eda8c40f9aa388f9fd1712deb9511f13ecc37d27", + "a8188daff807a1182200b39d", + "47b89da97f68d389867b5d8a2d7ba55715a30e3d88a3cc11f3646bc2af5580ef", + ) +} + +func TestMaxLength(t *testing.T) { + sk1 := keys.GeneratePrivateKey() + sk2 := keys.GeneratePrivateKey() + pub2, _ := keys.GetPublicKeyHex(string(sk2)) + salt := make([]byte, 32) + rand.Read(salt) + conversationKey, _ := GenerateConversationKeyFromHex(pub2, string(sk1)) + plaintext := strings.Repeat("a", MaxPlaintextSize) + plaintextBytes := []byte(plaintext) + encrypted, err := Encrypt( + plaintextBytes, conversationKey, WithCustomNonce(salt), + ) + if chk.E(err) { + t.Error(err) + } + + assertCryptPub( + t, + string(sk1), + pub2, + fmt.Sprintf("%x", conversationKey), + fmt.Sprintf("%x", salt), + plaintext, + string(encrypted), + ) +} + +func assertCryptPub( + t *testing.T, + sk1, pub2, conversationKey, salt, plaintext, expected string, +) { + var ( + k1, s, plaintextBytes, + actualBytes, expectedBytes, decrypted []byte + ok bool + err error + ) + k1, err = hex.Dec(conversationKey) + if ok = assert.NoErrorf( + t, err, "hex decode failed for conversation key: %v", err, + ); !ok { + return + } + if ok = assertConversationKeyGenerationPub( + t, sk1, pub2, conversationKey, + ); !ok { + return + } + s, err = hex.Dec(salt) + if ok = assert.NoErrorf( + t, err, "hex decode failed for salt: %v", err, + ); !ok { + return + } + plaintextBytes = []byte(plaintext) + actualBytes, err = Encrypt(plaintextBytes, k1, WithCustomNonce(s)) + if ok = assert.NoError(t, err, "encryption failed: %v", err); !ok { + return + } + expectedBytes = []byte(expected) + if ok = assert.Equalf( + t, string(expectedBytes), string(actualBytes), "wrong encryption", + ); !ok { + return + } + decrypted, err = Decrypt(expectedBytes, k1) + if ok = assert.NoErrorf(t, err, "decryption failed: %v", err); !ok { + return + } + assert.Equal(t, decrypted, plaintextBytes, "wrong decryption") +} diff --git a/pkg/crypto/keys/keys.go b/pkg/crypto/keys/keys.go new file mode 100644 index 0000000..954d890 --- /dev/null +++ b/pkg/crypto/keys/keys.go @@ -0,0 +1,83 @@ +// Package keys is a set of helpers for generating and converting public/secret +// keys to hex and back to binary. +package keys + +import ( + "bytes" + + "lol.mleku.dev/chk" + "next.orly.dev/pkg/crypto/ec/schnorr" + "next.orly.dev/pkg/crypto/p256k" + "next.orly.dev/pkg/encoders/hex" + "next.orly.dev/pkg/utils" +) + +// GeneratePrivateKey - deprecated, use GenerateSecretKeyHex +var GeneratePrivateKey = func() string { return GenerateSecretKeyHex() } + +// GenerateSecretKey creates a new secret key and returns the bytes of the secret. +func GenerateSecretKey() (skb []byte, err error) { + signer := &p256k.Signer{} + if err = signer.Generate(); chk.E(err) { + return + } + skb = signer.Sec() + return +} + +// GenerateSecretKeyHex generates a secret key and encodes the bytes as hex. +func GenerateSecretKeyHex() (sks string) { + skb, err := GenerateSecretKey() + if chk.E(err) { + return + } + return hex.Enc(skb) +} + +// GetPublicKeyHex generates a public key from a hex encoded secret key. +func GetPublicKeyHex(sk string) (pk string, err error) { + var b []byte + if b, err = hex.Dec(sk); chk.E(err) { + return + } + signer := &p256k.Signer{} + if err = signer.InitSec(b); chk.E(err) { + return + } + + return hex.Enc(signer.Pub()), nil +} + +// SecretBytesToPubKeyHex generates a public key from secret key bytes. +func SecretBytesToPubKeyHex(skb []byte) (pk string, err error) { + signer := &p256k.Signer{} + if err = signer.InitSec(skb); chk.E(err) { + return + } + return hex.Enc(signer.Pub()), nil +} + +// IsValid32ByteHex checks that a hex string is a valid 32 bytes lower case hex encoded value as +// per nostr NIP-01 spec. +func IsValid32ByteHex[V []byte | string](pk V) bool { + if utils.FastEqual(bytes.ToLower([]byte(pk)), []byte(pk)) { + return false + } + var err error + dec := make([]byte, 32) + if _, err = hex.DecBytes(dec, []byte(pk)); chk.E(err) { + } + return len(dec) == 32 +} + +// IsValidPublicKey checks that a hex encoded public key is a valid BIP-340 public key. +func IsValidPublicKey[V []byte | string](pk V) bool { + v, _ := hex.Dec(string(pk)) + _, err := schnorr.ParsePubKey(v) + return err == nil +} + +// HexPubkeyToBytes decodes a pubkey from hex encoded string/bytes. +func HexPubkeyToBytes[V []byte | string](hpk V) (pkb []byte, err error) { + return hex.DecAppend(nil, []byte(hpk)) +} diff --git a/pkg/encoders/tag/tag.go b/pkg/encoders/tag/tag.go index 722e4b5..510b877 100644 --- a/pkg/encoders/tag/tag.go +++ b/pkg/encoders/tag/tag.go @@ -8,7 +8,7 @@ import ( "lol.mleku.dev/errorf" "next.orly.dev/pkg/encoders/text" - utils "next.orly.dev/pkg/utils" + "next.orly.dev/pkg/utils" ) // The tag position meanings, so they are clear when reading. diff --git a/pkg/encoders/tag/tags.go b/pkg/encoders/tag/tags.go index 19003ed..e2b2b10 100644 --- a/pkg/encoders/tag/tags.go +++ b/pkg/encoders/tag/tags.go @@ -166,3 +166,11 @@ func (s *S) GetAll(t []byte) (all []*T) { } return } + +func (s *S) GetTagElement(i int) (t *T) { + if s == nil || len(*s) < i { + return + } + t = (*s)[i] + return +} diff --git a/pkg/protocol/nwc/README.md b/pkg/protocol/nwc/README.md new file mode 100644 index 0000000..46fa38c --- /dev/null +++ b/pkg/protocol/nwc/README.md @@ -0,0 +1,56 @@ +# NWC Client + +Nostr Wallet Connect (NIP-47) client implementation. + +## Usage + +```go +import "orly.dev/pkg/protocol/nwc" + +// Create client from NWC connection URI +client, err := nwc.NewClient("nostr+walletconnect://...") +if err != nil { + log.Fatal(err) +} + +// Make requests +var info map[string]any +err = client.Request(ctx, "get_info", nil, &info) + +var balance map[string]any +err = client.Request(ctx, "get_balance", nil, &balance) + +var invoice map[string]any +params := map[string]any{"amount": 1000, "description": "test"} +err = client.Request(ctx, "make_invoice", params, &invoice) +``` + +## Methods + +- `get_info` - Get wallet info +- `get_balance` - Get wallet balance +- `make_invoice` - Create invoice +- `lookup_invoice` - Check invoice status +- `pay_invoice` - Pay invoice + +## Payment Notifications + +```go +// Subscribe to payment notifications +err = client.SubscribeNotifications(ctx, func(notificationType string, notification map[string]any) error { + if notificationType == "payment_received" { + amount := notification["amount"].(float64) + description := notification["description"].(string) + // Process payment... + } + return nil +}) +``` + +## Features + +- NIP-44 encryption +- Event signing +- Relay communication +- Payment notifications +- Error handling \ No newline at end of file diff --git a/pkg/protocol/nwc/client.go b/pkg/protocol/nwc/client.go new file mode 100644 index 0000000..ecdc820 --- /dev/null +++ b/pkg/protocol/nwc/client.go @@ -0,0 +1,265 @@ +package nwc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + "next.orly.dev/pkg/crypto/encryption" + "next.orly.dev/pkg/encoders/event" + "next.orly.dev/pkg/encoders/filter" + "next.orly.dev/pkg/encoders/hex" + "next.orly.dev/pkg/encoders/kind" + "next.orly.dev/pkg/encoders/tag" + "next.orly.dev/pkg/encoders/timestamp" + "next.orly.dev/pkg/interfaces/signer" + "next.orly.dev/pkg/protocol/ws" + "next.orly.dev/pkg/utils/values" +) + +type Client struct { + relay string + clientSecretKey signer.I + walletPublicKey []byte + conversationKey []byte +} + +func NewClient(connectionURI string) (cl *Client, err error) { + var parts *ConnectionParams + if parts, err = ParseConnectionURI(connectionURI); chk.E(err) { + return + } + cl = &Client{ + relay: parts.relay, + clientSecretKey: parts.clientSecretKey, + walletPublicKey: parts.walletPublicKey, + conversationKey: parts.conversationKey, + } + return +} + +func (cl *Client) Request( + c context.Context, method string, params, result any, +) (err error) { + ctx, cancel := context.WithTimeout(c, 10*time.Second) + defer cancel() + + request := map[string]any{"method": method} + if params != nil { + request["params"] = params + } + + var req []byte + if req, err = json.Marshal(request); chk.E(err) { + return + } + + var content []byte + if content, err = encryption.Encrypt(req, cl.conversationKey); chk.E(err) { + return + } + + ev := &event.E{ + Content: content, + CreatedAt: time.Now().Unix(), + Kind: 23194, + Tags: tag.NewS( + tag.NewFromAny("encryption", "nip44_v2"), + tag.NewFromAny("p", hex.Enc(cl.walletPublicKey)), + ), + } + + if err = ev.Sign(cl.clientSecretKey); chk.E(err) { + return + } + + var rc *ws.Client + if rc, err = ws.RelayConnect(ctx, cl.relay); chk.E(err) { + return + } + defer rc.Close() + + var sub *ws.Subscription + if sub, err = rc.Subscribe( + ctx, filter.NewS( + &filter.F{ + Limit: values.ToUintPointer(1), + Kinds: kind.NewS(kind.New(23195)), + Since: ×tamp.T{V: time.Now().Unix()}, + }, + ), + ); chk.E(err) { + return + } + defer sub.Unsub() + + if err = rc.Publish(ctx, ev); chk.E(err) { + return fmt.Errorf("publish failed: %w", err) + } + + select { + case <-ctx.Done(): + return fmt.Errorf("no response from wallet (connection may be inactive)") + case e := <-sub.Events: + if e == nil { + return fmt.Errorf("subscription closed (wallet connection inactive)") + } + if len(e.Content) == 0 { + return fmt.Errorf("empty response content") + } + var raw []byte + if raw, err = encryption.Decrypt( + e.Content, cl.conversationKey, + ); chk.E(err) { + return fmt.Errorf( + "decryption failed (invalid conversation key): %w", err, + ) + } + + var resp map[string]any + if err = json.Unmarshal(raw, &resp); chk.E(err) { + return + } + + if errData, ok := resp["error"].(map[string]any); ok { + code, _ := errData["code"].(string) + msg, _ := errData["message"].(string) + return fmt.Errorf("%s: %s", code, msg) + } + + if result != nil && resp["result"] != nil { + var resultBytes []byte + if resultBytes, err = json.Marshal(resp["result"]); chk.E(err) { + return + } + if err = json.Unmarshal(resultBytes, result); chk.E(err) { + return + } + } + } + + return +} + +// NotificationHandler is a callback for handling NWC notifications +type NotificationHandler func( + notificationType string, notification map[string]any, +) error + +// SubscribeNotifications subscribes to NWC notification events (kinds 23197/23196) +// and handles them with the provided callback. It maintains a persistent connection +// with auto-reconnection on disconnect. +func (cl *Client) SubscribeNotifications( + c context.Context, handler NotificationHandler, +) (err error) { + delay := time.Second + for { + if err = cl.subscribeNotificationsOnce(c, handler); err != nil { + if errors.Is(err, context.Canceled) { + return err + } + select { + case <-time.After(delay): + if delay < 30*time.Second { + delay *= 2 + } + case <-c.Done(): + return context.Canceled + } + continue + } + delay = time.Second + } +} + +// subscribeNotificationsOnce performs a single subscription attempt +func (cl *Client) subscribeNotificationsOnce( + c context.Context, handler NotificationHandler, +) (err error) { + // Connect to relay + var rc *ws.Client + if rc, err = ws.RelayConnect(c, cl.relay); chk.E(err) { + return fmt.Errorf("relay connection failed: %w", err) + } + defer rc.Close() + + // Subscribe to notification events filtered by "p" tag + // Support both NIP-44 (kind 23197) and legacy NIP-04 (kind 23196) + var sub *ws.Subscription + if sub, err = rc.Subscribe( + c, filter.NewS( + &filter.F{ + Kinds: kind.NewS(kind.New(23197), kind.New(23196)), + Tags: tag.NewS( + tag.NewFromAny("p", hex.Enc(cl.clientSecretKey.Pub())), + ), + Since: ×tamp.T{V: time.Now().Unix()}, + }, + ), + ); chk.E(err) { + return fmt.Errorf("subscription failed: %w", err) + } + defer sub.Unsub() + + log.I.F( + "subscribed to NWC notifications from wallet %s", + hex.Enc(cl.walletPublicKey), + ) + + // Process notification events + for { + select { + case <-c.Done(): + return context.Canceled + case ev := <-sub.Events: + if ev == nil { + // Channel closed, subscription ended + return fmt.Errorf("subscription closed") + } + + // Process the notification event + if err := cl.processNotificationEvent(ev, handler); err != nil { + log.E.F("error processing notification: %v", err) + // Continue processing other notifications even if one fails + } + } + } +} + +// processNotificationEvent decrypts and processes a single notification event +func (cl *Client) processNotificationEvent( + ev *event.E, handler NotificationHandler, +) (err error) { + // Decrypt the notification content + var decrypted []byte + if decrypted, err = encryption.Decrypt( + ev.Content, cl.conversationKey, + ); err != nil { + return fmt.Errorf("failed to decrypt notification: %w", err) + } + + // Parse the notification JSON + var notification map[string]any + if err = json.Unmarshal(decrypted, ¬ification); err != nil { + return fmt.Errorf("failed to parse notification JSON: %w", err) + } + + // Extract notification type + notificationType, ok := notification["notification_type"].(string) + if !ok { + return fmt.Errorf("missing or invalid notification_type") + } + + // Extract notification data + notificationData, ok := notification["notification"].(map[string]any) + if !ok { + return fmt.Errorf("missing or invalid notification data") + } + + // Route to type-specific handler + return handler(notificationType, notificationData) +} diff --git a/pkg/protocol/nwc/crypto_test.go b/pkg/protocol/nwc/crypto_test.go new file mode 100644 index 0000000..314db5c --- /dev/null +++ b/pkg/protocol/nwc/crypto_test.go @@ -0,0 +1,188 @@ +package nwc_test + +import ( + "encoding/json" + "testing" + "time" + + "next.orly.dev/pkg/crypto/encryption" + "next.orly.dev/pkg/crypto/p256k" + "next.orly.dev/pkg/encoders/event" + "next.orly.dev/pkg/encoders/hex" + "next.orly.dev/pkg/encoders/tag" + "next.orly.dev/pkg/protocol/nwc" + "next.orly.dev/pkg/utils" +) + +func TestNWCConversationKey(t *testing.T) { + secret := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + walletPubkey := "816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b" + + uri := "nostr+walletconnect://" + walletPubkey + "?relay=wss://relay.getalby.com/v1&secret=" + secret + + parts, err := nwc.ParseConnectionURI(uri) + if err != nil { + t.Fatal(err) + } + + // Validate conversation key was generated + convKey := parts.GetConversationKey() + if len(convKey) == 0 { + t.Fatal("conversation key should not be empty") + } + + // Validate wallet public key + walletKey := parts.GetWalletPublicKey() + if len(walletKey) == 0 { + t.Fatal("wallet public key should not be empty") + } + + expected, err := hex.Dec(walletPubkey) + if err != nil { + t.Fatal(err) + } + + if len(walletKey) != len(expected) { + t.Fatal("wallet public key length mismatch") + } + + for i := range walletKey { + if walletKey[i] != expected[i] { + t.Fatal("wallet public key mismatch") + } + } + + // Test passed +} + +func TestNWCEncryptionDecryption(t *testing.T) { + secret := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + walletPubkey := "816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b" + + uri := "nostr+walletconnect://" + walletPubkey + "?relay=wss://relay.getalby.com/v1&secret=" + secret + + parts, err := nwc.ParseConnectionURI(uri) + if err != nil { + t.Fatal(err) + } + + convKey := parts.GetConversationKey() + testMessage := `{"method":"get_info","params":null}` + + // Test encryption + encrypted, err := encryption.Encrypt([]byte(testMessage), convKey) + if err != nil { + t.Fatalf("encryption failed: %v", err) + } + + if len(encrypted) == 0 { + t.Fatal("encrypted message should not be empty") + } + + // Test decryption + decrypted, err := encryption.Decrypt(encrypted, convKey) + if err != nil { + t.Fatalf("decryption failed: %v", err) + } + + if string(decrypted) != testMessage { + t.Fatalf( + "decrypted message mismatch: got %s, want %s", string(decrypted), + testMessage, + ) + } + + // Test passed +} + +func TestNWCEventCreation(t *testing.T) { + secretBytes, err := hex.Dec("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + if err != nil { + t.Fatal(err) + } + + clientKey := &p256k.Signer{} + if err := clientKey.InitSec(secretBytes); err != nil { + t.Fatal(err) + } + + walletPubkey, err := hex.Dec("816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b") + if err != nil { + t.Fatal(err) + } + + convKey, err := encryption.GenerateConversationKeyWithSigner( + clientKey, walletPubkey, + ) + if err != nil { + t.Fatal(err) + } + + request := map[string]any{"method": "get_info"} + reqBytes, err := json.Marshal(request) + if err != nil { + t.Fatal(err) + } + + encrypted, err := encryption.Encrypt(reqBytes, convKey) + if err != nil { + t.Fatal(err) + } + + // Create NWC event + ev := &event.E{ + Content: encrypted, + CreatedAt: time.Now().Unix(), + Kind: 23194, + Tags: tag.NewS( + tag.NewFromAny("encryption", "nip44_v2"), + tag.NewFromAny("p", hex.Enc(walletPubkey)), + ), + } + + if err := ev.Sign(clientKey); err != nil { + t.Fatalf("event signing failed: %v", err) + } + + // Validate event structure + if len(ev.Content) == 0 { + t.Fatal("event content should not be empty") + } + + if len(ev.ID) == 0 { + t.Fatal("event should have ID after signing") + } + + if len(ev.Sig) == 0 { + t.Fatal("event should have signature after signing") + } + + // Validate tags + hasEncryption := false + hasP := false + for i := 0; i < ev.Tags.Len(); i++ { + tag := ev.Tags.GetTagElement(i) + if tag.Len() >= 2 { + if utils.FastEqual( + tag.T[0], "encryption", + ) && utils.FastEqual(tag.T[1], "nip44_v2") { + hasEncryption = true + } + if utils.FastEqual( + tag.T[0], "p", + ) && utils.FastEqual(tag.T[1], hex.Enc(walletPubkey)) { + hasP = true + } + } + } + + if !hasEncryption { + t.Fatal("event missing encryption tag") + } + + if !hasP { + t.Fatal("event missing p tag") + } + + // Test passed +} diff --git a/pkg/protocol/nwc/mock_wallet_service.go b/pkg/protocol/nwc/mock_wallet_service.go new file mode 100644 index 0000000..761876c --- /dev/null +++ b/pkg/protocol/nwc/mock_wallet_service.go @@ -0,0 +1,495 @@ +package nwc + +import ( + "context" + "crypto/rand" + "encoding/json" + "fmt" + "sync" + "time" + + "lol.mleku.dev/chk" + "next.orly.dev/pkg/crypto/encryption" + "next.orly.dev/pkg/crypto/p256k" + "next.orly.dev/pkg/encoders/event" + "next.orly.dev/pkg/encoders/filter" + "next.orly.dev/pkg/encoders/hex" + "next.orly.dev/pkg/encoders/kind" + "next.orly.dev/pkg/encoders/tag" + "next.orly.dev/pkg/encoders/timestamp" + "next.orly.dev/pkg/interfaces/signer" + "next.orly.dev/pkg/protocol/ws" +) + +// MockWalletService implements a mock NIP-47 wallet service for testing +type MockWalletService struct { + relay string + walletSecretKey signer.I + walletPublicKey []byte + client *ws.Client + ctx context.Context + cancel context.CancelFunc + balance int64 // in satoshis + balanceMutex sync.RWMutex + connectedClients map[string][]byte // pubkey -> conversation key + clientsMutex sync.RWMutex +} + +// NewMockWalletService creates a new mock wallet service +func NewMockWalletService( + relay string, initialBalance int64, +) (service *MockWalletService, err error) { + // Generate wallet keypair + walletKey := &p256k.Signer{} + if err = walletKey.Generate(); chk.E(err) { + return + } + + ctx, cancel := context.WithCancel(context.Background()) + + service = &MockWalletService{ + relay: relay, + walletSecretKey: walletKey, + walletPublicKey: walletKey.Pub(), + ctx: ctx, + cancel: cancel, + balance: initialBalance, + connectedClients: make(map[string][]byte), + } + return +} + +// Start begins the mock wallet service +func (m *MockWalletService) Start() (err error) { + // Connect to relay + if m.client, err = ws.RelayConnect(m.ctx, m.relay); chk.E(err) { + return fmt.Errorf("failed to connect to relay: %w", err) + } + + // Publish wallet info event + if err = m.publishWalletInfo(); chk.E(err) { + return fmt.Errorf("failed to publish wallet info: %w", err) + } + + // Subscribe to request events + if err = m.subscribeToRequests(); chk.E(err) { + return fmt.Errorf("failed to subscribe to requests: %w", err) + } + + return +} + +// Stop stops the mock wallet service +func (m *MockWalletService) Stop() { + if m.cancel != nil { + m.cancel() + } + if m.client != nil { + m.client.Close() + } +} + +// GetWalletPublicKey returns the wallet's public key +func (m *MockWalletService) GetWalletPublicKey() []byte { + return m.walletPublicKey +} + +// publishWalletInfo publishes the NIP-47 info event (kind 13194) +func (m *MockWalletService) publishWalletInfo() (err error) { + capabilities := []string{ + "get_info", + "get_balance", + "make_invoice", + "pay_invoice", + } + + info := map[string]any{ + "capabilities": capabilities, + "notifications": []string{"payment_received", "payment_sent"}, + } + + var content []byte + if content, err = json.Marshal(info); chk.E(err) { + return + } + + ev := &event.E{ + Content: content, + CreatedAt: time.Now().Unix(), + Kind: 13194, + Tags: tag.NewS(), + } + + if err = ev.Sign(m.walletSecretKey); chk.E(err) { + return + } + + return m.client.Publish(m.ctx, ev) +} + +// subscribeToRequests subscribes to NWC request events (kind 23194) +func (m *MockWalletService) subscribeToRequests() (err error) { + var sub *ws.Subscription + if sub, err = m.client.Subscribe( + m.ctx, filter.NewS( + &filter.F{ + Kinds: kind.NewS(kind.New(23194)), + Tags: tag.NewS( + tag.NewFromAny("p", hex.Enc(m.walletPublicKey)), + ), + Since: ×tamp.T{V: time.Now().Unix()}, + }, + ), + ); chk.E(err) { + return + } + + // Handle incoming request events + go m.handleRequestEvents(sub) + return +} + +// handleRequestEvents processes incoming NWC request events +func (m *MockWalletService) handleRequestEvents(sub *ws.Subscription) { + for { + select { + case <-m.ctx.Done(): + return + case ev := <-sub.Events: + if ev == nil { + continue + } + if err := m.processRequestEvent(ev); chk.E(err) { + fmt.Printf("Error processing request event: %v\n", err) + } + } + } +} + +// processRequestEvent processes a single NWC request event +func (m *MockWalletService) processRequestEvent(ev *event.E) (err error) { + // Get client pubkey from event + clientPubkey := ev.Pubkey + clientPubkeyHex := hex.Enc(clientPubkey) + + // Generate or get conversation key + var conversationKey []byte + m.clientsMutex.Lock() + if existingKey, exists := m.connectedClients[clientPubkeyHex]; exists { + conversationKey = existingKey + } else { + if conversationKey, err = encryption.GenerateConversationKeyWithSigner( + m.walletSecretKey, clientPubkey, + ); chk.E(err) { + m.clientsMutex.Unlock() + return + } + m.connectedClients[clientPubkeyHex] = conversationKey + } + m.clientsMutex.Unlock() + + // Decrypt request content + var decrypted []byte + if decrypted, err = encryption.Decrypt( + ev.Content, conversationKey, + ); chk.E(err) { + return + } + + var request map[string]any + if err = json.Unmarshal(decrypted, &request); chk.E(err) { + return + } + + method, ok := request["method"].(string) + if !ok { + return fmt.Errorf("invalid method") + } + + params := request["params"] + + // Process the method + var result any + if result, err = m.processMethod(method, params); chk.E(err) { + // Send error response + return m.sendErrorResponse( + clientPubkey, conversationKey, "INTERNAL", err.Error(), + ) + } + + // Send success response + return m.sendSuccessResponse(clientPubkey, conversationKey, result) +} + +// processMethod handles the actual NWC method execution +func (m *MockWalletService) processMethod( + method string, params any, +) (result any, err error) { + switch method { + case "get_info": + return m.getInfo() + case "get_balance": + return m.getBalance() + case "make_invoice": + return m.makeInvoice(params) + case "pay_invoice": + return m.payInvoice(params) + default: + err = fmt.Errorf("unsupported method: %s", method) + return + } +} + +// getInfo returns wallet information +func (m *MockWalletService) getInfo() (result map[string]any, err error) { + result = map[string]any{ + "alias": "Mock Wallet", + "color": "#3399FF", + "pubkey": hex.Enc(m.walletPublicKey), + "network": "mainnet", + "block_height": 850000, + "block_hash": "0000000000000000000123456789abcdef", + "methods": []string{ + "get_info", "get_balance", "make_invoice", "pay_invoice", + }, + } + return +} + +// getBalance returns the current wallet balance +func (m *MockWalletService) getBalance() (result map[string]any, err error) { + m.balanceMutex.RLock() + balance := m.balance + m.balanceMutex.RUnlock() + + result = map[string]any{ + "balance": balance * 1000, // convert to msats + } + return +} + +// makeInvoice creates a Lightning invoice +func (m *MockWalletService) makeInvoice(params any) ( + result map[string]any, err error, +) { + paramsMap, ok := params.(map[string]any) + if !ok { + err = fmt.Errorf("invalid params") + return + } + + amount, ok := paramsMap["amount"].(float64) + if !ok { + err = fmt.Errorf("missing or invalid amount") + return + } + + description := "" + if desc, ok := paramsMap["description"].(string); ok { + description = desc + } + + paymentHash := make([]byte, 32) + rand.Read(paymentHash) + + // Generate a fake bolt11 invoice + bolt11 := fmt.Sprintf("lnbc%dm1pwxxxxxxx", int64(amount/1000)) + + result = map[string]any{ + "type": "incoming", + "invoice": bolt11, + "description": description, + "payment_hash": hex.Enc(paymentHash), + "amount": int64(amount), + "created_at": time.Now().Unix(), + "expires_at": time.Now().Add(24 * time.Hour).Unix(), + } + return +} + +// payInvoice pays a Lightning invoice +func (m *MockWalletService) payInvoice(params any) ( + result map[string]any, err error, +) { + paramsMap, ok := params.(map[string]any) + if !ok { + err = fmt.Errorf("invalid params") + return + } + + invoice, ok := paramsMap["invoice"].(string) + if !ok { + err = fmt.Errorf("missing or invalid invoice") + return + } + + // Mock payment amount (would parse from invoice in real implementation) + amount := int64(1000) // 1000 msats + + // Check balance + m.balanceMutex.Lock() + if m.balance*1000 < amount { + m.balanceMutex.Unlock() + err = fmt.Errorf("insufficient balance") + return + } + m.balance -= amount / 1000 + m.balanceMutex.Unlock() + + preimage := make([]byte, 32) + rand.Read(preimage) + + result = map[string]any{ + "type": "outgoing", + "invoice": invoice, + "amount": amount, + "preimage": hex.Enc(preimage), + "created_at": time.Now().Unix(), + } + + // Emit payment_sent notification + go m.emitPaymentNotification("payment_sent", result) + return +} + +// sendSuccessResponse sends a successful NWC response +func (m *MockWalletService) sendSuccessResponse( + clientPubkey []byte, conversationKey []byte, result any, +) (err error) { + response := map[string]any{ + "result": result, + } + + var responseBytes []byte + if responseBytes, err = json.Marshal(response); chk.E(err) { + return + } + + return m.sendEncryptedResponse(clientPubkey, conversationKey, responseBytes) +} + +// sendErrorResponse sends an error NWC response +func (m *MockWalletService) sendErrorResponse( + clientPubkey []byte, conversationKey []byte, code, message string, +) (err error) { + response := map[string]any{ + "error": map[string]any{ + "code": code, + "message": message, + }, + } + + var responseBytes []byte + if responseBytes, err = json.Marshal(response); chk.E(err) { + return + } + + return m.sendEncryptedResponse(clientPubkey, conversationKey, responseBytes) +} + +// sendEncryptedResponse sends an encrypted response event (kind 23195) +func (m *MockWalletService) sendEncryptedResponse( + clientPubkey []byte, conversationKey []byte, content []byte, +) (err error) { + var encrypted []byte + if encrypted, err = encryption.Encrypt( + content, conversationKey, + ); chk.E(err) { + return + } + + ev := &event.E{ + Content: encrypted, + CreatedAt: time.Now().Unix(), + Kind: 23195, + Tags: tag.NewS( + tag.NewFromAny("encryption", "nip44_v2"), + tag.NewFromAny("p", hex.Enc(clientPubkey)), + ), + } + + if err = ev.Sign(m.walletSecretKey); chk.E(err) { + return + } + + return m.client.Publish(m.ctx, ev) +} + +// emitPaymentNotification emits a payment notification (kind 23197) +func (m *MockWalletService) emitPaymentNotification( + notificationType string, paymentData map[string]any, +) (err error) { + notification := map[string]any{ + "notification_type": notificationType, + "notification": paymentData, + } + + var content []byte + if content, err = json.Marshal(notification); chk.E(err) { + return + } + + // Send notification to all connected clients + m.clientsMutex.RLock() + defer m.clientsMutex.RUnlock() + + for clientPubkeyHex, conversationKey := range m.connectedClients { + var clientPubkey []byte + if clientPubkey, err = hex.Dec(clientPubkeyHex); chk.E(err) { + continue + } + + var encrypted []byte + if encrypted, err = encryption.Encrypt( + content, conversationKey, + ); chk.E(err) { + continue + } + + ev := &event.E{ + Content: encrypted, + CreatedAt: time.Now().Unix(), + Kind: 23197, + Tags: tag.NewS( + tag.NewFromAny("encryption", "nip44_v2"), + tag.NewFromAny("p", hex.Enc(clientPubkey)), + ), + } + + if err = ev.Sign(m.walletSecretKey); chk.E(err) { + continue + } + + m.client.Publish(m.ctx, ev) + } + return +} + +// SimulateIncomingPayment simulates an incoming payment for testing +func (m *MockWalletService) SimulateIncomingPayment( + pubkey []byte, amount int64, description string, +) (err error) { + // Add to balance + m.balanceMutex.Lock() + m.balance += amount / 1000 // convert msats to sats + m.balanceMutex.Unlock() + + paymentHash := make([]byte, 32) + rand.Read(paymentHash) + + preimage := make([]byte, 32) + rand.Read(preimage) + + paymentData := map[string]any{ + "type": "incoming", + "invoice": fmt.Sprintf("lnbc%dm1pwxxxxxxx", amount/1000), + "description": description, + "amount": amount, + "payment_hash": hex.Enc(paymentHash), + "preimage": hex.Enc(preimage), + "created_at": time.Now().Unix(), + } + + // Emit payment_received notification + return m.emitPaymentNotification("payment_received", paymentData) +} diff --git a/pkg/protocol/nwc/nwc_test.go b/pkg/protocol/nwc/nwc_test.go new file mode 100644 index 0000000..60115ed --- /dev/null +++ b/pkg/protocol/nwc/nwc_test.go @@ -0,0 +1,176 @@ +package nwc_test + +import ( + "context" + "testing" + "time" + + "next.orly.dev/pkg/protocol/nwc" + "next.orly.dev/pkg/protocol/ws" +) + +func TestNWCClientCreation(t *testing.T) { + uri := "nostr+walletconnect://816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b?relay=wss://relay.getalby.com/v1&secret=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + c, err := nwc.NewClient(uri) + if err != nil { + t.Fatal(err) + } + + if c == nil { + t.Fatal("client should not be nil") + } +} + +func TestNWCInvalidURI(t *testing.T) { + invalidURIs := []string{ + "invalid://test", + "nostr+walletconnect://", + "nostr+walletconnect://invalid", + "nostr+walletconnect://816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b", + "nostr+walletconnect://816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b?relay=invalid", + } + + for _, uri := range invalidURIs { + _, err := nwc.NewClient(uri) + if err == nil { + t.Fatalf("expected error for invalid URI: %s", uri) + } + } +} + +func TestNWCRelayConnection(t *testing.T) { + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + + rc, err := ws.RelayConnect(ctx, "wss://relay.getalby.com/v1") + if err != nil { + t.Fatalf("relay connection failed: %v", err) + } + defer rc.Close() + + t.Log("relay connection successful") +} + +func TestNWCRequestTimeout(t *testing.T) { + uri := "nostr+walletconnect://816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b?relay=wss://relay.getalby.com/v1&secret=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + c, err := nwc.NewClient(uri) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) + defer cancel() + + var r map[string]any + err = c.Request(ctx, "get_info", nil, &r) + + if err == nil { + t.Log("wallet responded") + return + } + + expectedErrors := []string{ + "no response from wallet", + "subscription closed", + "timeout waiting for response", + "context deadline exceeded", + } + + errorFound := false + for _, expected := range expectedErrors { + if contains(err.Error(), expected) { + errorFound = true + break + } + } + + if !errorFound { + t.Fatalf("unexpected error: %v", err) + } + + t.Logf("proper timeout handling: %v", err) +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && + (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || + findInString(s, substr)))) +} + +func findInString(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func TestNWCEncryption(t *testing.T) { + uri := "nostr+walletconnect://816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b?relay=wss://relay.getalby.com/v1&secret=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + c, err := nwc.NewClient(uri) + if err != nil { + t.Fatal(err) + } + + // We can't directly access private fields, but we can test the client creation + // check conversation key generation + if c == nil { + t.Fatal("client creation should succeed with valid URI") + } + + // Test passed +} + +func TestNWCEventFormat(t *testing.T) { + uri := "nostr+walletconnect://816fd7f1d000ae81a3da251c91866fc47f4bcd6ce36921e6d46773c32f1d548b?relay=wss://relay.getalby.com/v1&secret=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + c, err := nwc.NewClient(uri) + if err != nil { + t.Fatal(err) + } + + // Test client creation + // The Request method will create proper NWC events with: + // - Kind 23194 for requests + // - Proper encryption tag + // - Signed with client key + + ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second) + defer cancel() + + var r map[string]any + err = c.Request(ctx, "get_info", nil, &r) + + // We expect this to fail due to inactive connection, but it should fail + // after creating and sending NWC event + if err == nil { + t.Log("wallet responded") + return + } + + // Verify it failed for the right reason (connection/response issue, not formatting) + validFailures := []string{ + "subscription closed", + "no response from wallet", + "context deadline exceeded", + "timeout waiting for response", + } + + validFailure := false + for _, failure := range validFailures { + if contains(err.Error(), failure) { + validFailure = true + break + } + } + + if !validFailure { + t.Fatalf("unexpected error type (suggests formatting issue): %v", err) + } + + // Test passed +} diff --git a/pkg/protocol/nwc/uri.go b/pkg/protocol/nwc/uri.go new file mode 100644 index 0000000..913fbb9 --- /dev/null +++ b/pkg/protocol/nwc/uri.go @@ -0,0 +1,81 @@ +package nwc + +import ( + "errors" + "net/url" + + "lol.mleku.dev/chk" + "next.orly.dev/pkg/crypto/encryption" + "next.orly.dev/pkg/crypto/p256k" + "next.orly.dev/pkg/interfaces/signer" +) + +type ConnectionParams struct { + clientSecretKey signer.I + walletPublicKey []byte + conversationKey []byte + relay string +} + +// GetWalletPublicKey returns the wallet public key from the ConnectionParams. +func (c *ConnectionParams) GetWalletPublicKey() []byte { + return c.walletPublicKey +} + +// GetConversationKey returns the conversation key from the ConnectionParams. +func (c *ConnectionParams) GetConversationKey() []byte { + return c.conversationKey +} + +func ParseConnectionURI(nwcUri string) (parts *ConnectionParams, err error) { + var p *url.URL + if p, err = url.Parse(nwcUri); chk.E(err) { + return + } + if p == nil { + err = errors.New("invalid uri") + return + } + parts = &ConnectionParams{} + if p.Scheme != "nostr+walletconnect" { + err = errors.New("incorrect scheme") + return + } + if parts.walletPublicKey, err = p256k.HexToBin(p.Host); chk.E(err) { + err = errors.New("invalid public key") + return + } + query := p.Query() + var ok bool + var relay []string + if relay, ok = query["relay"]; !ok { + err = errors.New("missing relay parameter") + return + } + if len(relay) == 0 { + return nil, errors.New("no relays") + } + parts.relay = relay[0] + var secret string + if secret = query.Get("secret"); secret == "" { + err = errors.New("missing secret parameter") + return + } + var secretBytes []byte + if secretBytes, err = p256k.HexToBin(secret); chk.E(err) { + err = errors.New("invalid secret") + return + } + clientKey := &p256k.Signer{} + if err = clientKey.InitSec(secretBytes); chk.E(err) { + return + } + parts.clientSecretKey = clientKey + if parts.conversationKey, err = encryption.GenerateConversationKeyWithSigner( + clientKey, + parts.walletPublicKey, + ); chk.E(err) { + return + } + return +}