-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathencoder.go
More file actions
179 lines (165 loc) · 6.48 KB
/
Copy pathencoder.go
File metadata and controls
179 lines (165 loc) · 6.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package cipher
import (
"context"
"errors"
"fmt"
"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/keyservice"
"github.com/dcadolph/cipher/internal/sopsx"
)
// Encoder encrypts a file's bytes for the given path.
type Encoder interface {
// Encode returns the encrypted bytes for data identified by path.
// The path is used for format detection unless the Encoder was
// configured with a fixed format. Implementations should return
// ErrAlreadyEncrypted when data already carries sops metadata
// and ErrEmpty when data has nothing to encrypt.
Encode(ctx context.Context, path string, data []byte) ([]byte, error)
}
// EncoderFunc adapts a plain function to Encoder.
type EncoderFunc func(ctx context.Context, path string, data []byte) ([]byte, error)
// Encode calls f.
func (f EncoderFunc) Encode(ctx context.Context, path string, data []byte) ([]byte, error) {
return f(ctx, path, data)
}
// MACMode controls which leaves are covered by the file MAC.
type MACMode int
const (
// MACInherit defers to the base or parent setting. Zero value.
// In a router-driven config a rule with MACInherit picks up the
// Encoder default. In a single-encoder config MACInherit means
// the sops default (MACOnAll).
MACInherit MACMode = iota
// MACOnAll computes the MAC over every leaf, encrypted or not.
// This is the sops default and the safer choice for tamper
// detection on the unencrypted parts of the file.
MACOnAll
// MACOnEncrypted computes the MAC over encrypted leaves only.
// Use when you intentionally let plaintext leaves change without
// invalidating the MAC.
MACOnEncrypted
)
// EncoderOptions tunes the behavior of an Encoder created with NewEncoderWith.
type EncoderOptions struct {
// Format, when non-zero, fixes the format for every Encode call.
// When zero, format is derived from the path on each call.
Format Format
// EncryptedRegex restricts encryption to keys matching this regex.
EncryptedRegex string
// UnencryptedRegex excludes keys matching this regex from encryption.
UnencryptedRegex string
// EncryptedSuffix restricts encryption to keys with this suffix.
EncryptedSuffix string
// UnencryptedSuffix excludes keys with this suffix from encryption.
UnencryptedSuffix string
// MAC controls which leaves the file MAC covers. Zero value
// (MACInherit) defers to base in a router or to the sops default.
// Set MACOnAll or MACOnEncrypted to lock the mode for this Encoder
// and let router rules override either direction.
MAC MACMode
// ShamirThreshold is the number of key groups required to recover
// the data key. Zero means the sops default.
ShamirThreshold int
// KeyServices overrides the default local key service. Empty means
// a single local key service is used.
KeyServices []keyservice.KeyServiceClient
// Cipher overrides the default AES cipher. Nil means aes.NewCipher().
Cipher sops.Cipher
// Logger receives encode-time events. Nil uses NopLogger.
Logger Logger
// OnEncrypt is called after every successful Encode with the file
// path, plaintext size, and ciphertext size. Nil is a no-op.
OnEncrypt func(path string, plaintextBytes, ciphertextBytes int)
// MaxPlaintextBytes caps the input size. Encode returns an
// ErrInputTooLarge wrapping ErrEncode when len(data) > MaxPlaintextBytes.
// Zero means no cap. Sops loads the whole file into memory before
// emitting, so very large inputs are best detected here.
MaxPlaintextBytes int
}
// ErrInputTooLarge is returned when an Encoder is asked to encrypt
// input that exceeds EncoderOptions.MaxPlaintextBytes.
var ErrInputTooLarge = errors.New("input exceeds MaxPlaintextBytes")
// NewEncoder returns an Encoder backed by sops using sensible defaults:
// AES cipher, local key service, format inferred from each file's path,
// no key-name filters. Panics if kp is nil.
func NewEncoder(kp KeyProvider) Encoder {
return NewEncoderWith(kp, EncoderOptions{})
}
// NewEncoderWith returns an Encoder backed by sops using the supplied
// options. Panics if kp is nil.
func NewEncoderWith(kp KeyProvider, opts EncoderOptions) Encoder {
if kp == nil {
panic("cipher: NewEncoderWith: KeyProvider required")
}
log := opts.Logger
if log == nil {
log = NopLogger
}
return EncoderFunc(func(ctx context.Context, path string, data []byte) ([]byte, error) {
log.Debugf("cipher.Encode start: path=%s bytes=%d", path, len(data))
if opts.MaxPlaintextBytes > 0 && len(data) > opts.MaxPlaintextBytes {
return nil, fmt.Errorf("%w: %w (limit %d, got %d)",
ErrEncode, ErrInputTooLarge, opts.MaxPlaintextBytes, len(data))
}
groups, err := kp.KeyGroups(ctx)
if err != nil {
return nil, fmt.Errorf("%w: key groups: %w", ErrEncode, err)
}
if len(groups) == 0 {
return nil, fmt.Errorf("%w: %w", ErrEncode, ErrNoKeyGroups)
}
out, err := sopsx.Encrypt(sopsx.EncryptInput{
Path: path,
Data: data,
Format: opts.Format,
KeyGroups: groups,
KeyServices: opts.KeyServices,
Cipher: opts.Cipher,
EncryptedRegex: opts.EncryptedRegex,
UnencryptedRegex: opts.UnencryptedRegex,
EncryptedSuffix: opts.EncryptedSuffix,
UnencryptedSuffix: opts.UnencryptedSuffix,
MACOnlyEncrypted: opts.MAC == MACOnEncrypted,
ShamirThreshold: opts.ShamirThreshold,
})
switch {
case errors.Is(err, sopsx.ErrAlreadyEncrypted):
log.Warnf("cipher.Encode skip already-encrypted: path=%s", path)
return nil, ErrAlreadyEncrypted
case errors.Is(err, sopsx.ErrEmpty):
log.Warnf("cipher.Encode skip empty: path=%s", path)
return nil, ErrEmpty
case errors.Is(err, sopsx.ErrNoKeyGroups):
return nil, fmt.Errorf("%w: %w", ErrEncode, ErrNoKeyGroups)
case err != nil:
return nil, fmt.Errorf("%w: %w", ErrEncode, err)
}
log.Debugf("cipher.Encode done: path=%s plaintext=%d ciphertext=%d",
path, len(data), len(out))
if opts.OnEncrypt != nil {
opts.OnEncrypt(path, len(data), len(out))
}
return out, nil
})
}
// ChainEncoders returns an Encoder that feeds the output of each Encoder
// into the next. Useful for composing pre-processing or transformations
// around a sops Encoder.
func ChainEncoders(first Encoder, rest ...Encoder) Encoder {
if first == nil {
panic("cipher: ChainEncoders: first encoder required")
}
return EncoderFunc(func(ctx context.Context, path string, data []byte) ([]byte, error) {
out, err := first.Encode(ctx, path, data)
if err != nil {
return nil, err
}
for _, e := range rest {
out, err = e.Encode(ctx, path, out)
if err != nil {
return nil, err
}
}
return out, nil
})
}