Skip to content

Commit

Permalink
implement nip49.
Browse files Browse the repository at this point in the history
  • Loading branch information
fiatjaf committed Jan 23, 2024
1 parent fb92dc4 commit 39f541f
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
111 changes: 111 additions & 0 deletions nip49/nip49.go
Original file line number Diff line number Diff line change
@@ -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)
}
53 changes: 53 additions & 0 deletions nip49/nip49_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

0 comments on commit 39f541f

Please sign in to comment.