2025-01-13 19:51:26 +01:00
using AIStudio.Tools.Rust ;
2025-02-15 15:41:12 +01:00
namespace AIStudio.Tools.Services ;
2025-01-13 19:51:26 +01:00
public sealed partial class RustService
{
2026-07-19 11:33:56 +02:00
private static string TranslateSecretStoreIssue ( SecretStoreIssueCode issueCode , string issue ) => issueCode switch
{
SecretStoreIssueCode . NONE => issue ,
SecretStoreIssueCode . SECRET_NOT_FOUND => TB ( "No saved secret was found." ),
SecretStoreIssueCode . NO_DEFAULT_COLLECTION => TB ( "AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default." ),
SecretStoreIssueCode . COLLECTION_LOCKED => TB ( "AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection." ),
SecretStoreIssueCode . PROMPT_DISMISSED => TB ( "The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt." ),
SecretStoreIssueCode . SERVICE_UNAVAILABLE => TB ( "No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service." ),
_ => TB ( "AI Studio could not access secure storage. See the log for technical details." ),
};
private static StoreSecretResponse TranslateSecretStoreIssue ( StoreSecretResponse response ) =>
response . Success ? response : response with { Issue = TranslateSecretStoreIssue ( response . IssueCode , response . Issue ) };
private static RequestedSecret TranslateSecretStoreIssue ( RequestedSecret response ) =>
response . Success ? response : response with { Issue = TranslateSecretStoreIssue ( response . IssueCode , response . Issue ) };
private static DeleteSecretResponse TranslateSecretStoreIssue ( DeleteSecretResponse response ) =>
response . Success ? response : response with { Issue = TranslateSecretStoreIssue ( response . IssueCode , response . Issue ) };
2026-05-18 16:26:51 +02:00
private static string SecretKey ( ISecretId secretId , SecretStoreType storeType ) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}" ;
private static string LegacySecretKey ( ISecretId secretId ) => $"secret::{secretId.SecretId}::{secretId.SecretName}" ;
2025-01-13 19:51:26 +01:00
/// <summary>
/// Try to get the secret data for the given secret ID.
/// </summary>
/// <param name="secretId">The secret ID to get the data for.</param>
2026-05-18 16:26:51 +02:00
/// <param name="storeType">The secret store type.</param>
2025-01-13 19:51:26 +01:00
/// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param>
/// <returns>The requested secret.</returns>
2026-05-18 16:26:51 +02:00
public async Task < RequestedSecret > GetSecret ( ISecretId secretId , SecretStoreType storeType , bool isTrying = false )
2025-01-13 19:51:26 +01:00
{
2026-05-18 16:26:51 +02:00
var secretKey = SecretKey ( secretId , storeType );
var secret = await this . GetSecretByKey ( secretKey , isTrying || storeType is SecretStoreType . DATA_SOURCE );
if ( secret . Success || storeType is not SecretStoreType . DATA_SOURCE )
return secret ;
var legacySecretKey = LegacySecretKey ( secretId );
var legacySecret = await this . GetSecretByKey ( legacySecretKey , isTrying : true );
if ( legacySecret . Success )
2025-01-13 19:51:26 +01:00
{
2026-05-18 16:26:51 +02:00
this . logger !. LogDebug ( $"Successfully retrieved the legacy data source secret for '{legacySecretKey}'." );
return legacySecret ;
2025-01-13 19:51:26 +01:00
}
2026-05-18 16:26:51 +02:00
2025-01-13 19:51:26 +01:00
return secret ;
}
/// <summary>
/// Try to store the secret data for the given secret ID.
/// </summary>
/// <param name="secretId">The secret ID to store the data for.</param>
/// <param name="secretData">The data to store.</param>
2026-05-18 16:26:51 +02:00
/// <param name="storeType">The secret store type.</param>
2025-01-13 19:51:26 +01:00
/// <returns>The store secret response.</returns>
2026-05-18 16:26:51 +02:00
public async Task < StoreSecretResponse > SetSecret ( ISecretId secretId , string secretData , SecretStoreType storeType )
2025-01-13 19:51:26 +01:00
{
2026-05-18 16:26:51 +02:00
var secretKey = SecretKey ( secretId , storeType );
2025-01-13 19:51:26 +01:00
var encryptedSecret = await this . encryptor !. Encrypt ( secretData );
2026-05-18 16:26:51 +02:00
var request = new StoreSecretRequest ( secretKey , Environment . UserName , encryptedSecret );
2025-01-13 19:51:26 +01:00
var result = await this . http . PostAsJsonAsync ( "/secrets/store" , request , this . jsonRustSerializerOptions );
if (! result . IsSuccessStatusCode )
{
2026-05-18 16:26:51 +02:00
this . logger !. LogError ( $"Failed to store the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'" );
return new StoreSecretResponse ( false , TB ( "Failed to store the secret data due to an API issue." ));
2025-01-13 19:51:26 +01:00
}
var state = await result . Content . ReadFromJsonAsync < StoreSecretResponse >( this . jsonRustSerializerOptions );
if (! state . Success )
2026-05-18 16:26:51 +02:00
this . logger !. LogError ( $"Failed to store the secret data for '{secretKey}': '{state.Issue}'" );
if ( state . Success && storeType is SecretStoreType . DATA_SOURCE )
await this . DeleteSecretByKey ( LegacySecretKey ( secretId ));
2025-01-13 19:51:26 +01:00
2026-07-19 11:33:56 +02:00
return TranslateSecretStoreIssue ( state );
2025-01-13 19:51:26 +01:00
}
/// <summary>
/// Tries to delete the secret data for the given secret ID.
/// </summary>
/// <param name="secretId">The secret ID to delete the data for.</param>
2026-05-18 16:26:51 +02:00
/// <param name="storeType">The secret store type.</param>
2025-01-13 19:51:26 +01:00
/// <returns>The delete secret response.</returns>
2026-05-18 16:26:51 +02:00
public async Task < DeleteSecretResponse > DeleteSecret ( ISecretId secretId , SecretStoreType storeType )
{
var deleteResult = await this . DeleteSecretByKey ( SecretKey ( secretId , storeType ));
if ( storeType is not SecretStoreType . DATA_SOURCE || ! deleteResult . Success )
return deleteResult ;
var legacyDeleteResult = await this . DeleteSecretByKey ( LegacySecretKey ( secretId ));
if (! legacyDeleteResult . Success )
return legacyDeleteResult ;
return deleteResult with { WasEntryFound = deleteResult . WasEntryFound || legacyDeleteResult . WasEntryFound };
}
private async Task < RequestedSecret > GetSecretByKey ( string secretKey , bool isTrying )
{
var secretRequest = new SelectSecretRequest ( secretKey , Environment . UserName , isTrying );
var result = await this . http . PostAsJsonAsync ( "/secrets/get" , secretRequest , this . jsonRustSerializerOptions );
if (! result . IsSuccessStatusCode )
{
2026-09-05 19:38:54 +02:00
if ( isTrying )
this . logger !. LogWarning ( $"Failed to get the secret data for '{secretKey}' due to an API issue (try mode): '{result.StatusCode}'" );
else
2026-05-18 16:26:51 +02:00
this . logger !. LogError ( $"Failed to get the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'" );
return new RequestedSecret ( false , new EncryptedText ( string . Empty ), TB ( "Failed to get the secret data due to an API issue." ));
}
2026-07-19 11:33:56 +02:00
var state = await result . Content . ReadFromJsonAsync < RequestedSecret >( this . jsonRustSerializerOptions );
if (! state . Success )
{
2026-09-05 19:38:54 +02:00
//
// A missing entry is what try mode is for: the absent keyring entry is how the app
// recognizes an unconfigured secret in the first place, so it is not worth a line
// in the log. Anything else — a locked keychain, an unavailable secret service, a
// dismissed prompt — is a real problem that used to hide on debug level:
//
2026-07-19 11:33:56 +02:00
if ( isTrying )
2026-09-05 19:38:54 +02:00
{
if ( state . IssueCode is not SecretStoreIssueCode . SECRET_NOT_FOUND )
this . logger !. LogWarning ( $"Failed to get the secret data for '{secretKey}' (try mode): '{state.Issue}'" );
}
2026-07-19 11:33:56 +02:00
else
this . logger !. LogError ( $"Failed to get the secret data for '{secretKey}': '{state.Issue}'" );
}
return TranslateSecretStoreIssue ( state );
2026-05-18 16:26:51 +02:00
}
private async Task < DeleteSecretResponse > DeleteSecretByKey ( string secretKey )
2025-01-13 19:51:26 +01:00
{
2026-05-18 16:26:51 +02:00
var request = new SelectSecretRequest ( secretKey , Environment . UserName , false );
2025-01-13 19:51:26 +01:00
var result = await this . http . PostAsJsonAsync ( "/secrets/delete" , request , this . jsonRustSerializerOptions );
if (! result . IsSuccessStatusCode )
{
2026-05-18 16:26:51 +02:00
this . logger !. LogError ( $"Failed to delete the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'" );
2025-05-04 14:59:30 +02:00
return new DeleteSecretResponse { Success = false , WasEntryFound = false , Issue = TB ( "Failed to delete the secret data due to an API issue." )};
2025-01-13 19:51:26 +01:00
}
var state = await result . Content . ReadFromJsonAsync < DeleteSecretResponse >( this . jsonRustSerializerOptions );
if (! state . Success )
2026-05-18 16:26:51 +02:00
this . logger !. LogError ( $"Failed to delete the secret data for '{secretKey}': '{state.Issue}'" );
2025-01-13 19:51:26 +01:00
2026-07-19 11:33:56 +02:00
return TranslateSecretStoreIssue ( state );
2025-01-13 19:51:26 +01:00
}
2026-07-19 11:33:56 +02:00
}