go-nostr/nip49/nip49.go
2024-10-28 14:56:36 -03:00

123 lines
3.2 KiB
Go

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"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
type KeySecurityByte byte
const (
KnownToHaveBeenHandledInsecurely KeySecurityByte = 0x00
NotKnownToHaveBeenHandledInsecurely KeySecurityByte = 0x01
ClientDoesNotTrackThisData KeySecurityByte = 0x02
)
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 := getKey(password, salt, n)
if err != nil {
return "", 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)
copy(concat[2+16+24+1:], ciphertext)
bits5, err := bech32.ConvertBits(concat, 8, 5, true)
if err != nil {
return "", err
}
return bech32.Encode("ncryptsec", bits5)
}
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 := getKey(password, salt, n)
if err != nil {
return nil, 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 getKey(password string, salt []byte, n int) ([]byte, error) {
normalizedPassword, _, err := transform.Bytes(norm.NFKC, []byte(password))
if err != nil {
return nil, fmt.Errorf("failed to normalize password: %w", err)
}
key, err := scrypt.Key(normalizedPassword, salt, n, 8, 1, 32)
if err != nil {
return nil, fmt.Errorf("failed to compute key with scrypt: %w", err)
}
return key, nil
}