 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			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>
		
			
				
	
	
		
			207 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			6.3 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"
 | |
| 	"encoding/hex"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 
 | |
| 	"filippo.io/age/internal/format"
 | |
| 	"golang.org/x/crypto/chacha20poly1305"
 | |
| 	"golang.org/x/crypto/scrypt"
 | |
| )
 | |
| 
 | |
| const scryptLabel = "age-encryption.org/v1/scrypt"
 | |
| 
 | |
| // ScryptRecipient is a password-based recipient. Anyone with the password can
 | |
| // decrypt the message.
 | |
| //
 | |
| // If a ScryptRecipient is used, it must be the only recipient for the file: it
 | |
| // can't be mixed with other recipient types and can't be used multiple times
 | |
| // for the same file.
 | |
| //
 | |
| // Its use is not recommended for automated systems, which should prefer
 | |
| // X25519Recipient.
 | |
| type ScryptRecipient struct {
 | |
| 	password   []byte
 | |
| 	workFactor int
 | |
| }
 | |
| 
 | |
| var _ Recipient = &ScryptRecipient{}
 | |
| 
 | |
| // NewScryptRecipient returns a new ScryptRecipient with the provided password.
 | |
| func NewScryptRecipient(password string) (*ScryptRecipient, error) {
 | |
| 	if len(password) == 0 {
 | |
| 		return nil, errors.New("passphrase can't be empty")
 | |
| 	}
 | |
| 	r := &ScryptRecipient{
 | |
| 		password: []byte(password),
 | |
| 		// TODO: automatically scale this to 1s (with a min) in the CLI.
 | |
| 		workFactor: 18, // 1s on a modern machine
 | |
| 	}
 | |
| 	return r, nil
 | |
| }
 | |
| 
 | |
| // SetWorkFactor sets the scrypt work factor to 2^logN.
 | |
| // It must be called before Wrap.
 | |
| //
 | |
| // If SetWorkFactor is not called, a reasonable default is used.
 | |
| func (r *ScryptRecipient) SetWorkFactor(logN int) {
 | |
| 	if logN > 30 || logN < 1 {
 | |
| 		panic("age: SetWorkFactor called with illegal value")
 | |
| 	}
 | |
| 	r.workFactor = logN
 | |
| }
 | |
| 
 | |
| const scryptSaltSize = 16
 | |
| 
 | |
| func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
 | |
| 	salt := make([]byte, scryptSaltSize)
 | |
| 	if _, err := rand.Read(salt[:]); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	logN := r.workFactor
 | |
| 	l := &Stanza{
 | |
| 		Type: "scrypt",
 | |
| 		Args: []string{format.EncodeToString(salt), strconv.Itoa(logN)},
 | |
| 	}
 | |
| 
 | |
| 	salt = append([]byte(scryptLabel), salt...)
 | |
| 	k, err := scrypt.Key(r.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	wrappedKey, err := aeadEncrypt(k, fileKey)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	l.Body = wrappedKey
 | |
| 
 | |
| 	return []*Stanza{l}, nil
 | |
| }
 | |
| 
 | |
| // WrapWithLabels implements [age.RecipientWithLabels], returning a random
 | |
| // label. This ensures a ScryptRecipient can't be mixed with other recipients
 | |
| // (including other ScryptRecipients).
 | |
| //
 | |
| // Users reasonably expect files encrypted to a passphrase to be [authenticated]
 | |
| // by that passphrase, i.e. for it to be impossible to produce a file that
 | |
| // decrypts successfully with a passphrase without knowing it. If a file is
 | |
| // encrypted to other recipients, those parties can produce different files that
 | |
| // would break that expectation.
 | |
| //
 | |
| // [authenticated]: https://words.filippo.io/dispatches/age-authentication/
 | |
| func (r *ScryptRecipient) WrapWithLabels(fileKey []byte) (stanzas []*Stanza, labels []string, err error) {
 | |
| 	stanzas, err = r.Wrap(fileKey)
 | |
| 
 | |
| 	random := make([]byte, 16)
 | |
| 	if _, err := rand.Read(random); err != nil {
 | |
| 		return nil, nil, err
 | |
| 	}
 | |
| 	labels = []string{hex.EncodeToString(random)}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // ScryptIdentity is a password-based identity.
 | |
| type ScryptIdentity struct {
 | |
| 	password      []byte
 | |
| 	maxWorkFactor int
 | |
| }
 | |
| 
 | |
| var _ Identity = &ScryptIdentity{}
 | |
| 
 | |
| // NewScryptIdentity returns a new ScryptIdentity with the provided password.
 | |
| func NewScryptIdentity(password string) (*ScryptIdentity, error) {
 | |
| 	if len(password) == 0 {
 | |
| 		return nil, errors.New("passphrase can't be empty")
 | |
| 	}
 | |
| 	i := &ScryptIdentity{
 | |
| 		password:      []byte(password),
 | |
| 		maxWorkFactor: 22, // 15s on a modern machine
 | |
| 	}
 | |
| 	return i, nil
 | |
| }
 | |
| 
 | |
| // SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN.
 | |
| // It must be called before Unwrap.
 | |
| //
 | |
| // This caps the amount of work that Decrypt might have to do to process
 | |
| // received files. If SetMaxWorkFactor is not called, a fairly high default is
 | |
| // used, which might not be suitable for systems processing untrusted files.
 | |
| func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
 | |
| 	if logN > 30 || logN < 1 {
 | |
| 		panic("age: SetMaxWorkFactor called with illegal value")
 | |
| 	}
 | |
| 	i.maxWorkFactor = logN
 | |
| }
 | |
| 
 | |
| func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
 | |
| 	for _, s := range stanzas {
 | |
| 		if s.Type == "scrypt" && len(stanzas) != 1 {
 | |
| 			return nil, errors.New("an scrypt recipient must be the only one")
 | |
| 		}
 | |
| 	}
 | |
| 	return multiUnwrap(i.unwrap, stanzas)
 | |
| }
 | |
| 
 | |
| var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)
 | |
| 
 | |
| func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
 | |
| 	if block.Type != "scrypt" {
 | |
| 		return nil, ErrIncorrectIdentity
 | |
| 	}
 | |
| 	if len(block.Args) != 2 {
 | |
| 		return nil, errors.New("invalid scrypt recipient block")
 | |
| 	}
 | |
| 	salt, err := format.DecodeString(block.Args[0])
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse scrypt salt: %v", err)
 | |
| 	}
 | |
| 	if len(salt) != scryptSaltSize {
 | |
| 		return nil, errors.New("invalid scrypt recipient block")
 | |
| 	}
 | |
| 	if w := block.Args[1]; !digitsRe.MatchString(w) {
 | |
| 		return nil, fmt.Errorf("scrypt work factor encoding invalid: %q", w)
 | |
| 	}
 | |
| 	logN, err := strconv.Atoi(block.Args[1])
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
 | |
| 	}
 | |
| 	if logN > i.maxWorkFactor {
 | |
| 		return nil, fmt.Errorf("scrypt work factor too large: %v", logN)
 | |
| 	}
 | |
| 	if logN <= 0 { // unreachable
 | |
| 		return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
 | |
| 	}
 | |
| 
 | |
| 	salt = append([]byte(scryptLabel), salt...)
 | |
| 	k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
 | |
| 	if err != nil { // unreachable
 | |
| 		return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// This AEAD is not robust, so an attacker could craft a message that
 | |
| 	// decrypts under two different keys (meaning two different passphrases) and
 | |
| 	// then use an error side-channel in an online decryption oracle to learn if
 | |
| 	// either key is correct. This is deemed acceptable because the use case (an
 | |
| 	// online decryption oracle) is not recommended, and the security loss is
 | |
| 	// only one bit. This also does not bypass any scrypt work, although that work
 | |
| 	// can be precomputed in an online oracle scenario.
 | |
| 	fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
 | |
| 	if err == errIncorrectCiphertextSize {
 | |
| 		return nil, errors.New("invalid scrypt recipient block: incorrect file key size")
 | |
| 	} else if err != nil {
 | |
| 		return nil, ErrIncorrectIdentity
 | |
| 	}
 | |
| 	return fileKey, nil
 | |
| }
 |