Skip to content

Commit 97311b5

Browse files
feat(KeeShare): Add export, Android UI, and sync integration
Major enhancements to make PR PhilippC#3130 superior to PhilippC#3106: - Add KeeShareExporter.cs: Export groups to .kdbx and signed .share containers - Add ConfigureKeeShareActivity.cs: Dashboard for KeeShare configurations - Add EditKeeShareActivity.cs: Per-group configuration (mode/path/password) - Add TrustSignerDialog.cs: SHA-256 fingerprint trust verification UI - Add 4 XML layouts for new activities - Add 44 KeeShare string resources - Integrate KeeShare menu in GroupBaseActivity - Add sync hooks in SyncUtil.cs to auto-trigger imports Key differentiator: Proper trust model with SHA-256 fingerprints and Trust Once/Trust Permanently/Reject options that PhilippC#3106 lacks.
1 parent e3130c5 commit 97311b5

12 files changed

Lines changed: 1739 additions & 1 deletion

File tree

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using System.Xml.Linq;
7+
using KeePassLib;
8+
using KeePassLib.Interfaces;
9+
using KeePassLib.Keys;
10+
using KeePassLib.Serialization;
11+
12+
namespace keepass2android.KeeShare
13+
{
14+
/// <summary>
15+
/// KeeShare operation mode for a group
16+
/// </summary>
17+
public enum KeeShareMode
18+
{
19+
/// <summary>Import entries from share file into this group</summary>
20+
Import,
21+
/// <summary>Export entries from this group to share file</summary>
22+
Export,
23+
/// <summary>Bidirectional sync between group and share file</summary>
24+
Synchronize
25+
}
26+
27+
/// <summary>
28+
/// Result of a KeeShare export operation
29+
/// </summary>
30+
public class KeeShareExportResult
31+
{
32+
public bool IsSuccess { get; set; }
33+
public string ErrorMessage { get; set; }
34+
public string SharePath { get; set; }
35+
public int EntriesExported { get; set; }
36+
public DateTime ExportTime { get; set; }
37+
}
38+
39+
/// <summary>
40+
/// Exports KeePass groups to .kdbx or signed .share containers
41+
/// </summary>
42+
public class KeeShareExporter
43+
{
44+
/// <summary>
45+
/// Exports a group to a .kdbx file (uncontainerized)
46+
/// </summary>
47+
public static KeeShareExportResult ExportToKdbx(
48+
PwDatabase sourceDb,
49+
PwGroup groupToExport,
50+
string targetPath,
51+
CompositeKey targetKey,
52+
IStatusLogger logger = null)
53+
{
54+
var result = new KeeShareExportResult
55+
{
56+
SharePath = targetPath,
57+
ExportTime = DateTime.UtcNow
58+
};
59+
60+
try
61+
{
62+
// Create a new database with only the target group content
63+
var exportDb = new PwDatabase();
64+
exportDb.New(new IOConnectionInfo(), targetKey);
65+
66+
// Copy database settings
67+
exportDb.Name = groupToExport.Name;
68+
exportDb.Description = $"KeeShare export from {sourceDb.Name}";
69+
70+
// Copy group content to root
71+
CopyGroupContent(groupToExport, exportDb.RootGroup, sourceDb, exportDb);
72+
73+
// Count exported entries
74+
result.EntriesExported = CountEntries(exportDb.RootGroup);
75+
76+
// Save the database
77+
var ioc = IOConnectionInfo.FromPath(targetPath);
78+
exportDb.SaveAs(ioc, false, logger);
79+
80+
result.IsSuccess = true;
81+
Kp2aLog.Log($"KeeShare: Exported {result.EntriesExported} entries to {targetPath}");
82+
}
83+
catch (Exception ex)
84+
{
85+
result.IsSuccess = false;
86+
result.ErrorMessage = ex.Message;
87+
Kp2aLog.Log($"KeeShare: Export failed: {ex.Message}");
88+
}
89+
90+
return result;
91+
}
92+
93+
/// <summary>
94+
/// Exports a group to a signed .share container
95+
/// </summary>
96+
public static KeeShareExportResult ExportToContainer(
97+
PwDatabase sourceDb,
98+
PwGroup groupToExport,
99+
string targetPath,
100+
CompositeKey innerKey,
101+
RSAParameters privateKey,
102+
string signerName,
103+
IStatusLogger logger = null)
104+
{
105+
var result = new KeeShareExportResult
106+
{
107+
SharePath = targetPath,
108+
ExportTime = DateTime.UtcNow
109+
};
110+
111+
try
112+
{
113+
// First export to a temp .kdbx
114+
using (var tempStream = new MemoryStream())
115+
{
116+
// Create export database
117+
var exportDb = new PwDatabase();
118+
exportDb.New(new IOConnectionInfo(), innerKey);
119+
exportDb.Name = groupToExport.Name;
120+
121+
// Copy content
122+
CopyGroupContent(groupToExport, exportDb.RootGroup, sourceDb, exportDb);
123+
result.EntriesExported = CountEntries(exportDb.RootGroup);
124+
125+
// Save to memory stream
126+
var format = new KdbxFile(exportDb);
127+
format.Save(tempStream, null, KdbxFormat.Default, logger);
128+
var kdbxData = tempStream.ToArray();
129+
130+
// Sign the data
131+
var signature = SignData(kdbxData, privateKey);
132+
var signatureXml = CreateSignatureXml(signature, signerName, privateKey);
133+
134+
// Create the .share container (ZIP with signature.xml and db.kdbx)
135+
CreateShareContainer(targetPath, kdbxData, signatureXml);
136+
}
137+
138+
result.IsSuccess = true;
139+
Kp2aLog.Log($"KeeShare: Exported {result.EntriesExported} entries to container {targetPath}");
140+
}
141+
catch (Exception ex)
142+
{
143+
result.IsSuccess = false;
144+
result.ErrorMessage = ex.Message;
145+
Kp2aLog.Log($"KeeShare: Container export failed: {ex.Message}");
146+
}
147+
148+
return result;
149+
}
150+
151+
/// <summary>
152+
/// Creates the RSA signature for the database content
153+
/// </summary>
154+
private static byte[] SignData(byte[] data, RSAParameters privateKey)
155+
{
156+
using (var rsa = RSA.Create())
157+
{
158+
rsa.ImportParameters(privateKey);
159+
return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
160+
}
161+
}
162+
163+
/// <summary>
164+
/// Creates the signature.xml content for the .share container
165+
/// </summary>
166+
private static string CreateSignatureXml(byte[] signature, string signerName, RSAParameters key)
167+
{
168+
// Serialize public key in SSH format
169+
var publicKeyBase64 = SerializePublicKeyToSsh(key);
170+
var signatureBase64 = Convert.ToBase64String(signature);
171+
172+
var doc = new XDocument(
173+
new XElement("KeeShare",
174+
new XElement("Signature", signatureBase64),
175+
new XElement("Certificate",
176+
new XElement("Signer", signerName),
177+
new XElement("Key", publicKeyBase64)
178+
)
179+
)
180+
);
181+
182+
return doc.ToString();
183+
}
184+
185+
/// <summary>
186+
/// Serializes RSA public key to SSH format (ssh-rsa)
187+
/// </summary>
188+
private static string SerializePublicKeyToSsh(RSAParameters key)
189+
{
190+
using (var ms = new MemoryStream())
191+
using (var writer = new BinaryWriter(ms))
192+
{
193+
// Write "ssh-rsa" type
194+
WriteBytes(writer, Encoding.UTF8.GetBytes("ssh-rsa"));
195+
// Write exponent
196+
WriteBytes(writer, key.Exponent);
197+
// Write modulus
198+
WriteBytes(writer, key.Modulus);
199+
200+
return Convert.ToBase64String(ms.ToArray());
201+
}
202+
}
203+
204+
/// <summary>
205+
/// Writes length-prefixed bytes (big-endian uint32 length)
206+
/// </summary>
207+
private static void WriteBytes(BinaryWriter writer, byte[] data)
208+
{
209+
var lenBytes = BitConverter.GetBytes((uint)data.Length);
210+
if (BitConverter.IsLittleEndian)
211+
Array.Reverse(lenBytes);
212+
writer.Write(lenBytes);
213+
writer.Write(data);
214+
}
215+
216+
/// <summary>
217+
/// Creates a .share ZIP container with signature and database
218+
/// </summary>
219+
private static void CreateShareContainer(string path, byte[] kdbxData, string signatureXml)
220+
{
221+
using (var fs = new FileStream(path, FileMode.Create))
222+
using (var archive = new ZipArchive(fs, ZipArchiveMode.Create))
223+
{
224+
// Add signature.xml
225+
var sigEntry = archive.CreateEntry("signature.xml", CompressionLevel.Optimal);
226+
using (var sigStream = sigEntry.Open())
227+
using (var writer = new StreamWriter(sigStream, Encoding.UTF8))
228+
{
229+
writer.Write(signatureXml);
230+
}
231+
232+
// Add database.kdbx
233+
var dbEntry = archive.CreateEntry("database.kdbx", CompressionLevel.Optimal);
234+
using (var dbStream = dbEntry.Open())
235+
{
236+
dbStream.Write(kdbxData, 0, kdbxData.Length);
237+
}
238+
}
239+
}
240+
241+
/// <summary>
242+
/// Copies content from source group to target group
243+
/// </summary>
244+
private static void CopyGroupContent(PwGroup source, PwGroup target, PwDatabase sourceDb, PwDatabase targetDb)
245+
{
246+
// Copy group properties
247+
target.Name = source.Name;
248+
target.Notes = source.Notes;
249+
target.IconId = source.IconId;
250+
target.CustomIconUuid = source.CustomIconUuid;
251+
252+
// Copy entries (clone them)
253+
foreach (var entry in source.Entries)
254+
{
255+
var clone = entry.CloneDeep();
256+
clone.SetUuid(entry.Uuid, false); // Keep same UUID for sync
257+
target.AddEntry(clone, true);
258+
}
259+
260+
// Recursively copy subgroups
261+
foreach (var subGroup in source.Groups)
262+
{
263+
// Skip KeeShare settings group
264+
if (subGroup.Name == "KeeShare" && subGroup.Notes.Contains("KeeShare.Settings"))
265+
continue;
266+
267+
var newSubGroup = new PwGroup(true, true, subGroup.Name, subGroup.IconId);
268+
target.AddGroup(newSubGroup, true);
269+
CopyGroupContent(subGroup, newSubGroup, sourceDb, targetDb);
270+
}
271+
}
272+
273+
/// <summary>
274+
/// Counts entries recursively
275+
/// </summary>
276+
private static int CountEntries(PwGroup group)
277+
{
278+
int count = group.Entries.UCount;
279+
foreach (var subGroup in group.Groups)
280+
{
281+
count += CountEntries(subGroup);
282+
}
283+
return (int)count;
284+
}
285+
286+
/// <summary>
287+
/// Generates a new RSA key pair for signing
288+
/// </summary>
289+
public static RSAParameters GenerateKeyPair(out RSAParameters privateKey)
290+
{
291+
using (var rsa = RSA.Create(2048))
292+
{
293+
privateKey = rsa.ExportParameters(true);
294+
return rsa.ExportParameters(false); // Public key only
295+
}
296+
}
297+
298+
/// <summary>
299+
/// Computes SHA-256 fingerprint of a public key
300+
/// </summary>
301+
public static string ComputeKeyFingerprint(RSAParameters publicKey)
302+
{
303+
var keyBytes = Encoding.UTF8.GetBytes(
304+
Convert.ToBase64String(publicKey.Modulus) +
305+
Convert.ToBase64String(publicKey.Exponent));
306+
307+
using (var sha256 = SHA256.Create())
308+
{
309+
var hash = sha256.ComputeHash(keyBytes);
310+
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
311+
}
312+
}
313+
}
314+
}

0 commit comments

Comments
 (0)