Skip to content

Commit a5cb5b6

Browse files
committed
feature: use secret-tool to store API key to system keychain on Linux
Signed-off-by: leo <longshuang@msn.cn>
1 parent 83f4793 commit a5cb5b6

5 files changed

Lines changed: 118 additions & 5 deletions

File tree

‎src/AI/ApiKey.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ public string KeyToAccess
4242
[JsonIgnore]
4343
public bool IsValidStorage
4444
{
45-
get => Storage != ApiKeyStorage.SystemCredentialManager || !OperatingSystem.IsLinux();
45+
get
46+
{
47+
if (_storage == ApiKeyStorage.SystemCredentialManager)
48+
return Native.OS.CredentialStorage != null;
49+
return true;
50+
}
4651
}
4752

4853
public void PrepareForSaving()

‎src/Native/Linux.cs‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ public void TerminateProcess(Process proc)
207207
}
208208
}
209209

210+
public OS.ICredentialStorage CreateCredentialStorage()
211+
{
212+
var toolPath = FindExecutable("secret-tool");
213+
if (string.IsNullOrEmpty(toolPath))
214+
return null;
215+
216+
if (!File.Exists(toolPath))
217+
return null;
218+
219+
return new LinuxCredentialStorage(toolPath);
220+
}
221+
210222
private string FindExecutable(string filename)
211223
{
212224
var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
@@ -231,4 +243,99 @@ private string GetXdgDirectory(string envVar, string fallback, string subDirName
231243
return Path.Combine(fallback, subDirName);
232244
}
233245
}
246+
247+
[SupportedOSPlatform("linux")]
248+
public class LinuxCredentialStorage : OS.ICredentialStorage
249+
{
250+
public LinuxCredentialStorage(string toolPath)
251+
{
252+
_toolPath = toolPath;
253+
}
254+
255+
public void Store(string host, string username, string password)
256+
{
257+
var label = $"sourcegit:{host}:{username}";
258+
259+
var psi = new ProcessStartInfo
260+
{
261+
FileName = _toolPath,
262+
Arguments = $"store --label={label.Quoted()} service \"sourcegit\" host {host.Quoted()} account {username.Quoted()}",
263+
RedirectStandardInput = true,
264+
RedirectStandardOutput = true,
265+
RedirectStandardError = true,
266+
UseShellExecute = true,
267+
CreateNoWindow = true
268+
};
269+
270+
using (var proc = Process.Start(psi))
271+
{
272+
if (proc == null)
273+
throw new Exception($"Failed to start credential helper: {_toolPath}");
274+
275+
proc.StandardInput.WriteLine(password);
276+
proc.StandardInput.Close();
277+
278+
var err = proc.StandardError.ReadToEnd();
279+
proc.WaitForExit();
280+
281+
if (proc.ExitCode != 0)
282+
throw new Exception($"Credential helper exited with code {proc.ExitCode}: {err}");
283+
}
284+
}
285+
286+
public string Read(string host, string username)
287+
{
288+
var psi = new ProcessStartInfo
289+
{
290+
FileName = _toolPath,
291+
Arguments = $"lookup service \"sourcegit\" host {host.Quoted()} account {username.Quoted()}",
292+
RedirectStandardOutput = true,
293+
RedirectStandardError = true,
294+
UseShellExecute = true,
295+
CreateNoWindow = true
296+
};
297+
298+
using (var proc = Process.Start(psi))
299+
{
300+
if (proc == null)
301+
throw new Exception($"Failed to start credential helper: {_toolPath}");
302+
303+
var output = proc.StandardOutput.ReadToEnd();
304+
var err = proc.StandardError.ReadToEnd();
305+
proc.WaitForExit();
306+
307+
if (proc.ExitCode != 0)
308+
throw new Exception($"Credential helper exited with code {proc.ExitCode}: {err}");
309+
310+
return output.TrimEnd('\r', '\n');
311+
}
312+
}
313+
314+
public void Erase(string host, string username)
315+
{
316+
var psi = new ProcessStartInfo
317+
{
318+
FileName = _toolPath,
319+
Arguments = $"clear service \"sourcegit\" host {host.Quoted()} account {username.Quoted()}",
320+
RedirectStandardOutput = true,
321+
RedirectStandardError = true,
322+
UseShellExecute = true,
323+
CreateNoWindow = true
324+
};
325+
326+
using (var proc = Process.Start(psi))
327+
{
328+
if (proc == null)
329+
throw new Exception($"Failed to start credential helper: {_toolPath}");
330+
331+
var err = proc.StandardError.ReadToEnd();
332+
proc.WaitForExit();
333+
334+
if (proc.ExitCode != 0)
335+
throw new Exception($"Credential helper exited with code {proc.ExitCode}: {err}");
336+
}
337+
}
338+
339+
private string _toolPath;
340+
}
234341
}

‎src/Native/OS.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,9 @@ static OS()
158158
}
159159
else if (OperatingSystem.IsLinux())
160160
{
161-
_backend = new Linux();
162-
CredentialStorage = null;
161+
var impl = new Linux();
162+
_backend = impl;
163+
CredentialStorage = impl.CreateCredentialStorage();
163164
}
164165
else
165166
{

‎src/Resources/Locales/en_US.axaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@
416416
<x:String x:Key="Text.EditApiKey" xml:space="preserve">Edit API Key</x:String>
417417
<x:String x:Key="Text.EditApiKey.ApiKey" xml:space="preserve">API Key</x:String>
418418
<x:String x:Key="Text.EditApiKey.EnvironmentVariable" xml:space="preserve">Environment Variable</x:String>
419-
<x:String x:Key="Text.EditApiKey.InvalidStorage" xml:space="preserve">Currently, saving API key to system credential manager is not supported on Linux</x:String>
419+
<x:String x:Key="Text.EditApiKey.InvalidStorage" xml:space="preserve">To store key in system credential manager on Linux, you should install `secret-tool`</x:String>
420420
<x:String x:Key="Text.EditApiKey.Server" xml:space="preserve">Server</x:String>
421421
<x:String x:Key="Text.EditApiKey.Storage" xml:space="preserve">Storage</x:String>
422422
<x:String x:Key="Text.EditBranchDescription" xml:space="preserve">Edit Branch's Description</x:String>

‎src/Resources/Locales/zh_CN.axaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@
420420
<x:String x:Key="Text.EditApiKey" xml:space="preserve">编辑 API 密钥</x:String>
421421
<x:String x:Key="Text.EditApiKey.ApiKey" xml:space="preserve">API 密钥</x:String>
422422
<x:String x:Key="Text.EditApiKey.EnvironmentVariable" xml:space="preserve">环境变量</x:String>
423-
<x:String x:Key="Text.EditApiKey.InvalidStorage" xml:space="preserve">当前在 Linux 上不支持将 API 密钥保存到系统凭据管理器</x:String>
423+
<x:String x:Key="Text.EditApiKey.InvalidStorage" xml:space="preserve">未检测到 `secret-tool`, 请安装后重启程序再尝试</x:String>
424424
<x:String x:Key="Text.EditApiKey.Server" xml:space="preserve">服务地址</x:String>
425425
<x:String x:Key="Text.EditApiKey.Storage" xml:space="preserve">存储方式</x:String>
426426
<x:String x:Key="Text.EditBranchDescription" xml:space="preserve">编辑分支描述</x:String>

0 commit comments

Comments
 (0)