-
Notifications
You must be signed in to change notification settings - Fork 894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for age. #688
Merged
Merged
Add support for age. #688
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
e8d0004
Add support for age.
jimmycuadra 9ca46d0
Use idiomatic style for assignment and error handling.
jimmycuadra 9e4cbc9
Allow age key dir to be set with SOPS_AGE_KEY_DIR and add tests.
jimmycuadra 2741ab5
Use user config dir instead of home dir as the root for age keys.
jimmycuadra d9b196c
Determine age package path using current file rather than pwd.
jimmycuadra 5c171c8
Don't swallow potential errors from os.Stat.
jimmycuadra 617db43
Use a single keys.txt file for age private keys.
jimmycuadra 7f7ecbc
Try decrypting with all possible keys in the keyfile.
jimmycuadra ade5692
Document age usage.
jimmycuadra a66a0a8
Reorder README sections and fix RST link.
jimmycuadra 6a6a936
Use more concise style for constructing map.
jimmycuadra 1dbea5d
Fix whitespace errors.
jimmycuadra 6068838
Update go.mod/go.sum.
jimmycuadra 50a89c8
age: .sops.yaml support
colemickens 8f6271f
age: MasterKeysFromRecipients: gracefully handle empty string
colemickens e9acafc
Update to age 1.0.0-beta5.
jimmycuadra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# created: 2020-07-18T03:16:47-07:00 | ||
# public key: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw | ||
AGE-SECRET-KEY-1NJT5YCS2LWU4V4QAJQ6R4JNU7LXPDX602DZ9NUFANVU5GDTGUWCQ5T59M6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package age | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"filippo.io/age" | ||
"github.com/sirupsen/logrus" | ||
"go.mozilla.org/sops/v3/logging" | ||
) | ||
|
||
var log *logrus.Logger | ||
|
||
func init() { | ||
log = logging.NewLogger("AGE") | ||
} | ||
|
||
const privateKeySizeLimit = 1 << 24 // 16 MiB | ||
|
||
// MasterKey is an age key used to encrypt and decrypt sops' data key. | ||
type MasterKey struct { | ||
Identity string // a Bech32-encoded private key | ||
Recipient string // a Bech32-encoded public key | ||
EncryptedKey string // a sops data key encrypted with age | ||
|
||
parsedIdentity *age.X25519Identity // a parsed age private key | ||
parsedRecipient *age.X25519Recipient // a parsed age public key | ||
} | ||
|
||
// Encrypt takes a sops data key, encrypts it with age and stores the result in the EncryptedKey field. | ||
func (key *MasterKey) Encrypt(datakey []byte) error { | ||
buffer := &bytes.Buffer{} | ||
|
||
if key.parsedRecipient == nil { | ||
parsedRecipient, err := parseRecipient(key.Recipient) | ||
|
||
if err != nil { | ||
log.WithField("recipient", key.parsedRecipient).Error("Encryption failed") | ||
return err | ||
} | ||
|
||
key.parsedRecipient = parsedRecipient | ||
} | ||
|
||
w, err := age.Encrypt(buffer, key.parsedRecipient) | ||
if err != nil { | ||
return fmt.Errorf("failed to open file for encrypting sops data key with age: %v", err) | ||
} | ||
|
||
if _, err := w.Write(datakey); err != nil { | ||
log.WithField("recipient", key.parsedRecipient).Error("Encryption failed") | ||
return fmt.Errorf("failed to encrypt sops data key with age: %v", err) | ||
} | ||
|
||
if err := w.Close(); err != nil { | ||
log.WithField("recipient", key.parsedRecipient).Error("Encryption failed") | ||
return fmt.Errorf("failed to close file for encrypting sops data key with age: %v", err) | ||
} | ||
|
||
key.EncryptedKey = buffer.String() | ||
|
||
log.WithField("recipient", key.parsedRecipient).Info("Encryption succeeded") | ||
|
||
return nil | ||
} | ||
|
||
// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet. | ||
func (key *MasterKey) EncryptIfNeeded(datakey []byte) error { | ||
if key.EncryptedKey == "" { | ||
return key.Encrypt(datakey) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// EncryptedDataKey returns the encrypted data key this master key holds. | ||
func (key *MasterKey) EncryptedDataKey() []byte { | ||
return []byte(key.EncryptedKey) | ||
} | ||
|
||
// SetEncryptedDataKey sets the encrypted data key for this master key. | ||
func (key *MasterKey) SetEncryptedDataKey(enc []byte) { | ||
key.EncryptedKey = string(enc) | ||
} | ||
|
||
// Decrypt decrypts the EncryptedKey field with the age identity and returns the result. | ||
func (key *MasterKey) Decrypt() ([]byte, error) { | ||
ageKeyFilePath, ok := os.LookupEnv("SOPS_AGE_KEY_FILE") | ||
|
||
if !ok { | ||
userConfigDir, err := os.UserConfigDir() | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("user config directory could not be determined: %v", err) | ||
} | ||
|
||
ageKeyFilePath = filepath.Join(userConfigDir, "sops", "age", "keys.txt") | ||
} | ||
|
||
ageKeyFile, err := os.Open(ageKeyFilePath) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to open file: %v", err) | ||
} | ||
|
||
defer ageKeyFile.Close() | ||
|
||
identities, err := age.ParseIdentities(ageKeyFile) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
buffer := &bytes.Buffer{} | ||
reader := bytes.NewReader([]byte(key.EncryptedKey)) | ||
r, err := age.Decrypt(reader, identities...) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("no age identity found in %q that could decrypt the data", ageKeyFilePath) | ||
} | ||
|
||
if _, err := io.Copy(buffer, r); err != nil { | ||
return nil, fmt.Errorf("failed to copy decrypted data into bytes.Buffer") | ||
} | ||
|
||
return buffer.Bytes(), nil | ||
} | ||
|
||
// NeedsRotation returns whether the data key needs to be rotated or not. | ||
func (key *MasterKey) NeedsRotation() bool { | ||
return false | ||
} | ||
|
||
// ToString converts the key to a string representation. | ||
func (key *MasterKey) ToString() string { | ||
return key.Recipient | ||
} | ||
|
||
// ToMap converts the MasterKey to a map for serialization purposes. | ||
func (key *MasterKey) ToMap() map[string]interface{} { | ||
return map[string]interface{}{"recipient": key.Recipient, "enc": key.EncryptedKey} | ||
} | ||
|
||
// MasterKeysFromRecipients takes a comma-separated list of Bech32-encoded public keys and returns a | ||
// slice of new MasterKeys. | ||
func MasterKeysFromRecipients(commaSeparatedRecipients string) ([]*MasterKey, error) { | ||
if commaSeparatedRecipients == "" { | ||
// otherwise Split returns [""] and MasterKeyFromRecipient is unhappy | ||
return make([]*MasterKey, 0), nil | ||
} | ||
recipients := strings.Split(commaSeparatedRecipients, ",") | ||
|
||
var keys []*MasterKey | ||
|
||
for _, recipient := range recipients { | ||
key, err := MasterKeyFromRecipient(recipient) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
keys = append(keys, key) | ||
} | ||
|
||
return keys, nil | ||
} | ||
|
||
// MasterKeyFromRecipient takes a Bech32-encoded public key and returns a new MasterKey. | ||
func MasterKeyFromRecipient(recipient string) (*MasterKey, error) { | ||
parsedRecipient, err := parseRecipient(recipient) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &MasterKey{ | ||
Recipient: recipient, | ||
parsedRecipient: parsedRecipient, | ||
}, nil | ||
} | ||
|
||
// parseRecipient attempts to parse a string containing an encoded age public key | ||
func parseRecipient(recipient string) (*age.X25519Recipient, error) { | ||
parsedRecipient, err := age.ParseX25519Recipient(recipient) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %v", err) | ||
} | ||
|
||
return parsedRecipient, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package age | ||
|
||
import ( | ||
"os" | ||
"path" | ||
"runtime" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestMasterKeysFromRecipientsEmpty(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
commaSeparatedRecipients := "" | ||
recipients, err := MasterKeysFromRecipients(commaSeparatedRecipients) | ||
|
||
assert.NoError(err) | ||
|
||
assert.Equal(recipients, make([]*MasterKey,0)) | ||
} | ||
|
||
func TestAge(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
key, err := MasterKeyFromRecipient("age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw") | ||
|
||
assert.NoError(err) | ||
assert.Equal("age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw", key.ToString()) | ||
|
||
dataKey := []byte("abcdefghijklmnopqrstuvwxyz123456") | ||
|
||
err = key.Encrypt(dataKey) | ||
assert.NoError(err) | ||
|
||
_, filename, _, _ := runtime.Caller(0) | ||
err = os.Setenv("SOPS_AGE_KEY_FILE", path.Join(path.Dir(filename), "keys.txt")) | ||
assert.NoError(err) | ||
|
||
decryptedKey, err := key.Decrypt() | ||
assert.NoError(err) | ||
assert.Equal(dataKey, decryptedKey) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good middle ground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. Good work!