feat(identity): added --add-recipient and --remove-recipient flags for multi-recipient keys
This commit is contained in:
parent
f9ff2c0d62
commit
579e6a1eee
12 changed files with 575 additions and 51 deletions
|
|
@ -46,7 +46,7 @@ func TestEncryptDecryptRoundtrip(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadIdentityMissing(t *testing.T) {
|
||||
t.Setenv("PDA_CONFIG", t.TempDir())
|
||||
t.Setenv("PDA_DATA", t.TempDir())
|
||||
id, err := loadIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -58,7 +58,7 @@ func TestLoadIdentityMissing(t *testing.T) {
|
|||
|
||||
func TestEnsureIdentityCreatesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_CONFIG", dir)
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
|
||||
id, err := ensureIdentity()
|
||||
if err != nil {
|
||||
|
|
@ -89,7 +89,7 @@ func TestEnsureIdentityCreatesFile(t *testing.T) {
|
|||
|
||||
func TestEnsureIdentityIdempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_CONFIG", dir)
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
|
||||
id1, err := ensureIdentity()
|
||||
if err != nil {
|
||||
|
|
@ -109,7 +109,7 @@ func TestSecretEntryRoundtrip(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recipient := id.Recipient()
|
||||
recipients := []age.Recipient{id.Recipient()}
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.ndjson")
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ func TestSecretEntryRoundtrip(t *testing.T) {
|
|||
{Key: "encrypted", Value: []byte("secret-value"), Secret: true},
|
||||
}
|
||||
|
||||
if err := writeStoreFile(path, entries, recipient); err != nil {
|
||||
if err := writeStoreFile(path, entries, recipients); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
@ -153,14 +153,14 @@ func TestSecretEntryLockedWithoutIdentity(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recipient := id.Recipient()
|
||||
recipients := []age.Recipient{id.Recipient()}
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.ndjson")
|
||||
|
||||
entries := []Entry{
|
||||
{Key: "encrypted", Value: []byte("secret-value"), Secret: true},
|
||||
}
|
||||
if err := writeStoreFile(path, entries, recipient); err != nil {
|
||||
if err := writeStoreFile(path, entries, recipients); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ func TestLockedPassthrough(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recipient := id.Recipient()
|
||||
recipients := []age.Recipient{id.Recipient()}
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.ndjson")
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ func TestLockedPassthrough(t *testing.T) {
|
|||
entries := []Entry{
|
||||
{Key: "encrypted", Value: []byte("secret-value"), Secret: true},
|
||||
}
|
||||
if err := writeStoreFile(path, entries, recipient); err != nil {
|
||||
if err := writeStoreFile(path, entries, recipients); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
@ -224,9 +224,240 @@ func TestLockedPassthrough(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMultiRecipientEncryptDecrypt(t *testing.T) {
|
||||
id1, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
recipients := []age.Recipient{id1.Recipient(), id2.Recipient()}
|
||||
plaintext := []byte("shared secret")
|
||||
|
||||
ciphertext, err := encrypt(plaintext, recipients...)
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt: %v", err)
|
||||
}
|
||||
|
||||
// Both identities should be able to decrypt
|
||||
for i, id := range []*age.X25519Identity{id1, id2} {
|
||||
got, err := decrypt(ciphertext, id)
|
||||
if err != nil {
|
||||
t.Fatalf("identity %d decrypt: %v", i, err)
|
||||
}
|
||||
if string(got) != string(plaintext) {
|
||||
t.Errorf("identity %d: got %q, want %q", i, got, plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiRecipientStoreRoundtrip(t *testing.T) {
|
||||
id1, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
recipients := []age.Recipient{id1.Recipient(), id2.Recipient()}
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.ndjson")
|
||||
|
||||
entries := []Entry{
|
||||
{Key: "secret", Value: []byte("multi-recipient-value"), Secret: true},
|
||||
}
|
||||
if err := writeStoreFile(path, entries, recipients); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Both identities should decrypt the store
|
||||
for i, id := range []*age.X25519Identity{id1, id2} {
|
||||
got, err := readStoreFile(path, id)
|
||||
if err != nil {
|
||||
t.Fatalf("identity %d read: %v", i, err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("identity %d: got %d entries, want 1", i, len(got))
|
||||
}
|
||||
if string(got[0].Value) != "multi-recipient-value" {
|
||||
t.Errorf("identity %d: value = %q, want %q", i, got[0].Value, "multi-recipient-value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRecipientsMissing(t *testing.T) {
|
||||
t.Setenv("PDA_DATA", t.TempDir())
|
||||
recipients, err := loadRecipients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if recipients != nil {
|
||||
t.Fatal("expected nil recipients for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveLoadRecipientsRoundtrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
|
||||
id1, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
toSave := []*age.X25519Recipient{id1.Recipient(), id2.Recipient()}
|
||||
if err := saveRecipients(toSave); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check file permissions
|
||||
path := filepath.Join(dir, "recipients.txt")
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("recipients file not created: %v", err)
|
||||
}
|
||||
if perm := info.Mode().Perm(); perm != 0o600 {
|
||||
t.Errorf("recipients file permissions = %o, want 0600", perm)
|
||||
}
|
||||
|
||||
loaded, err := loadRecipients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(loaded) != 2 {
|
||||
t.Fatalf("got %d recipients, want 2", len(loaded))
|
||||
}
|
||||
if loaded[0].String() != id1.Recipient().String() {
|
||||
t.Errorf("recipient 0 = %s, want %s", loaded[0], id1.Recipient())
|
||||
}
|
||||
if loaded[1].String() != id2.Recipient().String() {
|
||||
t.Errorf("recipient 1 = %s, want %s", loaded[1], id2.Recipient())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRecipientsEmptyDeletesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
|
||||
// Create a recipients file first
|
||||
id, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := saveRecipients([]*age.X25519Recipient{id.Recipient()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Save empty list should delete the file
|
||||
if err := saveRecipients(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "recipients.txt")
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Error("expected recipients file to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllRecipientsNoIdentityNoFile(t *testing.T) {
|
||||
t.Setenv("PDA_DATA", t.TempDir())
|
||||
recipients, err := allRecipients(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if recipients != nil {
|
||||
t.Fatal("expected nil recipients")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllRecipientsCombines(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
|
||||
id, err := ensureIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
extra, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := saveRecipients([]*age.X25519Recipient{extra.Recipient()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
recipients, err := allRecipients(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(recipients) != 2 {
|
||||
t.Fatalf("got %d recipients, want 2", len(recipients))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReencryptAllStores(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
|
||||
id, err := ensureIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write a store with a secret
|
||||
storePath := filepath.Join(dir, "test.ndjson")
|
||||
entries := []Entry{
|
||||
{Key: "plain", Value: []byte("hello")},
|
||||
{Key: "secret", Value: []byte("secret-value"), Secret: true},
|
||||
}
|
||||
if err := writeStoreFile(storePath, entries, []age.Recipient{id.Recipient()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a second identity and re-encrypt for both
|
||||
id2, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newRecipients := []age.Recipient{id.Recipient(), id2.Recipient()}
|
||||
|
||||
count, err := reencryptAllStores(id, newRecipients)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("re-encrypted %d secrets, want 1", count)
|
||||
}
|
||||
|
||||
// Both identities should be able to decrypt
|
||||
for i, identity := range []*age.X25519Identity{id, id2} {
|
||||
got, err := readStoreFile(storePath, identity)
|
||||
if err != nil {
|
||||
t.Fatalf("identity %d read: %v", i, err)
|
||||
}
|
||||
idx := findEntry(got, "secret")
|
||||
if idx < 0 {
|
||||
t.Fatalf("identity %d: secret key not found", i)
|
||||
}
|
||||
if string(got[idx].Value) != "secret-value" {
|
||||
t.Errorf("identity %d: value = %q, want %q", i, got[idx].Value, "secret-value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateTestIdentity(t *testing.T) (*age.X25519Identity, error) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PDA_CONFIG", dir)
|
||||
t.Setenv("PDA_DATA", dir)
|
||||
return ensureIdentity()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue