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>
209 lines
6.1 KiB
Go
209 lines
6.1 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 age
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"filippo.io/age/internal/bech32"
|
|
"filippo.io/age/internal/format"
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
"golang.org/x/crypto/curve25519"
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
const x25519Label = "age-encryption.org/v1/X25519"
|
|
|
|
// X25519Recipient is the standard age public key. Messages encrypted to this
|
|
// recipient can be decrypted with the corresponding X25519Identity.
|
|
//
|
|
// This recipient is anonymous, in the sense that an attacker can't tell from
|
|
// the message alone if it is encrypted to a certain recipient.
|
|
type X25519Recipient struct {
|
|
theirPublicKey []byte
|
|
}
|
|
|
|
var _ Recipient = &X25519Recipient{}
|
|
|
|
// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.
|
|
func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {
|
|
if len(publicKey) != curve25519.PointSize {
|
|
return nil, errors.New("invalid X25519 public key")
|
|
}
|
|
r := &X25519Recipient{
|
|
theirPublicKey: make([]byte, curve25519.PointSize),
|
|
}
|
|
copy(r.theirPublicKey, publicKey)
|
|
return r, nil
|
|
}
|
|
|
|
// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key
|
|
// encoding with the "age1" prefix.
|
|
func ParseX25519Recipient(s string) (*X25519Recipient, error) {
|
|
t, k, err := bech32.Decode(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
|
}
|
|
if t != "age" {
|
|
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
|
|
}
|
|
r, err := newX25519RecipientFromPoint(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error) {
|
|
ephemeral := make([]byte, curve25519.ScalarSize)
|
|
if _, err := rand.Read(ephemeral); err != nil {
|
|
return nil, err
|
|
}
|
|
ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
l := &Stanza{
|
|
Type: "X25519",
|
|
Args: []string{format.EncodeToString(ourPublicKey)},
|
|
}
|
|
|
|
salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))
|
|
salt = append(salt, ourPublicKey...)
|
|
salt = append(salt, r.theirPublicKey...)
|
|
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
|
|
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
|
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wrappedKey, err := aeadEncrypt(wrappingKey, fileKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
l.Body = wrappedKey
|
|
|
|
return []*Stanza{l}, nil
|
|
}
|
|
|
|
// String returns the Bech32 public key encoding of r.
|
|
func (r *X25519Recipient) String() string {
|
|
s, _ := bech32.Encode("age", r.theirPublicKey)
|
|
return s
|
|
}
|
|
|
|
// X25519Identity is the standard age private key, which can decrypt messages
|
|
// encrypted to the corresponding X25519Recipient.
|
|
type X25519Identity struct {
|
|
secretKey, ourPublicKey []byte
|
|
}
|
|
|
|
var _ Identity = &X25519Identity{}
|
|
|
|
// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.
|
|
func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {
|
|
if len(secretKey) != curve25519.ScalarSize {
|
|
return nil, errors.New("invalid X25519 secret key")
|
|
}
|
|
i := &X25519Identity{
|
|
secretKey: make([]byte, curve25519.ScalarSize),
|
|
}
|
|
copy(i.secretKey, secretKey)
|
|
i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)
|
|
return i, nil
|
|
}
|
|
|
|
// GenerateX25519Identity randomly generates a new X25519Identity.
|
|
func GenerateX25519Identity() (*X25519Identity, error) {
|
|
secretKey := make([]byte, curve25519.ScalarSize)
|
|
if _, err := rand.Read(secretKey); err != nil {
|
|
return nil, fmt.Errorf("internal error: %v", err)
|
|
}
|
|
return newX25519IdentityFromScalar(secretKey)
|
|
}
|
|
|
|
// ParseX25519Identity returns a new X25519Identity from a Bech32 private key
|
|
// encoding with the "AGE-SECRET-KEY-1" prefix.
|
|
func ParseX25519Identity(s string) (*X25519Identity, error) {
|
|
t, k, err := bech32.Decode(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed secret key: %v", err)
|
|
}
|
|
if t != "AGE-SECRET-KEY-" {
|
|
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
|
|
}
|
|
r, err := newX25519IdentityFromScalar(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed secret key: %v", err)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
|
return multiUnwrap(i.unwrap, stanzas)
|
|
}
|
|
|
|
func (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) {
|
|
if block.Type != "X25519" {
|
|
return nil, ErrIncorrectIdentity
|
|
}
|
|
if len(block.Args) != 1 {
|
|
return nil, errors.New("invalid X25519 recipient block")
|
|
}
|
|
publicKey, err := format.DecodeString(block.Args[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse X25519 recipient: %v", err)
|
|
}
|
|
if len(publicKey) != curve25519.PointSize {
|
|
return nil, errors.New("invalid X25519 recipient block")
|
|
}
|
|
|
|
sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
|
|
}
|
|
|
|
salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))
|
|
salt = append(salt, publicKey...)
|
|
salt = append(salt, i.ourPublicKey...)
|
|
h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))
|
|
wrappingKey := make([]byte, chacha20poly1305.KeySize)
|
|
if _, err := io.ReadFull(h, wrappingKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)
|
|
if err == errIncorrectCiphertextSize {
|
|
return nil, errors.New("invalid X25519 recipient block: incorrect file key size")
|
|
} else if err != nil {
|
|
return nil, ErrIncorrectIdentity
|
|
}
|
|
return fileKey, nil
|
|
}
|
|
|
|
// Recipient returns the public X25519Recipient value corresponding to i.
|
|
func (i *X25519Identity) Recipient() *X25519Recipient {
|
|
r := &X25519Recipient{}
|
|
r.theirPublicKey = i.ourPublicKey
|
|
return r
|
|
}
|
|
|
|
// String returns the Bech32 private key encoding of i.
|
|
func (i *X25519Identity) String() string {
|
|
s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey)
|
|
return strings.ToUpper(s)
|
|
}
|