mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
The important one is the decryption side one, because when a user types a password they expect it to both decrypt and authenticate the file. Moved that one out of Decrypt and into ScryptIdentity, now that Identities get all the stanzas. special_cases-- This also opens the door to other Identity implementations that do allow multiple scrypt recipients, if someone really wants that. The CLI will never allow it, but an explicit choice by an API consumer feels like something we shouldn't interfere with. Moreover, this also allows alternative Identity implementations that use different recipient types to replicate the behavior if they have the same authentication semantics. The encryption side one is only a courtesy, to stop API users from making files that won't decrypt. Unfortunately, that one needs to stay as a special case in Encrypt, as the Recipient can't see around itself. However, changed it to a type assertion, so custom recipients can generate multiple scrypt recipient stanzas, if they really want.
179 lines
5.3 KiB
Go
179 lines
5.3 KiB
Go
// Copyright 2019 Google LLC
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file or at
|
|
// https://developers.google.com/open-source/licenses/bsd
|
|
|
|
package age
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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")
|
|
}
|
|
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 {
|
|
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 {
|
|
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
|
|
}
|