@@ -875,11 +875,21 @@ final class KDBXXMLParser: NSObject, XMLParserDelegate {
875875
876876 case " Value " :
877877 if inValue {
878- var val = currentText. trimmingCharacters ( in: . whitespacesAndNewlines)
879- if isProtected, let decoded = Data ( base64Encoded: val) {
880- val = decryptProtectedValue ( decoded)
878+ // Preserve leading/trailing whitespace in stored values — a
879+ // username or custom field may intentionally contain spaces,
880+ // and trimming here silently destroys that data. Only the
881+ // base64 ciphertext of a protected value needs trimming before
882+ // decoding.
883+ if isProtected {
884+ let base64 = currentText. trimmingCharacters ( in: . whitespacesAndNewlines)
885+ if let decoded = Data ( base64Encoded: base64) {
886+ currentValue = decryptProtectedValue ( decoded)
887+ } else {
888+ currentValue = currentText
889+ }
890+ } else {
891+ currentValue = currentText
881892 }
882- currentValue = val
883893 inValue = false
884894 }
885895
@@ -925,6 +935,10 @@ final class KDBXXMLParser: NSObject, XMLParserDelegate {
925935
926936 case " Tags " :
927937 if !isInsideHistory( ) , let entry = currentEntry {
938+ // Track element presence separately from content so that an
939+ // empty `<Tags></Tags>` element round-trips instead of being
940+ // silently dropped on save.
941+ entry. hasTagsElement = true
928942 let trimmed = currentText. trimmingCharacters ( in: . whitespacesAndNewlines)
929943 if !trimmed. isEmpty {
930944 entry. tags = trimmed. components ( separatedBy: CharacterSet ( [ " , " , " ; " ] ) ) . map {
@@ -1174,14 +1188,12 @@ final class KDBXXMLParser: NSObject, XMLParserDelegate {
11741188 }
11751189
11761190 private func parseKPDate( _ string: String ) -> Date ? {
1177- // KDBX4 can use base64-encoded binary date or ISO 8601
1178- if string. contains ( " - " ) || string. contains ( " T " ) {
1179- let formatter = ISO8601DateFormatter ( )
1180- formatter. formatOptions = [ . withInternetDateTime, . withFractionalSeconds]
1181- return formatter. date ( from: string) ?? ISO8601DateFormatter ( ) . date ( from: string)
1182- }
1183- // Base64 binary timestamp (seconds since 0001-01-01)
1184- if let data = Data ( base64Encoded: string) , data. count == 8 {
1191+ // KDBX4 stores timestamps as 8 bytes of little-endian seconds since
1192+ // year 0001, base64-encoded (always 12 characters with padding).
1193+ // Try this form first — base64 strings can legitimately contain the
1194+ // characters 'T' and '-', so a substring check is not a reliable way
1195+ // to distinguish binary from ISO-8601 form.
1196+ if string. count == 12 , let data = Data ( base64Encoded: string) , data. count == 8 {
11851197 let seconds = data. withUnsafeBytes { $0. loadUnaligned ( as: Int64 . self) . littleEndian }
11861198 guard let kpEpoch = DateComponents (
11871199 calendar: . init( identifier: . gregorian) ,
@@ -1193,7 +1205,10 @@ final class KDBXXMLParser: NSObject, XMLParserDelegate {
11931205 }
11941206 return kpEpoch. addingTimeInterval ( TimeInterval ( seconds) )
11951207 }
1196- return nil
1208+ // KDBX 3.x and some KDBX 4 writers use ISO-8601 text form.
1209+ let formatter = ISO8601DateFormatter ( )
1210+ formatter. formatOptions = [ . withInternetDateTime, . withFractionalSeconds]
1211+ return formatter. date ( from: string) ?? ISO8601DateFormatter ( ) . date ( from: string)
11971212 }
11981213}
11991214
@@ -1208,6 +1223,7 @@ private class EntryBuilder {
12081223 var notes = " "
12091224 var iconID = 0
12101225 var tags : [ String ] = [ ]
1226+ var hasTagsElement = false
12111227 var customFields : [ String : String ] = [ : ]
12121228 var protectedStringKeys : Set < String > = [ ]
12131229 var otpURL : String ?
@@ -1229,8 +1245,10 @@ private class EntryBuilder {
12291245 notes: notes,
12301246 iconID: iconID,
12311247 tags: tags,
1248+ hasTagsElement: hasTagsElement,
12321249 customFields: customFields. filter { !$0. key. hasPrefix ( " TimeOtp- " ) && $0. key != " TOTP Settings " && $0. key != " TOTP Seed " } ,
12331250 totpConfig: totpConfig,
1251+ otpURL: otpURL,
12341252 creationTime: creationTime,
12351253 lastModificationTime: lastModificationTime,
12361254 unknownXML: unknownXML,
0 commit comments