147 lines
3.4 KiB
Go
147 lines
3.4 KiB
Go
package symm
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/cipher"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
)
|
|
|
|
const (
|
|
gcmStreamMagic = "SCG1"
|
|
gcmStreamChunkSize = 32 * 1024
|
|
)
|
|
|
|
var ErrInvalidGCMStreamChunk = errors.New("invalid gcm stream chunk")
|
|
|
|
func encryptGCMChunk(aead cipher.AEAD, plain, nonce, aad []byte, chunkIndex uint64) []byte {
|
|
chunkNonce := deriveChunkNonce(nonce, chunkIndex)
|
|
return aead.Seal(nil, chunkNonce, plain, aad)
|
|
}
|
|
|
|
func decryptGCMChunk(aead cipher.AEAD, ciphertext, nonce, aad []byte, chunkIndex uint64) ([]byte, error) {
|
|
chunkNonce := deriveChunkNonce(nonce, chunkIndex)
|
|
return aead.Open(nil, chunkNonce, ciphertext, aad)
|
|
}
|
|
|
|
func encryptGCMChunkedStream(dst io.Writer, src io.Reader, aead cipher.AEAD, nonce, aad []byte) error {
|
|
if _, err := dst.Write([]byte(gcmStreamMagic)); err != nil {
|
|
return err
|
|
}
|
|
|
|
buf := make([]byte, gcmStreamChunkSize)
|
|
lenBuf := make([]byte, 4)
|
|
var chunkIndex uint64
|
|
|
|
for {
|
|
n, err := src.Read(buf)
|
|
if n > 0 {
|
|
sealed := encryptGCMChunk(aead, buf[:n], nonce, aad, chunkIndex)
|
|
binary.BigEndian.PutUint32(lenBuf, uint32(len(sealed)))
|
|
if _, werr := dst.Write(lenBuf); werr != nil {
|
|
return werr
|
|
}
|
|
if _, werr := dst.Write(sealed); werr != nil {
|
|
return werr
|
|
}
|
|
chunkIndex++
|
|
}
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func decryptGCMChunkedOrLegacyStream(dst io.Writer, src io.Reader, aead cipher.AEAD, nonce, aad []byte) error {
|
|
header := make([]byte, len(gcmStreamMagic))
|
|
n, err := io.ReadFull(src, header)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != io.ErrUnexpectedEOF {
|
|
return err
|
|
}
|
|
return decryptGCMLegacyBuffered(dst, io.MultiReader(bytes.NewReader(header[:n]), src), aead, nonce, aad)
|
|
}
|
|
|
|
if string(header) != gcmStreamMagic {
|
|
return decryptGCMLegacyBuffered(dst, io.MultiReader(bytes.NewReader(header), src), aead, nonce, aad)
|
|
}
|
|
return decryptGCMChunkedStream(dst, src, aead, nonce, aad)
|
|
}
|
|
|
|
func decryptGCMChunkedStream(dst io.Writer, src io.Reader, aead cipher.AEAD, nonce, aad []byte) error {
|
|
lenBuf := make([]byte, 4)
|
|
maxChunkLen := uint32(gcmStreamChunkSize + aead.Overhead())
|
|
var chunkIndex uint64
|
|
|
|
for {
|
|
_, err := io.ReadFull(src, lenBuf)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err == io.ErrUnexpectedEOF {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
return err
|
|
}
|
|
|
|
chunkLen := binary.BigEndian.Uint32(lenBuf)
|
|
if chunkLen < uint32(aead.Overhead()) || chunkLen > maxChunkLen {
|
|
return ErrInvalidGCMStreamChunk
|
|
}
|
|
|
|
chunk := make([]byte, chunkLen)
|
|
if _, err := io.ReadFull(src, chunk); err != nil {
|
|
if err == io.ErrUnexpectedEOF {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
return err
|
|
}
|
|
|
|
plain, err := decryptGCMChunk(aead, chunk, nonce, aad, chunkIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := dst.Write(plain); err != nil {
|
|
return err
|
|
}
|
|
chunkIndex++
|
|
}
|
|
}
|
|
|
|
func decryptGCMLegacyBuffered(dst io.Writer, src io.Reader, aead cipher.AEAD, nonce, aad []byte) error {
|
|
enc, err := io.ReadAll(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
plain, err := aead.Open(nil, nonce, enc, aad)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = dst.Write(plain)
|
|
return err
|
|
}
|
|
|
|
func deriveChunkNonce(baseNonce []byte, chunkIndex uint64) []byte {
|
|
nonce := make([]byte, len(baseNonce))
|
|
copy(nonce, baseNonce)
|
|
if len(nonce) < 8 {
|
|
return nonce
|
|
}
|
|
|
|
var indexBytes [8]byte
|
|
binary.BigEndian.PutUint64(indexBytes[:], chunkIndex)
|
|
off := len(nonce) - 8
|
|
for i := 0; i < 8; i++ {
|
|
nonce[off+i] ^= indexBytes[i]
|
|
}
|
|
return nonce
|
|
}
|