2323
2424#endregion Copyright and GPL License
2525
26+ using AxCrypt . Abstractions ;
2627using AxCrypt . Api . Model ;
2728using AxCrypt . Core . Crypto ;
2829using 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