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.