Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions pkg/noise/dh.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,50 @@ import (
"github.com/skycoin/skycoin/src/cipher"
)

const keypairPoolSize = 64

// keypairPool holds pre-generated ephemeral keypairs for noise handshakes.
// secp256k1 key generation is expensive (EC multiply + validation), so we
// generate them in the background and serve them from a buffered channel.
var keypairPool = func() chan noise.DHKey {
ch := make(chan noise.DHKey, keypairPoolSize)
go func() {
for {
pk, sk := cipher.GenerateKeyPair()
ch <- noise.DHKey{
Private: sk[:],
Public: pk[:],
}
}
}()
return ch
}()

// Secp256k1 implements `noise.DHFunc`.
type Secp256k1 struct{}

// GenerateKeypair helps to implement `noise.DHFunc`.
func (Secp256k1) GenerateKeypair(_ io.Reader) (noise.DHKey, error) {
pk, sk := cipher.GenerateKeyPair()
return noise.DHKey{
Private: sk[:],
Public: pk[:],
}, nil
return <-keypairPool, nil
}

// DH helps to implement `noise.DHFunc`.
// Keys are already validated by the noise handshake state machine, so we
// skip the redundant NewPubKey/NewSecKey validation and copy directly.
// cipher.ECDH still performs its own internal validation.
func (Secp256k1) DH(sk, pk []byte) []byte {
pubKey, err := cipher.NewPubKey(pk)
if err != nil {
panic(fmt.Sprintf("noise DH: invalid public key: %v", err))
}
secKey, err := cipher.NewSecKey(sk)
if err != nil {
panic(fmt.Sprintf("noise DH: invalid secret key: %v", err))
}
var pubKey cipher.PubKey
var secKey cipher.SecKey
copy(pubKey[:], pk)
copy(secKey[:], sk)
ecdh, err := cipher.ECDH(pubKey, secKey)
if err != nil {
panic(fmt.Sprintf("noise DH: ECDH failed: %v", err))
}
return append(ecdh, byte(0))
// DHLen() returns 33; ECDH returns 32-byte SHA256 hash, pad to 33.
out := make([]byte, 33)
copy(out, ecdh)
return out
}

// DHLen helps to implement `noise.DHFunc`.
Expand Down
18 changes: 12 additions & 6 deletions pkg/noise/noise.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type Noise struct {

encNonce uint64 // increment after encryption
decNonce uint64 // expect increment with each subsequent packet

encBuf [nonceSize]byte // reusable nonce buffer for encryption
}

// New creates a new Noise with:
Expand Down Expand Up @@ -134,21 +136,25 @@ func (ns *Noise) LocalStatic() cipher.PubKey {
// RemoteStatic returns the remote static public key.
// Returns an empty public key if the peer static key is invalid.
func (ns *Noise) RemoteStatic() cipher.PubKey {
pk, err := cipher.NewPubKey(ns.hs.PeerStatic())
if err != nil {
noiseLogger.WithError(err).Error("Invalid remote static public key")
raw := ns.hs.PeerStatic()
if len(raw) != len(cipher.PubKey{}) {
return cipher.PubKey{}
}
var pk cipher.PubKey
copy(pk[:], raw)
return pk
}

// EncryptUnsafe encrypts plaintext without interlocking, should only
// be used with external lock.
func (ns *Noise) EncryptUnsafe(plaintext []byte) []byte {
ns.encNonce++
buf := make([]byte, nonceSize)
binary.BigEndian.PutUint64(buf, ns.encNonce)
return append(buf, ns.enc.Cipher().Encrypt(nil, ns.encNonce, nil, plaintext)...)
binary.BigEndian.PutUint64(ns.encBuf[:], ns.encNonce)
ciphertext := ns.enc.Cipher().Encrypt(nil, ns.encNonce, nil, plaintext)
out := make([]byte, nonceSize+len(ciphertext))
copy(out, ns.encBuf[:])
copy(out[nonceSize:], ciphertext)
return out
}

// DecryptUnsafe decrypts ciphertext without interlocking, should only
Expand Down
33 changes: 28 additions & 5 deletions pkg/noise/read_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ const (
authSize = 24 // noise auth data size
)

// framePool reuses frame buffers to reduce allocations in the hot path.
var framePool = sync.Pool{
New: func() interface{} {
b := make([]byte, maxFrameSize)
return &b
},
}

type timeoutError struct{}

func (timeoutError) Error() string { return "deadline exceeded" }
Expand Down Expand Up @@ -273,16 +281,31 @@ func ResponderHandshake(ns *Noise, r *bufio.Reader, w io.Writer) error {
// WriteRawFrame writes a raw frame (data prefixed with a uint16 len).
// It returns the bytes written.
func WriteRawFrame(w io.Writer, p []byte) ([]byte, error) {
buf := make([]byte, prefixSize+len(p))
frameLen := prefixSize + len(p)
lenp, ok := safecast.To[uint16](len(p))
if ok {
if !ok {
return []byte{}, fmt.Errorf("failed to cast length of slice to uint16")
}

// Use pooled buffer for frames that fit, fall back to allocation for oversized.
if frameLen <= maxFrameSize {
bp := framePool.Get().(*[]byte)
buf := (*bp)[:frameLen]
binary.BigEndian.PutUint16(buf, lenp)
copy(buf[prefixSize:], p)

n, err := w.Write(buf)
return buf[:n], err
framePool.Put(bp)
if err != nil {
return buf[:n], err
}
return buf[:n], nil
}
return []byte{}, fmt.Errorf("failed to cast length of slice to uint16")

buf := make([]byte, frameLen)
binary.BigEndian.PutUint16(buf, lenp)
copy(buf[prefixSize:], p)
n, err := w.Write(buf)
return buf[:n], err
}

// ReadRawFrame attempts to read a raw frame from a buffered reader.
Expand Down
Loading