safing-jess/core.go

687 lines
18 KiB
Go

package jess
import (
"crypto/subtle"
"errors"
"fmt"
"github.com/safing/structures/container"
)
// Close encrypts (and possibly signs) the given data and returns a Letter. Storyline: Close takes an envelope, inserts the message and closes it, resulting in a letter.
func (s *Session) Close(data []byte) (*Letter, error) { //nolint:gocognit
var err error
var associatedData []byte
letter := &Letter{}
if s.wire == nil || s.wire.msgNo == 0 {
letter.Version = s.envelope.Version
letter.SuiteID = s.envelope.SuiteID
}
// Check for additional data in slice, which we should not touch.
// TODO: Pre-allocate needed overhead for AEAD and others.
if len(data) != cap(data) {
// Make a copy of the data in order to not modify unrelated data.
copiedData := make([]byte, len(data))
copy(copiedData, data)
data = copiedData
}
// ==============
// key management
// ==============
// create nonce
nonce, err := RandomBytes(s.NonceSize())
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}
letter.Nonce = nonce
if s.kdf != nil {
// if we require a key
// key establishment
if s.wire != nil {
err = s.wire.sendHandshakeAndInitKDF(letter)
if err != nil {
return nil, err
}
} else {
keyMaterial, err := s.setupClosingKeyMaterial(letter)
if err != nil {
return nil, err
}
// init KDF
err = s.kdf.InitKeyDerivation(letter.Nonce, keyMaterial...)
if err != nil {
return nil, fmt.Errorf("failed to init %s kdf: %w", s.kdf.Info().Name, err)
}
}
// ==========
// encryption
// ==========
// setup tools
err = s.setup()
if err != nil {
return nil, err
}
defer s.reset() //nolint:errcheck // TODO: handle error? Currently there should be none.
// Ciphers
for _, tool := range s.ciphers {
data, err = tool.Encrypt(data)
if err != nil {
return nil, fmt.Errorf("failed to encrypt with %s: %w", tool.Info().Name, err)
}
}
// build associated data
if len(s.integratedCiphers) > 0 || len(s.macs) > 0 {
associatedData = letter.compileAssociatedData()
}
// Integrated Ciphers / AEAD
for _, tool := range s.integratedCiphers {
data, err = tool.AuthenticatedEncrypt(data, associatedData)
if err != nil {
return nil, fmt.Errorf("failed to auth-encrypt with %s: %w", tool.Info().Name, err)
}
}
if len(s.macs) > 0 {
// run managed mac hashers
if s.managedMACHashers != nil {
err = s.feedManagedHashers(s.managedMACHashers, data, associatedData)
if err != nil {
return nil, err
}
defer s.resetManagedHashers(s.managedMACHashers)
}
// run MAC tools
allMacs := container.New()
for _, tool := range s.macs {
mac, err := tool.MAC(data, associatedData)
if err != nil {
return nil, fmt.Errorf("failed to calculate MAC with %s: %w", tool.Info().Name, err)
}
allMacs.Append(mac)
}
letter.Mac = allMacs.CompileData()
}
} else if len(s.ciphers) > 0 || len(s.integratedCiphers) > 0 || len(s.macs) > 0 {
// check if there is really nothing to do with a key
return nil, errors.New("missing a kdf tool")
}
// data processing is complete
letter.Data = data
// Signature
if len(s.signers) > 0 {
associatedSigningData := letter.compileAssociatedSigningData(associatedData)
// run managed signing hashers
if s.managedSigningHashers != nil {
err = s.feedManagedHashers(s.managedSigningHashers, data, associatedSigningData)
if err != nil {
return nil, err
}
defer s.resetManagedHashers(s.managedSigningHashers)
}
// run signers
for _, tool := range s.signers {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopSenders(tool.Info().Name, func(signet *Signet) error {
sig, err := tool.Sign(data, associatedSigningData, signet)
if err != nil {
return fmt.Errorf("failed to sign with %s: %w", tool.Info().Name, err)
}
letter.Signatures = append(letter.Signatures, &Seal{
Scheme: tool.Info().Name,
ID: signet.ID,
Value: sig,
})
return nil
})
if err != nil {
return nil, err
}
}
}
return letter, nil
}
// Open decrypts (and possibly verifies) the given letter and returns the original data. Storyline: Open takes a letter, checks any seals, opens it and returns the message.
func (s *Session) Open(letter *Letter) ([]byte, error) { //nolint:gocognit,gocyclo
// debugging:
/*
fmt.Printf("opening: %+v\n", letter)
for _, seal := range letter.Keys {
fmt.Printf("key: %+v\n", seal)
}
*/
var err error
if s.wire == nil && letter.Version != 1 {
return nil, fmt.Errorf("unsupported letter version: %d", letter.Version)
}
// ======
// verify
// ======
// TODO: signature verification is run before tool setup. Currently, this is ok, but might change in the future. This might break additional signing algorithms that actually need setup.
data := letter.Data
// build associated data
var associatedData []byte
if len(s.integratedCiphers) > 0 || len(s.macs) > 0 {
associatedData = letter.compileAssociatedData()
}
// Signature
if len(s.signers) > 0 {
associatedSigningData := letter.compileAssociatedSigningData(associatedData)
// run managed signing hashers
if s.managedSigningHashers != nil {
err = s.feedManagedHashers(s.managedSigningHashers, data, associatedSigningData)
if err != nil {
return nil, err
}
defer s.resetManagedHashers(s.managedSigningHashers)
}
// run signers
if len(s.envelope.Senders) != len(letter.Signatures) {
return nil, errors.New("mismatch regarding available signatures and senders")
}
sigIndex := 0
for _, tool := range s.signers {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopSenders(tool.Info().Name, func(signet *Signet) error {
err := tool.Verify(data, associatedSigningData, letter.Signatures[sigIndex].Value, signet)
if err != nil {
return fmt.Errorf("failed to verify signature (%s) with ID %s: %w", tool.Info().Name, letter.Signatures[sigIndex].ID, err)
}
sigIndex++
return nil
})
if err != nil {
return nil, err
}
}
}
// end early if we are only verifying sigs
if s.kdf == nil {
// check if there is really nothing to do with a key
if len(s.ciphers) > 0 || len(s.integratedCiphers) > 0 || len(s.macs) > 0 {
return nil, errors.New("missing a kdf tool")
}
return data, nil
}
// ==============
// key management
// ==============
// key establishment
if s.wire != nil {
err = s.wire.recvHandshakeAndInitKDF(letter)
if err != nil {
return nil, err
}
} else {
keyMaterial, err := s.setupOpeningKeyMaterial(letter)
if err != nil {
return nil, err
}
// init KDF
err = s.kdf.InitKeyDerivation(letter.Nonce, keyMaterial...)
if err != nil {
return nil, fmt.Errorf("failed to init %s kdf: %w", s.kdf.Info().Name, err)
}
}
// ==========
// decryption
// ==========
// setup tools
err = s.setup()
if err != nil {
return nil, err
}
defer s.reset() //nolint:errcheck // TODO: handle error? Currently there should be none.
// MAC
if len(s.macs) > 0 {
// run managed mac hashers
if s.managedMACHashers != nil {
err = s.feedManagedHashers(s.managedMACHashers, data, associatedData)
if err != nil {
return nil, err
}
defer s.resetManagedHashers(s.managedMACHashers)
}
// run MAC tools
allMacs := container.New()
for _, tool := range s.macs {
mac, err := tool.MAC(data, associatedData)
if err != nil {
return nil, fmt.Errorf("failed to calculate MAC with %s: %w", tool.Info().Name, err)
}
allMacs.Append(mac)
}
if subtle.ConstantTimeCompare(letter.Mac, allMacs.CompileData()) != 1 {
return nil, fmt.Errorf("%w: MAC verification failed", ErrIntegrityViolation)
}
}
// Integrated Ciphers / AEAD (in reversed order)
for i := len(s.integratedCiphers) - 1; i >= 0; i-- {
data, err = s.integratedCiphers[i].AuthenticatedDecrypt(data, associatedData)
if err != nil {
return nil, fmt.Errorf("%w: [%s] %w", ErrIntegrityViolation, s.integratedCiphers[i].Info().Name, err)
}
}
// Ciphers (in reversed order)
for i := len(s.ciphers) - 1; i >= 0; i-- {
data, err = s.ciphers[i].Decrypt(data)
if err != nil {
return nil, fmt.Errorf("%w: decryption failed: [%s] %w", ErrIntegrityViolation, s.ciphers[i].Info().Name, err)
}
}
return data, nil
}
// Verify verifies signatures of the given letter.
func (s *Session) Verify(letter *Letter) error {
// debugging:
/*
fmt.Printf("opening: %+v\n", letter)
for _, sig := range letter.Signatures {
fmt.Printf("sig: %+v\n", sig)
}
*/
var err error
if s.wire == nil && letter.Version != 1 {
return fmt.Errorf("unsupported letter version: %d", letter.Version)
}
// ======
// verify
// ======
// TODO: signature verification is run before tool setup. Currently, this is ok, but might change in the future. This might break additional signing algorithms that actually need setup.
data := letter.Data
// build associated data
var associatedData []byte
if len(s.integratedCiphers) > 0 || len(s.macs) > 0 {
associatedData = letter.compileAssociatedData()
}
// Signature
if len(s.signers) > 0 {
associatedSigningData := letter.compileAssociatedSigningData(associatedData)
// run managed signing hashers
if s.managedSigningHashers != nil {
err = s.feedManagedHashers(s.managedSigningHashers, data, associatedSigningData)
if err != nil {
return err
}
defer s.resetManagedHashers(s.managedSigningHashers)
}
// run signers
if len(s.envelope.Senders) != len(letter.Signatures) {
return errors.New("mismatch regarding available signatures and senders")
}
sigIndex := 0
for _, tool := range s.signers {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopSenders(tool.Info().Name, func(signet *Signet) error {
err := tool.Verify(data, associatedSigningData, letter.Signatures[sigIndex].Value, signet)
if err != nil {
return fmt.Errorf("failed to verify signature (%s) with ID %s: %w", tool.Info().Name, letter.Signatures[sigIndex].ID, err)
}
sigIndex++
return nil
})
if err != nil {
return err
}
}
} else {
return errors.New("no signatures to verify")
}
return nil
}
func (s *Session) setupClosingKeyMaterial(letter *Letter) ([][]byte, error) {
signetsUsed := 0
var keyMaterial [][]byte
// add raw keys
_ = s.envelope.LoopSecrets(SignetSchemeKey, func(signet *Signet) error {
letter.Keys = append(letter.Keys, &Seal{
Scheme: SignetSchemeKey,
ID: signet.ID,
})
keyMaterial = append(keyMaterial, signet.Key)
signetsUsed++
return nil
})
// add passwords
err := s.envelope.LoopSecrets(SignetSchemePassword, func(signet *Signet) error {
if len(signet.Key) == 0 {
return fmt.Errorf("signet [%s] is missing it's password", signet.ID)
}
pwKey, err := s.passDerivator.DeriveKeyFromPassword(signet.Key, letter.Nonce)
if err != nil {
return fmt.Errorf("failed to get derive key from password with %s: %w", s.passDerivator.Info().Name, err)
}
letter.Keys = append(letter.Keys, &Seal{
Scheme: SignetSchemePassword,
ID: signet.ID,
})
keyMaterial = append(keyMaterial, pwKey)
signetsUsed++
return nil
})
if err != nil {
return nil, err
}
// add key exchange
for _, tool := range s.keyExchangers {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopRecipients(tool.Info().Name, func(recipient *Signet) error {
// generate new sender exchange signet
senderSignet := NewSignetBase(tool.Definition())
err := senderSignet.GenerateKey()
if err != nil {
return fmt.Errorf("failed to generate new sender signet for %s: %w", tool.Info().Name, err)
}
// create exchange and add to letter
exchKey, err := tool.MakeSharedKey(senderSignet, recipient)
if err != nil {
return fmt.Errorf("failed to make managed key with %s: %w", tool.Info().Name, err)
}
// add to letter
senderRcpt, err := senderSignet.AsRecipient() // convert to public signet
if err != nil {
return fmt.Errorf("failed to get public sender signet for %s: %w", tool.Info().Name, err)
}
err = senderRcpt.StoreKey()
if err != nil {
return fmt.Errorf("failed to serialize sender public key for %s: %w", tool.Info().Name, err)
}
letter.Keys = append(letter.Keys, &Seal{
ID: recipient.ID,
Value: senderRcpt.Key,
})
// save sender signet to state (or burn)
if s.wire == nil {
_ = senderSignet.Burn()
} else {
s.wire.eKXSignets = append(s.wire.eKXSignets, &kxPair{
tool: tool,
signet: senderSignet,
})
}
// add key
keyMaterial = append(keyMaterial, exchKey)
return nil
})
}
if err != nil {
return nil, err
}
// add key encapsulation
for _, tool := range s.keyEncapsulators {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopRecipients(tool.Info().Name, func(recipient *Signet) error {
// save to state
if s.wire != nil {
s.wire.eKESignets = append(s.wire.eKESignets, &kePair{
tool: tool,
})
}
// generate new key
newKey, err := RandomBytes(tool.Helper().DefaultSymmetricKeySize())
if err != nil {
return fmt.Errorf("failed to generate new key for %s: %w", tool.Info().Name, err)
}
// encapsulate key
wrappedKey, err := tool.EncapsulateKey(newKey, recipient)
if err != nil {
return fmt.Errorf("failed to encapsulate key with %s: %w", tool.Info().Name, err)
}
// add to letter
letter.Keys = append(letter.Keys, &Seal{
ID: recipient.ID,
Value: wrappedKey,
})
// add key
keyMaterial = append(keyMaterial, newKey)
return nil
})
}
if err != nil {
return nil, err
}
return keyMaterial, nil
}
func (s *Session) setupOpeningKeyMaterial(letter *Letter) ([][]byte, error) {
// Hint: Signets are loaded from the seals in the letter, so the order will always match.
var keyMaterial [][]byte
sealIndex := 0
// sanity check
if s.wire == nil {
// TODO:
// initial wire handshake is special:
// key encapsulators send two seals in the initial handshake messages
// one of them is added to the recipients
// the other is a new ephermal key
if len(s.envelope.Secrets)+
len(s.envelope.Senders)+
len(s.envelope.Recipients) < len(letter.Keys) {
return nil, fmt.Errorf("missing Keys in letter")
}
}
// add raw keys
_ = s.envelope.LoopSecrets(SignetSchemeKey, func(signet *Signet) error {
keyMaterial = append(keyMaterial, signet.Key)
sealIndex++ // basically just skip, because key has to be loaded from the Signet anyway
return nil
})
// add passwords
err := s.envelope.LoopSecrets(SignetSchemePassword, func(signet *Signet) error {
if len(signet.Key) == 0 {
return fmt.Errorf("signet [%s] is missing it's password", signet.ID)
}
pwKey, err := s.passDerivator.DeriveKeyFromPassword(signet.Key, letter.Nonce)
if err != nil {
return fmt.Errorf("failed to get derive key from password with %s: %w", s.passDerivator.Info().Name, err)
}
keyMaterial = append(keyMaterial, pwKey)
sealIndex++ // basically just skip, because password has to be loaded from the Signet anyway
return nil
})
if err != nil {
return nil, err
}
// add key exchange
for _, tool := range s.keyExchangers {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopRecipients(tool.Info().Name, func(signet *Signet) error {
// get senderRcpt
peerSignet := &Signet{
Version: letter.Version,
tool: tool.Definition(),
Key: letter.Keys[sealIndex].Value,
Public: true,
}
sealIndex++
// load key
err := peerSignet.LoadKey()
if err != nil {
return fmt.Errorf("failed to load ephermal signet for key exchange: %w", err)
}
// save to state
if s.wire != nil {
s.wire.eKXSignets = append(s.wire.eKXSignets, &kxPair{
tool: tool,
peer: peerSignet,
})
}
// make shared key
exchKey, err := tool.MakeSharedKey(signet, peerSignet)
if err != nil {
return fmt.Errorf("failed to make shared key with %s: %w", tool.Info().Name, err)
}
// add key
keyMaterial = append(keyMaterial, exchKey)
return nil
})
}
if err != nil {
return nil, err
}
// add key encapsulation
for _, tool := range s.keyEncapsulators {
//nolint:scopelint // function is executed immediately within loop
err = s.envelope.LoopRecipients(tool.Info().Name, func(signet *Signet) error {
// save to state
if s.wire != nil {
s.wire.eKESignets = append(s.wire.eKESignets, &kePair{
tool: tool,
})
}
unwrappedKey, err := tool.UnwrapKey(letter.Keys[sealIndex].Value, signet)
if err != nil {
return err
}
sealIndex++
// add key
keyMaterial = append(keyMaterial, unwrappedKey)
return nil
})
}
if err != nil {
return nil, err
}
return keyMaterial, nil
}
// setup runs the setup function on all tools.
func (s *Session) setup() error {
for _, tool := range s.toolsWithState {
err := tool.Setup()
if err != nil {
return fmt.Errorf("failed to run tool %s setup: %w", tool.Info().Name, err)
}
}
return nil
}
// reset runs the reset function on all tools and managed hashers.
func (s *Session) reset() error {
// reset all tools
for _, tool := range s.toolsWithState {
err := tool.Reset()
if err != nil {
return fmt.Errorf("failed to run tool %s reset: %w", tool.Info().Name, err)
}
}
return nil
}
func (s *Session) feedManagedHashers(managedHashers map[string]*managedHasher, data, associatedData []byte) error {
for _, mngdHasher := range managedHashers {
n, err := mngdHasher.hash.Write(data)
if err != nil {
return fmt.Errorf("failed to write data to managed hasher %s: %w", mngdHasher.tool.Name, err)
}
if n != len(data) {
return fmt.Errorf("failed to fully write data to managed hasher %s", mngdHasher.tool.Name)
}
n, err = mngdHasher.hash.Write(associatedData)
if err != nil {
return fmt.Errorf("failed to write associated data to managed hasher %s: %w", mngdHasher.tool.Name, err)
}
if n != len(associatedData) {
return fmt.Errorf("failed to fully write associated data to managed hasher %s", mngdHasher.tool.Name)
}
}
return nil
}
func (s *Session) resetManagedHashers(managedHashers map[string]*managedHasher) {
for _, mngdHasher := range managedHashers {
mngdHasher.hash.Reset()
}
}