- Replaced all instances of p256k1signer with the new p8k.Signer across various modules, including event creation, policy handling, and database interactions. - Updated related test cases and benchmarks to ensure compatibility with the new signer interface. - Bumped version to v0.25.0 to reflect these significant changes and improvements in cryptographic operations.
275 lines
7.1 KiB
Go
275 lines
7.1 KiB
Go
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"
|
|
"github.com/minio/sha256-simd"
|
|
"next.orly.dev/pkg/encoders/hex"
|
|
"next.orly.dev/pkg/interfaces/signer"
|
|
"next.orly.dev/pkg/interfaces/signer/p8k"
|
|
"next.orly.dev/pkg/utils"
|
|
)
|
|
|
|
const (
|
|
version byte = 2
|
|
MinPlaintextSize int = 0x0001 // 1b msg => padded to 32b
|
|
MaxPlaintextSize int = 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
|
|
}
|
|
// Pre-allocate with exact size to avoid reallocation
|
|
ctLen := 1 + 32 + len(cipher) + 32
|
|
ct := make([]byte, ctLen)
|
|
ct[0] = version
|
|
copy(ct[1:], o.nonce)
|
|
copy(ct[33:], cipher)
|
|
copy(ct[33+len(cipher):], mac)
|
|
cipherString = make([]byte, base64.StdEncoding.EncodedLen(ctLen))
|
|
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
|
|
}
|
|
// Pre-allocate decoded buffer to avoid string conversion overhead
|
|
decodedLen := base64.StdEncoding.DecodedLen(len(b64ciphertextWrapped))
|
|
decoded := make([]byte, decodedLen)
|
|
var n int
|
|
if n, err = base64.StdEncoding.Decode(decoded, b64ciphertextWrapped); chk.E(err) {
|
|
return
|
|
}
|
|
decoded = decoded[:n]
|
|
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 *p8k.Signer
|
|
if sign, err = p8k.New(); chk.E(err) {
|
|
return
|
|
}
|
|
var sk []byte
|
|
if sk, err = hex.Dec(skh); chk.E(err) {
|
|
return
|
|
}
|
|
if err = sign.InitSec(sk); chk.E(err) {
|
|
return
|
|
}
|
|
var pk []byte
|
|
if pk, err = hex.Dec(pkh); chk.E(err) {
|
|
return
|
|
}
|
|
var shared []byte
|
|
if shared, err = sign.ECDHRaw(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.ECDHRaw(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
|
|
}
|