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>
188 lines
4.0 KiB
Go
188 lines
4.0 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 armor provides a strict, streaming implementation of the ASCII
|
|
// armoring format for age files.
|
|
//
|
|
// It's PEM with type "AGE ENCRYPTED FILE", 64 character columns, no headers,
|
|
// and strict base64 decoding.
|
|
package armor
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"filippo.io/age/internal/format"
|
|
)
|
|
|
|
const (
|
|
Header = "-----BEGIN AGE ENCRYPTED FILE-----"
|
|
Footer = "-----END AGE ENCRYPTED FILE-----"
|
|
)
|
|
|
|
type armoredWriter struct {
|
|
started, closed bool
|
|
encoder *format.WrappedBase64Encoder
|
|
dst io.Writer
|
|
}
|
|
|
|
func (a *armoredWriter) Write(p []byte) (int, error) {
|
|
if !a.started {
|
|
if _, err := io.WriteString(a.dst, Header+"\n"); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
a.started = true
|
|
return a.encoder.Write(p)
|
|
}
|
|
|
|
func (a *armoredWriter) Close() error {
|
|
if a.closed {
|
|
return errors.New("ArmoredWriter already closed")
|
|
}
|
|
a.closed = true
|
|
if err := a.encoder.Close(); err != nil {
|
|
return err
|
|
}
|
|
footer := Footer + "\n"
|
|
if !a.encoder.LastLineIsEmpty() {
|
|
footer = "\n" + footer
|
|
}
|
|
_, err := io.WriteString(a.dst, footer)
|
|
return err
|
|
}
|
|
|
|
func NewWriter(dst io.Writer) io.WriteCloser {
|
|
// TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps.
|
|
return &armoredWriter{
|
|
dst: dst,
|
|
encoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst),
|
|
}
|
|
}
|
|
|
|
type armoredReader struct {
|
|
r *bufio.Reader
|
|
started bool
|
|
unread []byte // backed by buf
|
|
buf [format.BytesPerLine]byte
|
|
err error
|
|
}
|
|
|
|
func NewReader(r io.Reader) io.Reader {
|
|
return &armoredReader{r: bufio.NewReader(r)}
|
|
}
|
|
|
|
func (r *armoredReader) 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
|
|
}
|
|
|
|
getLine := func() ([]byte, error) {
|
|
line, err := r.r.ReadBytes('\n')
|
|
if err == io.EOF && len(line) == 0 {
|
|
return nil, io.ErrUnexpectedEOF
|
|
} else if err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
line = bytes.TrimSuffix(line, []byte("\n"))
|
|
line = bytes.TrimSuffix(line, []byte("\r"))
|
|
return line, nil
|
|
}
|
|
|
|
const maxWhitespace = 1024
|
|
drainTrailing := func() error {
|
|
buf, err := io.ReadAll(io.LimitReader(r.r, maxWhitespace))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(bytes.TrimSpace(buf)) != 0 {
|
|
return errors.New("trailing data after armored file")
|
|
}
|
|
if len(buf) == maxWhitespace {
|
|
return errors.New("too much trailing whitespace")
|
|
}
|
|
return io.EOF
|
|
}
|
|
|
|
var removedWhitespace int
|
|
for !r.started {
|
|
line, err := getLine()
|
|
if err != nil {
|
|
return 0, r.setErr(err)
|
|
}
|
|
// Ignore leading whitespace.
|
|
if len(bytes.TrimSpace(line)) == 0 {
|
|
removedWhitespace += len(line) + 1
|
|
if removedWhitespace > maxWhitespace {
|
|
return 0, r.setErr(errors.New("too much leading whitespace"))
|
|
}
|
|
continue
|
|
}
|
|
if string(line) != Header {
|
|
return 0, r.setErr(fmt.Errorf("invalid first line: %q", line))
|
|
}
|
|
r.started = true
|
|
}
|
|
line, err := getLine()
|
|
if err != nil {
|
|
return 0, r.setErr(err)
|
|
}
|
|
if string(line) == Footer {
|
|
return 0, r.setErr(drainTrailing())
|
|
}
|
|
if len(line) > format.ColumnsPerLine {
|
|
return 0, r.setErr(errors.New("column limit exceeded"))
|
|
}
|
|
r.unread = r.buf[:]
|
|
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
|
|
if err != nil {
|
|
return 0, r.setErr(err)
|
|
}
|
|
r.unread = r.unread[:n]
|
|
|
|
if n < format.BytesPerLine {
|
|
line, err := getLine()
|
|
if err != nil {
|
|
return 0, r.setErr(err)
|
|
}
|
|
if string(line) != Footer {
|
|
return 0, r.setErr(fmt.Errorf("invalid closing line: %q", line))
|
|
}
|
|
r.setErr(drainTrailing())
|
|
}
|
|
|
|
nn := copy(p, r.unread)
|
|
r.unread = r.unread[nn:]
|
|
return nn, nil
|
|
}
|
|
|
|
type Error struct {
|
|
err error
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
return "invalid armor: " + e.err.Error()
|
|
}
|
|
|
|
func (e *Error) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
func (r *armoredReader) setErr(err error) error {
|
|
if err != io.EOF {
|
|
err = &Error{err}
|
|
}
|
|
r.err = err
|
|
return err
|
|
}
|