Skip to content

Commit fb28987

Browse files
committed
⚡ Ensure idempotency in load private keys #130
1 parent c28da3a commit fb28987

File tree

1 file changed

+50
-17
lines changed

1 file changed

+50
-17
lines changed

src/Xecrets.Cli/Operation/LoadPrivateKeysOperation.cs

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#endregion Copyright and GPL License
2525

26+
using AxCrypt.Abstractions;
2627
using AxCrypt.Api.Model;
2728
using AxCrypt.Core.Crypto;
2829
using AxCrypt.Core.Extensions;
@@ -71,11 +72,13 @@ private static Status RealAsyncInternal(Parameters parameters)
7172
{
7273
var store = New<IStandardIoDataStore>(parameters.Arg1);
7374
UserAccounts? userAccounts;
75+
string json;
7476
using (StreamReader reader = new StreamReader(store.OpenRead()))
7577
{
78+
json = reader.ReadToEnd();
7679
try
7780
{
78-
userAccounts = UserAccounts.DeserializeFrom(reader);
81+
userAccounts = New<IStringSerializer>().Deserialize<UserAccounts>(json);
7982
}
8083
catch (Exception ex)
8184
{
@@ -97,9 +100,18 @@ private static Status RealAsyncInternal(Parameters parameters)
97100
}
98101

99102
store = New<IStandardIoDataStore>(parameters.Arg2);
103+
100104
using (StreamWriter writer = new StreamWriter(store.OpenWrite()))
101105
{
102-
reEncryptedAccounts.SerializeTo(writer);
106+
// Ensure this operation is idempotent
107+
if (object.ReferenceEquals(userAccounts, reEncryptedAccounts))
108+
{
109+
writer.Write(json);
110+
}
111+
else
112+
{
113+
reEncryptedAccounts.SerializeTo(writer);
114+
}
103115
}
104116
return Status.Success;
105117
}
@@ -111,35 +123,56 @@ private static Status RealAsyncInternal(Parameters parameters)
111123
return userAccounts;
112124
}
113125

126+
Passphrase reEncryptionPassphrase = parameters.Identities.First(i => i.Passphrase != Passphrase.Empty).Passphrase;
127+
114128
List<Passphrase> passphrases = [.. parameters.Identities
115-
.Where(i => i.Passphrase != Passphrase.Empty)
129+
.Where(i => i.Passphrase != Passphrase.Empty && i.Passphrase != reEncryptionPassphrase)
116130
.Select(i => i.Passphrase)];
117131

118-
List<UserKeyPair> userKeyPairs = [];
132+
EmailAddress mainUserEmail = parameters.Identities
133+
.FirstOrDefault(i => i.UserEmail != EmailAddress.Empty)?.UserEmail ?? EmailAddress.Empty;
134+
string userEmail = mainUserEmail == EmailAddress.Empty
135+
? userAccounts.Accounts.First().UserName : mainUserEmail.Address;
136+
137+
List<UserKeyPair> decryptedKeyPairs = [];
119138
List<AccountKey> nonDecryptableAccountKeys = [];
139+
bool statusChanged = userAccounts.Accounts.Where(a => a.UserName != userEmail).Any();
120140
foreach (AccountKey key in userAccounts.Accounts.Select(a => a.AccountKeys).SelectMany(a => a))
121141
{
122-
if (TryDecryptKey(key, passphrases, out UserKeyPair? userKeyPair))
142+
statusChanged |= key.User != userEmail;
143+
if (TryDecryptKey(key, [reEncryptionPassphrase], out UserKeyPair? userKeyPair))
144+
{
145+
decryptedKeyPairs.Add(userKeyPair!);
146+
statusChanged |= key.Status != PrivateKeyStatus.PassphraseKnown;
147+
continue;
148+
}
149+
150+
if (TryDecryptKey(key, passphrases, out userKeyPair))
123151
{
124-
userKeyPairs.Add(userKeyPair!);
152+
decryptedKeyPairs.Add(userKeyPair!);
153+
statusChanged = true;
125154
continue;
126155
}
156+
127157
nonDecryptableAccountKeys.Add(key);
158+
statusChanged |= key.Status != PrivateKeyStatus.PassphraseUnknown;
128159
}
129-
userKeyPairs = [.. userKeyPairs.OrderByDescending(k => k.Timestamp)];
130-
if (userKeyPairs.Count != 0)
160+
161+
decryptedKeyPairs = [.. decryptedKeyPairs.OrderByDescending(k => k.Timestamp)];
162+
if (decryptedKeyPairs.Count != 0)
131163
{
132-
parameters.Identities.Add(new LogOnIdentity(userKeyPairs, Passphrase.Empty));
164+
parameters.Identities.Add(new LogOnIdentity(decryptedKeyPairs, Passphrase.Empty));
133165
}
166+
134167
if (parameters.Arg2.Length == 0)
135168
{
136169
return null;
137170
}
138-
139-
EmailAddress mainUserEmail = parameters.Identities
140-
.FirstOrDefault(i => i.UserEmail != EmailAddress.Empty)?.UserEmail ?? EmailAddress.Empty;
141-
string userEmail = mainUserEmail == EmailAddress.Empty
142-
? userAccounts.Accounts.First().UserName : mainUserEmail.Address;
171+
if (!statusChanged)
172+
{
173+
// Ensure idempotency
174+
return userAccounts;
175+
}
143176

144177
UserAccount reEncryptedAccount = new UserAccount(userEmail)
145178
{
@@ -149,14 +182,14 @@ private static Status RealAsyncInternal(Parameters parameters)
149182
Tag = null!,
150183
Signature = null!,
151184
};
152-
Passphrase firstPassphrase = parameters.Identities.First(i => i.Passphrase != Passphrase.Empty).Passphrase;
153-
foreach (UserKeyPair keyPair in userKeyPairs)
185+
foreach (UserKeyPair keyPair in decryptedKeyPairs)
154186
{
155-
reEncryptedAccount.AccountKeys.Add(keyPair.ToAccountKey(firstPassphrase));
187+
reEncryptedAccount.AccountKeys.Add(keyPair.ToAccountKey(reEncryptionPassphrase));
156188
}
157189
foreach (AccountKey key in nonDecryptableAccountKeys)
158190
{
159191
key.Status = PrivateKeyStatus.PassphraseUnknown;
192+
key.User = userEmail;
160193
reEncryptedAccount.AccountKeys.Add(key);
161194
}
162195
UserAccounts reEncryptedAccounts = new()

0 commit comments

Comments
 (0)