mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-31 10:11:42 +08:00
266 lines
6.0 KiB
Go
266 lines
6.0 KiB
Go
package wafsec
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
const streamChunkSize = 64 * 1024
|
|
|
|
var streamMagic = []byte("SWLOGE01")
|
|
var ErrStreamAuthentication = errors.New("stream authentication failed")
|
|
|
|
// NewStreamKey returns a random data key and its instance-key envelope.
|
|
func NewStreamKey() ([]byte, string, error) {
|
|
key := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
|
return nil, "", err
|
|
}
|
|
wrapped, err := DataEncrypt(base64.StdEncoding.EncodeToString(key))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return key, wrapped, nil
|
|
}
|
|
|
|
func UnwrapStreamKey(wrapped string) ([]byte, error) {
|
|
if !IsDataKeyCiphertext(wrapped) {
|
|
return nil, ErrStreamAuthentication
|
|
}
|
|
text, err := DataDecrypt(wrapped, nil)
|
|
if err != nil {
|
|
return nil, ErrStreamAuthentication
|
|
}
|
|
key, err := base64.StdEncoding.DecodeString(text)
|
|
if err != nil || len(key) != 32 {
|
|
return nil, ErrStreamAuthentication
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
type StreamWriter struct {
|
|
dst io.Writer
|
|
aead cipher.AEAD
|
|
header []byte
|
|
seq uint64
|
|
buf []byte
|
|
err error
|
|
closed bool
|
|
}
|
|
|
|
// NewStreamWriter binds an authenticated stream to contextID; Close is required.
|
|
func NewStreamWriter(dst io.Writer, key []byte, contextID string) (*StreamWriter, error) {
|
|
if len(key) != 32 || dst == nil {
|
|
return nil, errors.New("invalid stream options")
|
|
}
|
|
header := make([]byte, 80)
|
|
copy(header, streamMagic)
|
|
if _, err := io.ReadFull(rand.Reader, header[8:44]); err != nil {
|
|
return nil, err
|
|
}
|
|
binary.BigEndian.PutUint32(header[44:48], streamChunkSize)
|
|
digest := sha256.Sum256([]byte(contextID))
|
|
copy(header[48:], digest[:])
|
|
aead, err := streamAEAD(key, header[8:40])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = streamWriteAll(dst, header); err != nil {
|
|
return nil, err
|
|
}
|
|
return &StreamWriter{dst: dst, aead: aead, header: header, buf: make([]byte, 0, streamChunkSize)}, nil
|
|
}
|
|
|
|
func streamAEAD(key, salt []byte) (cipher.AEAD, error) {
|
|
derived := make([]byte, 32)
|
|
if _, err := io.ReadFull(hkdf.New(sha256.New, key, salt, []byte("samwaf/log-export/stream/v1")), derived); err != nil {
|
|
return nil, err
|
|
}
|
|
block, err := aes.NewCipher(derived)
|
|
clear(derived)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cipher.NewGCM(block)
|
|
}
|
|
|
|
func streamNonce(header []byte, seq uint64) []byte {
|
|
nonce := make([]byte, 12)
|
|
copy(nonce, header[40:44])
|
|
binary.BigEndian.PutUint64(nonce[4:], seq)
|
|
return nonce
|
|
}
|
|
|
|
func streamAAD(header []byte, seq uint64, n uint32) []byte {
|
|
aad := make([]byte, len(header)+12)
|
|
copy(aad, header)
|
|
binary.BigEndian.PutUint64(aad[len(header):], seq)
|
|
binary.BigEndian.PutUint32(aad[len(header)+8:], n)
|
|
return aad
|
|
}
|
|
|
|
func streamWriteAll(w io.Writer, p []byte) error {
|
|
for len(p) > 0 {
|
|
n, err := w.Write(p)
|
|
if n < 0 || n > len(p) {
|
|
return io.ErrShortWrite
|
|
}
|
|
p = p[n:]
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return io.ErrShortWrite
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *StreamWriter) record(p []byte) error {
|
|
if w.seq == ^uint64(0) {
|
|
return errors.New("stream sequence exhausted")
|
|
}
|
|
n := uint32(len(p))
|
|
sealed := w.aead.Seal(nil, streamNonce(w.header, w.seq), p, streamAAD(w.header, w.seq, n))
|
|
var length [4]byte
|
|
binary.BigEndian.PutUint32(length[:], n)
|
|
if err := streamWriteAll(w.dst, length[:]); err != nil {
|
|
return err
|
|
}
|
|
if err := streamWriteAll(w.dst, sealed); err != nil {
|
|
return err
|
|
}
|
|
w.seq++
|
|
return nil
|
|
}
|
|
|
|
func (w *StreamWriter) Write(p []byte) (int, error) {
|
|
if w.closed {
|
|
return 0, errors.New("stream closed")
|
|
}
|
|
if w.err != nil {
|
|
return 0, w.err
|
|
}
|
|
written := 0
|
|
for len(p) > 0 {
|
|
n := min(streamChunkSize-len(w.buf), len(p))
|
|
w.buf = append(w.buf, p[:n]...)
|
|
p = p[n:]
|
|
written += n
|
|
if len(w.buf) == streamChunkSize {
|
|
w.err = w.record(w.buf)
|
|
if w.err != nil {
|
|
return written, w.err
|
|
}
|
|
w.buf = w.buf[:0]
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
func (w *StreamWriter) Close() error {
|
|
if w.closed {
|
|
return w.err
|
|
}
|
|
w.closed = true
|
|
if w.err == nil && len(w.buf) > 0 {
|
|
w.err = w.record(w.buf)
|
|
}
|
|
clear(w.buf)
|
|
w.buf = nil
|
|
if w.err == nil {
|
|
w.err = w.record(nil)
|
|
}
|
|
return w.err
|
|
}
|
|
|
|
type StreamReader struct {
|
|
src io.Reader
|
|
aead cipher.AEAD
|
|
header []byte
|
|
seq uint64
|
|
buf []byte
|
|
done bool
|
|
err error
|
|
}
|
|
|
|
func NewStreamReader(src io.Reader, key []byte, contextID string) (*StreamReader, error) {
|
|
if src == nil || len(key) != 32 {
|
|
return nil, ErrStreamAuthentication
|
|
}
|
|
header := make([]byte, 80)
|
|
if _, err := io.ReadFull(src, header); err != nil {
|
|
return nil, ErrStreamAuthentication
|
|
}
|
|
digest := sha256.Sum256([]byte(contextID))
|
|
if !bytes.Equal(header[:8], streamMagic) || binary.BigEndian.Uint32(header[44:48]) != streamChunkSize || !bytes.Equal(header[48:], digest[:]) {
|
|
return nil, ErrStreamAuthentication
|
|
}
|
|
aead, err := streamAEAD(key, header[8:40])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &StreamReader{src: src, aead: aead, header: header}, nil
|
|
}
|
|
|
|
func (r *StreamReader) next() error {
|
|
var length [4]byte
|
|
if _, err := io.ReadFull(r.src, length[:]); err != nil {
|
|
return ErrStreamAuthentication
|
|
}
|
|
n := binary.BigEndian.Uint32(length[:])
|
|
if n > streamChunkSize || r.seq == ^uint64(0) {
|
|
return ErrStreamAuthentication
|
|
}
|
|
sealed := make([]byte, int(n)+r.aead.Overhead())
|
|
if _, err := io.ReadFull(r.src, sealed); err != nil {
|
|
return ErrStreamAuthentication
|
|
}
|
|
plain, err := r.aead.Open(nil, streamNonce(r.header, r.seq), sealed, streamAAD(r.header, r.seq, n))
|
|
if err != nil {
|
|
return ErrStreamAuthentication
|
|
}
|
|
r.seq++
|
|
if n == 0 {
|
|
var extra [1]byte
|
|
count, err := io.ReadFull(r.src, extra[:])
|
|
if count != 0 || err != io.EOF {
|
|
return ErrStreamAuthentication
|
|
}
|
|
r.done = true
|
|
return io.EOF
|
|
}
|
|
r.buf = plain
|
|
return nil
|
|
}
|
|
|
|
func (r *StreamReader) Read(p []byte) (int, error) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
if r.err != nil {
|
|
return 0, r.err
|
|
}
|
|
if r.done {
|
|
return 0, io.EOF
|
|
}
|
|
if len(r.buf) == 0 {
|
|
if err := r.next(); err != nil {
|
|
r.err = err
|
|
return 0, err
|
|
}
|
|
}
|
|
n := copy(p, r.buf)
|
|
r.buf = r.buf[n:]
|
|
return n, nil
|
|
}
|