From 39f541fc034bdf29e45d66751e8c9ae49d7ed3b4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Jan 2024 22:27:13 -0300 Subject: [PATCH] implement nip49. --- go.mod | 2 +- nip49/nip49.go | 111 ++++++++++++++++++++++++++++++++++++++++++++ nip49/nip49_test.go | 53 +++++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 nip49/nip49.go create mode 100644 nip49/nip49_test.go diff --git a/go.mod b/go.mod index 6f70751..2137d08 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/tidwall/gjson v1.14.4 github.com/tyler-smith/go-bip32 v1.0.0 github.com/tyler-smith/go-bip39 v1.1.0 + golang.org/x/crypto v0.7.0 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/net v0.8.0 ) @@ -27,6 +28,5 @@ require ( github.com/stretchr/testify v1.8.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/crypto v0.7.0 // indirect golang.org/x/sys v0.8.0 // indirect ) diff --git a/nip49/nip49.go b/nip49/nip49.go new file mode 100644 index 0000000..c4c88f5 --- /dev/null +++ b/nip49/nip49.go @@ -0,0 +1,111 @@ +package nip49 + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "math" + + "github.com/btcsuite/btcd/btcutil/bech32" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/scrypt" +) + +type KeySecurityByte byte + +const ( + KnownToHaveBeenHandledInsecurely KeySecurityByte = 0x00 + NotKnownToHaveBeenHandledInsecurely KeySecurityByte = 0x01 + ClientDoesNotTrackThisData KeySecurityByte = 0x02 +) + +func Decrypt(bech32string string, password string) (secretKey string, err error) { + secb, err := DecryptToBytes(bech32string, password) + return hex.EncodeToString(secb), err +} + +func DecryptToBytes(bech32string string, password string) (secretKey []byte, err error) { + prefix, bits5, err := bech32.DecodeNoLimit(bech32string) + if err != nil { + return nil, err + } + if prefix != "ncryptsec" { + return nil, fmt.Errorf("expected prefix ncryptsec1") + } + + data, err := bech32.ConvertBits(bits5, 5, 8, false) + if err != nil { + return nil, fmt.Errorf("failed translating data into 8 bits: %s", err.Error()) + } + + version := data[0] + if version != 0x02 { + return nil, fmt.Errorf("expected version 0x02, got %v", version) + } + + logn := data[1] + n := int(math.Pow(2, float64(int(logn)))) + salt := data[2 : 2+16] + nonce := data[2+16 : 2+16+24] + ad := data[2+16+24 : 2+16+24+1] + // keySecurityByte := ad[0] + encryptedKey := data[2+16+24+1:] + + key, err := scrypt.Key([]byte(password), salt, n, 8, 1, 32) + if err != nil { + return nil, fmt.Errorf("failed to compute key with scrypt: %w", err) + } + + c2p1, err := chacha20poly1305.NewX(key) + if err != nil { + return nil, fmt.Errorf("failed to start xchacha20poly1305: %w", err) + } + + return c2p1.Open(nil, nonce, encryptedKey, ad) +} + +func Encrypt(secretKey string, password string, logn uint8, ksb KeySecurityByte) (b32code string, err error) { + skb, err := hex.DecodeString(secretKey) + if err != nil || len(skb) != 32 { + return "", fmt.Errorf("invalid secret key") + } + return EncryptBytes(skb, password, logn, ksb) +} + +func EncryptBytes(secretKey []byte, password string, logn uint8, ksb KeySecurityByte) (b32code string, err error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return "", fmt.Errorf("failed to read salt: %w", err) + } + n := int(math.Pow(2, float64(int(logn)))) + key, err := scrypt.Key([]byte(password), salt, n, 8, 1, 32) + if err != nil { + return "", fmt.Errorf("failed to compute key with scrypt: %w", err) + } + + concat := make([]byte, 91) + concat[0] = 0x02 + concat[1] = byte(logn) + copy(concat[2:2+16], salt) + rand.Read(concat[2+16 : 2+16+24]) // nonce + ad := []byte{byte(ksb)} + copy(concat[2+16+24:2+16+24+1], ad) + + c2p1, err := chacha20poly1305.NewX(key) + if err != nil { + return "", fmt.Errorf("failed to start xchacha20poly1305: %w", err) + } + ciphertext := c2p1.Seal(nil, concat[2+16:2+16+24], secretKey, ad) + if err != nil { + return "", fmt.Errorf("failed to encrypt: %w", err) + } + copy(concat[2+16+24+1:], ciphertext) + + fmt.Println(hex.EncodeToString(ciphertext), len(ciphertext), len(concat), len(concat)-(2+16+24+1)) + + bits5, err := bech32.ConvertBits(concat, 8, 5, true) + if err != nil { + return "", err + } + return bech32.Encode("ncryptsec", bits5) +} diff --git a/nip49/nip49_test.go b/nip49/nip49_test.go new file mode 100644 index 0000000..2e31fc5 --- /dev/null +++ b/nip49/nip49_test.go @@ -0,0 +1,53 @@ +package nip49 + +import ( + "strings" + "testing" +) + +func TestDecryptKeyFromNIPText(t *testing.T) { + ncrypt := "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" + secretKey, err := Decrypt(ncrypt, "nostr") + if err != nil { + t.Fatalf("failed to decrypt: %s", err) + } + if secretKey != "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" { + t.Fatalf("decrypted wrongly: %s", secretKey) + } +} + +func TestEncryptAndDecrypt(t *testing.T) { + for i, f := range []struct { + password string + secretkey string + logn uint8 + ksb KeySecurityByte + }{ + {".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00}, + {"skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01}, + {"777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02}, + {".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00}, + {"skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01}, + {"777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02}, + {"", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 4, 0x00}, + {"", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 5, 0x01}, + {"", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 1, 0x00}, + {"", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x01}, + } { + bech32code, err := Encrypt(f.secretkey, f.password, f.logn, f.ksb) + if err != nil { + t.Fatalf("failed to encrypt %d: %s", i, err) + } + if !strings.HasPrefix(bech32code, "ncryptsec1") || len(bech32code) != 162 { + t.Fatalf("bech32 code is wrong %d: %s", i, bech32code) + } + + secretKey, err := Decrypt(bech32code, f.password) + if err != nil { + t.Fatalf("failed to decrypt %d: %s", i, err) + } + if secretKey != f.secretkey { + t.Fatalf("decrypted to the wrong value %d: %s", i, secretKey) + } + } +}