Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
5.2 KiB
Go
231 lines
5.2 KiB
Go
// Copyright 2019 The age Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package stream implements a variant of the STREAM chunked encryption scheme.
|
|
package stream
|
|
|
|
import (
|
|
"crypto/cipher"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
const ChunkSize = 64 * 1024
|
|
|
|
type Reader struct {
|
|
a cipher.AEAD
|
|
src io.Reader
|
|
|
|
unread []byte // decrypted but unread data, backed by buf
|
|
buf [encChunkSize]byte
|
|
|
|
err error
|
|
nonce [chacha20poly1305.NonceSize]byte
|
|
}
|
|
|
|
const (
|
|
encChunkSize = ChunkSize + chacha20poly1305.Overhead
|
|
lastChunkFlag = 0x01
|
|
)
|
|
|
|
func NewReader(key []byte, src io.Reader) (*Reader, error) {
|
|
aead, err := chacha20poly1305.New(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Reader{
|
|
a: aead,
|
|
src: src,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Reader) Read(p []byte) (int, error) {
|
|
if len(r.unread) > 0 {
|
|
n := copy(p, r.unread)
|
|
r.unread = r.unread[n:]
|
|
return n, nil
|
|
}
|
|
if r.err != nil {
|
|
return 0, r.err
|
|
}
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
last, err := r.readChunk()
|
|
if err != nil {
|
|
r.err = err
|
|
return 0, err
|
|
}
|
|
|
|
n := copy(p, r.unread)
|
|
r.unread = r.unread[n:]
|
|
|
|
if last {
|
|
// Ensure there is an EOF after the last chunk as expected. In other
|
|
// words, check for trailing data after a full-length final chunk.
|
|
// Hopefully, the underlying reader supports returning EOF even if it
|
|
// had previously returned an EOF to ReadFull.
|
|
if _, err := r.src.Read(make([]byte, 1)); err == nil {
|
|
r.err = errors.New("trailing data after end of encrypted file")
|
|
} else if err != io.EOF {
|
|
r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err)
|
|
} else {
|
|
r.err = io.EOF
|
|
}
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// readChunk reads the next chunk of ciphertext from r.src and makes it available
|
|
// in r.unread. last is true if the chunk was marked as the end of the message.
|
|
// readChunk must not be called again after returning a last chunk or an error.
|
|
func (r *Reader) readChunk() (last bool, err error) {
|
|
if len(r.unread) != 0 {
|
|
panic("stream: internal error: readChunk called with dirty buffer")
|
|
}
|
|
|
|
in := r.buf[:]
|
|
n, err := io.ReadFull(r.src, in)
|
|
switch {
|
|
case err == io.EOF:
|
|
// A message can't end without a marked chunk. This message is truncated.
|
|
return false, io.ErrUnexpectedEOF
|
|
case err == io.ErrUnexpectedEOF:
|
|
// The last chunk can be short, but not empty unless it's the first and
|
|
// only chunk.
|
|
if !nonceIsZero(&r.nonce) && n == r.a.Overhead() {
|
|
return false, errors.New("last chunk is empty, try age v1.0.0, and please consider reporting this")
|
|
}
|
|
in = in[:n]
|
|
last = true
|
|
setLastChunkFlag(&r.nonce)
|
|
case err != nil:
|
|
return false, err
|
|
}
|
|
|
|
outBuf := make([]byte, 0, ChunkSize)
|
|
out, err := r.a.Open(outBuf, r.nonce[:], in, nil)
|
|
if err != nil && !last {
|
|
// Check if this was a full-length final chunk.
|
|
last = true
|
|
setLastChunkFlag(&r.nonce)
|
|
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
|
|
}
|
|
if err != nil {
|
|
return false, errors.New("failed to decrypt and authenticate payload chunk")
|
|
}
|
|
|
|
incNonce(&r.nonce)
|
|
r.unread = r.buf[:copy(r.buf[:], out)]
|
|
return last, nil
|
|
}
|
|
|
|
func incNonce(nonce *[chacha20poly1305.NonceSize]byte) {
|
|
for i := len(nonce) - 2; i >= 0; i-- {
|
|
nonce[i]++
|
|
if nonce[i] != 0 {
|
|
break
|
|
} else if i == 0 {
|
|
// The counter is 88 bits, this is unreachable.
|
|
panic("stream: chunk counter wrapped around")
|
|
}
|
|
}
|
|
}
|
|
|
|
func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {
|
|
nonce[len(nonce)-1] = lastChunkFlag
|
|
}
|
|
|
|
func nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool {
|
|
return *nonce == [chacha20poly1305.NonceSize]byte{}
|
|
}
|
|
|
|
type Writer struct {
|
|
a cipher.AEAD
|
|
dst io.Writer
|
|
unwritten []byte // backed by buf
|
|
buf [encChunkSize]byte
|
|
nonce [chacha20poly1305.NonceSize]byte
|
|
err error
|
|
}
|
|
|
|
func NewWriter(key []byte, dst io.Writer) (*Writer, error) {
|
|
aead, err := chacha20poly1305.New(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
w := &Writer{
|
|
a: aead,
|
|
dst: dst,
|
|
}
|
|
w.unwritten = w.buf[:0]
|
|
return w, nil
|
|
}
|
|
|
|
func (w *Writer) Write(p []byte) (n int, err error) {
|
|
// TODO: consider refactoring with a bytes.Buffer.
|
|
if w.err != nil {
|
|
return 0, w.err
|
|
}
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
total := len(p)
|
|
for len(p) > 0 {
|
|
freeBuf := w.buf[len(w.unwritten):ChunkSize]
|
|
n := copy(freeBuf, p)
|
|
p = p[n:]
|
|
w.unwritten = w.unwritten[:len(w.unwritten)+n]
|
|
|
|
if len(w.unwritten) == ChunkSize && len(p) > 0 {
|
|
if err := w.flushChunk(notLastChunk); err != nil {
|
|
w.err = err
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// Close flushes the last chunk. It does not close the underlying Writer.
|
|
func (w *Writer) Close() error {
|
|
if w.err != nil {
|
|
return w.err
|
|
}
|
|
|
|
w.err = w.flushChunk(lastChunk)
|
|
if w.err != nil {
|
|
return w.err
|
|
}
|
|
|
|
w.err = errors.New("stream.Writer is already closed")
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
lastChunk = true
|
|
notLastChunk = false
|
|
)
|
|
|
|
func (w *Writer) flushChunk(last bool) error {
|
|
if !last && len(w.unwritten) != ChunkSize {
|
|
panic("stream: internal error: flush called with partial chunk")
|
|
}
|
|
|
|
if last {
|
|
setLastChunkFlag(&w.nonce)
|
|
}
|
|
buf := w.a.Seal(w.buf[:0], w.nonce[:], w.unwritten, nil)
|
|
_, err := w.dst.Write(buf)
|
|
w.unwritten = w.buf[:0]
|
|
incNonce(&w.nonce)
|
|
return err
|
|
}
|