ipfs, libp2p secio with AES-GCM spport

secio currently supports AES-CTR + SHA only

const DefaultSupportedCiphers = "AES-256,AES-128"
const DefaultSupportedHashes = "SHA256,SHA512"

Current implementation

Negotiation procedure can be found in secio code

s.runHandshakeSync()

After negotiating encParams, etmReader and etmWriter will be created to support encryption and decryption. Moreover, these two implement msgio.ReaderCloser and msgio.WriteCloser interfaces.

These two are powered by cipher.Stream which is created from AES-CTR.

type etmWriter struct {
	str cipher.Stream // the stream cipher to encrypt with
	mac HMAC          // the mac to authenticate data with
	w   io.Writer

	sync.Mutex
}

func makeMacAndCipher(e encParams) (mac HMAC, c cipher.Stream, err error) {
	mac, err = newMac(e.hashT, e.keys.MacKey)
	if err != nil {
		return
	}

	bc, err := newBlockCipher(e.cipherT, e.keys.CipherKey)
	if err != nil {
		return
	}

	c = cipher.NewCTR(bc, e.keys.IV)
	return
}

Actually, Enc/Dec operations can be found in several Read/Write methods.
Look at etmWriter.WriteMsg:

func (w *etmWriter) WriteMsg(b []byte) error {
	w.Lock()
	defer w.Unlock()

	// encrypt.
	buf := pool.Get(4 + len(b) + w.mac.Size())
	defer pool.Put(buf)
	data := buf[4 : 4+len(b)]
	w.str.XORKeyStream(data, b)

	// log.Debugf("ENC plaintext (%d): %s %v", len(b), b, b)
	// log.Debugf("ENC ciphertext (%d): %s %v", len(data), data, data)

	// then, mac.
	if _, err := w.mac.Write(data); err != nil {
		return err
	}

	// Sum appends.
	data = w.mac.Sum(data)
	w.mac.Reset()
	binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))

	_, err := w.w.Write(buf)
	return err
}

Adding GCM support

Two new Reader & Writer are needed. Take Writer as example:

type gcmWriter struct {
	aead cipher.AEAD // the gcm cipher to encrypt with
	w   io.Writer

	// iv for GCM
	iv  []byte

	sync.Mutex
}

// NewGCMWriter AEAD
func NewGCMWriter(w io.Writer, e encParams) (msgio.WriteCloser, error) {
	cipher, err := makeGcmCipher(e)
	if err != nil {
		return nil, err
	}
	iv := make([]byte, len(e.keys.IV))
	copy(iv, e.keys.IV)
	return &gcmWriter{w: w, aead: cipher, iv: iv}, nil
}

func makeGcmCipher(e encParams) (c cipher.AEAD, err error) {
	bc, err := newBlockCipher(e.cipherT, e.keys.CipherKey)
	if err != nil {
		return
	}

	c, err = cipher.NewGCM(bc) // , e.keys.IV
	return
}

AEAD block cipher will be created using the streched keys. Meanwhile, IV has to be copied to the Writer as well. GCM Cipher is using the default configurations: standardNonceSize and defaultTagSize

Writer’s interface

Only WriteMsg need to be changed. It is quite straitforward.

func (w *gcmWriter) WriteMsg(b []byte) error {
	w.Lock()
	defer w.Unlock()

	// encrypt.
	buf := pool.Get(4 + len(b) + w.aead.Overhead())
	defer pool.Put(buf)
	data := buf[4 : 4+len(b) + w.aead.Overhead()]

	_ = w.aead.Seal(data[:0], w.iv, b, nil)

	//log.Debugf("ENC plaintext  (%d): %x", len(b), b)
	//log.Debugf("ENC ciphertext (%d): %x", len(data), data)

	binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))

	_, err := w.w.Write(buf)
	return err
}

Reader’s Interface

Only have to change macCheckThenDecrypt(), as shown below. Don’t use AAD at this moment, so set it to nil.

func (r *gcmReader) macCheckThenDecrypt(m []byte) (int, error) {
	l := len(m)
	if l < r.aead.Overhead() {
		return 0, fmt.Errorf("buffer (%d) shorter than MAC size (%d)", l, r.aead.Overhead())
	}

	//log.Debugf("DEC ciphertext (%d): %x", len(m), m)
	_, err := r.aead.Open(m[:0], r.iv, m, nil)
	if err != nil {
		log.Debug("MAC Invalid")
		return 0, ErrMACInvalid2
	}

	// ok seems good.
	m = m[:l - r.aead.Overhead()]
	//log.Debugf("DEC plaintext  (%d): %x", len(m), m)
	return len(m), nil
}

Some slight changes to runHandshakeSync()

Have to set up Reader and Writer according to the agreed encParams

	w, err := NewWriter(s.insecure, s.local)
	if err != nil {
		return err
	}
	r, err := NewReader(s.insecure, s.remote)
	if err != nil {
		return err
	}
	...
	...
func NewWriter(w io.Writer, e encParams) (msgio.WriteCloser, error) {
	switch e.cipherT {
	case "AES-128", "AES-256":
		return NewETMWriter(w, e)
	case "GCM-128", "GCM-256":
		return NewGCMWriter(w, e)
	default:
		return nil, fmt.Errorf("unrecognized cipher type: %s", e.cipherT)
	}
}

func NewReader(r io.Reader, e encParams) (msgio.ReadCloser, error) {
	switch e.cipherT {
	case "AES-128", "AES-256":
		return NewETMReader(r, e)
	case "GCM-128", "GCM-256":
		return NewGCMReader(r, e)
	default:
		return nil, fmt.Errorf("unrecognized cipher type: %s", e.cipherT)
	}
}

KeyStretcher

Have to add GCM support:

	case "AES-128":
		ivSize = 16
		cipherKeySize = 16
	case "AES-256":
		ivSize = 16
		cipherKeySize = 32
	case "GCM-128":
		ivSize = 12
		cipherKeySize = 16
	case "GCM-256":
		ivSize = 12
		cipherKeySize = 32

algorithm preference

Add GCM-128,GCM-256 to DefaultSupportedCiphers. Of course we prefer to GCM because we all love better perfromance…

const DefaultSupportedCiphers = "GCM-128,GCM-256,AES-256,AES-128"

Conclusion

GCM is much faster than AES-CTR+SHA. It is well known SHA eats up too much CPU cycles…

Performance test data can be found in ipfs, libp2p stream peformance test. See case 1 vs. case 7.

发布了24 篇原创文章 · 获赞 6 · 访问量 2337

猜你喜欢

转载自blog.csdn.net/m0_37889044/article/details/104466718