Skip to content

Commit 47cca82

Browse files
committed
Add SQLCipher encrypted SQLite support
1 parent b777ceb commit 47cca82

5 files changed

Lines changed: 211 additions & 62 deletions

File tree

‎QuickLook.Plugin/QuickLook.Plugin.DbViewer/DbViewerPanel.xaml.cs‎

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public partial class DbViewerPanel : UserControl
1616
{
1717
private const int DefaultPageSize = 200;
1818
private string _path;
19+
private string _sqlitePassword;
1920
private DatabaseType _databaseType;
2021
private string _currentObjectName;
2122
private int _totalCount;
@@ -29,10 +30,14 @@ public DbViewerPanel()
2930
pagination.PageSize = DefaultPageSize;
3031
}
3132

32-
public void LoadDatabase(string path)
33+
public void LoadDatabase(string path, string password = null)
3334
{
3435
_path = path;
35-
_databaseType = DetectDatabaseType(path);
36+
_sqlitePassword = password;
37+
// When a password is supplied the caller already verified it is SQLite.
38+
_databaseType = password != null
39+
? DatabaseType.SQLite
40+
: Plugin.DetectDatabaseType(path);
3641
_hasLoadedData = false;
3742

3843
if (_databaseType == DatabaseType.Unknown)
@@ -155,41 +160,19 @@ private void LoadCurrentPage()
155160
pageInfoTextBlock.Text = $"{_totalCount} 行 / 每页 {DefaultPageSize} 行 / 第 {pagination.CurrentPage} 页,共 {pagination.PageCount} 页";
156161
}
157162

158-
private static DatabaseType DetectDatabaseType(string path)
163+
private SqliteConnection OpenSqliteConnection(string path)
159164
{
160-
try
161-
{
162-
using var stream = File.OpenRead(path);
163-
var header = new byte[59]; // need 32+27 bytes for LiteDB magic
164-
var count = stream.Read(header, 0, header.Length);
165-
166-
if (count >= 16)
167-
{
168-
var text = System.Text.Encoding.ASCII.GetString(header, 0, 16);
169-
if (text.StartsWith("SQLite format 3", StringComparison.Ordinal))
170-
return DatabaseType.SQLite;
171-
}
172-
173-
// LiteDB 5.x: magic string "** This is a LiteDB file **" at offset 32
174-
if (count >= 59)
175-
{
176-
var liteDbMagic = System.Text.Encoding.ASCII.GetString(header, 32, 27);
177-
if (liteDbMagic == "** This is a LiteDB file **")
178-
return DatabaseType.LiteDb;
179-
}
180-
}
181-
catch
182-
{
183-
}
184-
185-
return DatabaseType.Unknown;
165+
var csb = new SqliteConnectionStringBuilder { DataSource = path };
166+
if (!string.IsNullOrEmpty(_sqlitePassword))
167+
csb.Password = _sqlitePassword;
168+
return new SqliteConnection(csb.ToString());
186169
}
187170

188-
private static string[] LoadSqliteNames(string path)
171+
private string[] LoadSqliteNames(string path)
189172
{
190173
var items = new List<string>();
191174

192-
using var connection = new SqliteConnection($"Data Source={path}");
175+
using var connection = OpenSqliteConnection(path);
193176
connection.Open();
194177

195178
using var command = connection.CreateCommand();
@@ -201,18 +184,18 @@ private static string[] LoadSqliteNames(string path)
201184
items.Add(reader.GetString(0));
202185
}
203186

204-
return items.ToArray();
187+
return [.. items];
205188
}
206189

207190
private static string[] LoadLiteDbNames(string path)
208191
{
209192
using var db = new LiteDatabase(path);
210-
return db.GetCollectionNames().OrderBy(name => name).ToArray();
193+
return [.. db.GetCollectionNames().OrderBy(name => name)];
211194
}
212195

213-
private static int GetSqliteRowCount(string path, string tableName)
196+
private int GetSqliteRowCount(string path, string tableName)
214197
{
215-
using var connection = new SqliteConnection($"Data Source={path}");
198+
using var connection = OpenSqliteConnection(path);
216199
connection.Open();
217200

218201
using var command = connection.CreateCommand();
@@ -224,12 +207,12 @@ private static int GetLiteDbRowCount(string path, string collectionName)
224207
{
225208
using var db = new LiteDatabase(path);
226209
var collection = db.GetCollection(collectionName);
227-
return (int)collection.Count();
210+
return collection.Count();
228211
}
229212

230-
private static DataTable LoadSqlitePage(string path, string tableName, int page, int pageSize)
213+
private DataTable LoadSqlitePage(string path, string tableName, int page, int pageSize)
231214
{
232-
using var connection = new SqliteConnection($"Data Source={path}");
215+
using var connection = OpenSqliteConnection(path);
233216
connection.Open();
234217

235218
using var command = connection.CreateCommand();
@@ -250,9 +233,9 @@ private static DataTable LoadLiteDbPage(string path, string collectionName, int
250233
return ConvertBsonDocumentsToTable(docs);
251234
}
252235

253-
private static DataTable LoadSqliteTable(string path, string tableName)
236+
private DataTable LoadSqliteTable(string path, string tableName)
254237
{
255-
using var connection = new SqliteConnection($"Data Source={path}");
238+
using var connection = OpenSqliteConnection(path);
256239
connection.Open();
257240

258241
using var command = connection.CreateCommand();
@@ -311,11 +294,4 @@ private static object ConvertBsonValue(BsonValue value)
311294

312295
return value.RawValue?.ToString() ?? value.ToString() ?? string.Empty;
313296
}
314-
315-
private enum DatabaseType
316-
{
317-
Unknown,
318-
SQLite,
319-
LiteDb,
320-
}
321297
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<UserControl x:Class="QuickLook.Plugin.DbViewer.PasswordControl"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
d:DesignHeight="450"
7+
d:DesignWidth="800"
8+
mc:Ignorable="d">
9+
<Grid>
10+
<Border HorizontalAlignment="Center"
11+
VerticalAlignment="Center"
12+
Background="{DynamicResource CardBackground}"
13+
BorderBrush="{DynamicResource CardBorderBrush}"
14+
BorderThickness="1"
15+
CornerRadius="5">
16+
<Grid Margin="24">
17+
<StackPanel>
18+
<TextBlock x:Name="titleTextBlock"
19+
FontSize="18"
20+
FontWeight="DemiBold"
21+
Text="输入密码" />
22+
<TextBlock x:Name="hintTextBlock"
23+
Margin="0,20,0,0"
24+
Text="此数据库已加密,请输入密码以打开。" />
25+
<PasswordBox x:Name="passwordBox"
26+
MinWidth="400"
27+
Margin="0,16,0,0" />
28+
<TextBlock x:Name="passwordErrorTextBlock"
29+
MinWidth="400"
30+
Margin="0,16,0,0"
31+
Foreground="Red"
32+
Text="密码错误,请重试"
33+
Visibility="Collapsed" />
34+
<Grid MinWidth="400" Margin="0,16,0,0">
35+
<Grid.ColumnDefinitions>
36+
<ColumnDefinition Width="*" />
37+
<ColumnDefinition Width="*" />
38+
</Grid.ColumnDefinitions>
39+
<Button x:Name="openButton"
40+
Grid.Column="0"
41+
Margin="0,0,4,0"
42+
HorizontalAlignment="Stretch"
43+
Content="打开" />
44+
<Button x:Name="cancelButton"
45+
Grid.Column="1"
46+
Margin="4,0,0,0"
47+
HorizontalAlignment="Stretch"
48+
Content="取消" />
49+
</Grid>
50+
</StackPanel>
51+
</Grid>
52+
</Border>
53+
</Grid>
54+
</UserControl>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Controls;
4+
5+
namespace QuickLook.Plugin.DbViewer;
6+
7+
public partial class PasswordControl : UserControl
8+
{
9+
public event Func<string, bool> PasswordRequested;
10+
11+
public string Password => passwordBox.Dispatcher.Invoke(() => passwordBox.Password);
12+
13+
public PasswordControl()
14+
{
15+
InitializeComponent();
16+
17+
openButton.Click += OpenButton_Click;
18+
cancelButton.Click += CancelButton_Click;
19+
passwordBox.KeyDown += (s, e) =>
20+
{
21+
if (e.Key == System.Windows.Input.Key.Enter)
22+
OpenButton_Click(s, null);
23+
};
24+
}
25+
26+
private void OpenButton_Click(object sender, RoutedEventArgs e)
27+
{
28+
var pw = Password;
29+
if (string.IsNullOrEmpty(pw))
30+
return;
31+
32+
passwordErrorTextBlock.Visibility = Visibility.Collapsed;
33+
34+
bool accepted = PasswordRequested?.Invoke(pw) ?? false;
35+
if (!accepted)
36+
passwordErrorTextBlock.Dispatcher.Invoke(() => passwordErrorTextBlock.Visibility = Visibility.Visible);
37+
}
38+
39+
private void CancelButton_Click(object sender, RoutedEventArgs e)
40+
{
41+
Window.GetWindow(this)?.Close();
42+
}
43+
}

‎QuickLook.Plugin/QuickLook.Plugin.DbViewer/Plugin.cs‎

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.Data.Sqlite;
12
using QuickLook.Common.Commands;
23
using QuickLook.Common.Controls;
34
using QuickLook.Common.Plugin;
@@ -12,6 +13,7 @@ namespace QuickLook.Plugin.DbViewer;
1213
public sealed partial class Plugin : IViewer, IMoreMenu
1314
{
1415
private DbViewerPanel _panel;
16+
private PasswordControl _passwordControl;
1517
private string _currentPath;
1618

1719
public int Priority => 0;
@@ -20,6 +22,14 @@ public sealed partial class Plugin : IViewer, IMoreMenu
2022

2123
public void Init()
2224
{
25+
try
26+
{
27+
SQLitePCL.Batteries_V2.Init();
28+
}
29+
catch
30+
{
31+
// Provider may already be initialised by another component; proceed.
32+
}
2333
}
2434

2535
public bool CanHandle(string path)
@@ -45,22 +55,59 @@ public void Prepare(string path, ContextObject context)
4555
public void View(string path, ContextObject context)
4656
{
4757
_currentPath = path;
48-
_panel = new DbViewerPanel();
4958

59+
var dbType = DetectDatabaseType(path);
60+
61+
if (dbType == DatabaseType.EncryptedSQLite)
62+
{
63+
_passwordControl = new PasswordControl();
64+
_passwordControl.PasswordRequested += password =>
65+
{
66+
// Verify the password
67+
try
68+
{
69+
var csb = new SqliteConnectionStringBuilder { DataSource = path, Password = password };
70+
using var conn = new SqliteConnection(csb.ToString());
71+
conn.Open();
72+
using var cmd = conn.CreateCommand();
73+
cmd.CommandText = "SELECT 1";
74+
cmd.ExecuteScalar();
75+
}
76+
catch
77+
{
78+
return false;
79+
}
80+
81+
// Password accepted — switch to the data panel
82+
_panel = new DbViewerPanel();
83+
context.ViewerContent = _panel;
84+
context.Title = Path.GetFileName(path);
85+
_panel.LoadDatabase(path, password);
86+
context.IsBusy = false;
87+
return true;
88+
};
89+
90+
context.ViewerContent = _passwordControl;
91+
context.Title = $"[ENCRYPTED] {Path.GetFileName(path)}";
92+
context.IsBusy = false;
93+
return;
94+
}
95+
96+
_panel = new DbViewerPanel();
5097
context.ViewerContent = _panel;
5198
context.Title = Path.GetFileName(path);
5299
_panel.LoadDatabase(path);
53-
54100
context.IsBusy = false;
55101
}
56102

57103
public void Cleanup()
58104
{
59105
GC.SuppressFinalize(this);
60106
_panel = null;
107+
_passwordControl = null;
61108
}
62109

63-
private static DatabaseType DetectDatabaseType(string path)
110+
internal static DatabaseType DetectDatabaseType(string path)
64111
{
65112
try
66113
{
@@ -88,6 +135,13 @@ private static DatabaseType DetectDatabaseType(string path)
88135
// Ignore invalid file reads during detection.
89136
}
90137

138+
// Header-based detection failed.
139+
// Encrypted SQLite files have no recognisable header; use the extension
140+
// as a heuristic so we can show the password dialog.
141+
var ext = Path.GetExtension(path).ToLowerInvariant();
142+
if (ext is ".sqlite" or ".sqlite3" or ".db" or ".db3" or ".sdb")
143+
return DatabaseType.EncryptedSQLite;
144+
91145
return DatabaseType.Unknown;
92146
}
93147

@@ -117,11 +171,12 @@ private void ExportToExcel()
117171

118172
_panel.ExportCurrentTableToExcel(_currentPath);
119173
}
174+
}
120175

121-
private enum DatabaseType
122-
{
123-
Unknown,
124-
SQLite,
125-
LiteDb,
126-
}
176+
internal enum DatabaseType
177+
{
178+
Unknown,
179+
SQLite,
180+
EncryptedSQLite,
181+
LiteDb,
127182
}

0 commit comments

Comments
 (0)