mudlet/test/SecureStringUtilsTest.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

416 lines
18 KiB
C++
Raw Permalink Normal View History

Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
/***************************************************************************
* Copyright (C) 2025 by Mike Conley - mike.conley@stickmud.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include <SecureStringUtils.h>
#include <QtTest/QtTest>
#include <QVersionNumber>
Add: sign in to supported games using your browser (e.g. Google, Discord, or the game's own account) (#9378) #### Brief overview of PR changes/additions Adds client-side support for the GMCP `Char.Login` v2 sign-in flow. When a game offers it, Mudlet automates sign-in *around the game's own login screen* — it renders no sign-in UI of its own: - Hands off to the game's interactive screen with an empty `Char.Login.Credentials {}` when nothing is stored, so the player picks a provider (Google, Discord, GitHub, the game's own account, …) as text on the game's own screen. - Opens the sign-in URL the server pushes (`Char.Login.URL`) in the system browser — but only after the player has acted on this connection, never unprompted. - Autofills a stored character name + password when the profile has them. - Persists and replays the server's reconnect token (`Char.Login.Token` / `Char.Login.Reconnect`) for instant, password-less reconnects, with a "forget saved sign-in" control in Preferences → Connection. - Resumes the *remembered* provider's browser sign-in without a menu when a saved token has expired or been revoked (`Char.Login.Credentials {account, provider}`), falling back to the interactive hand-off only when no provider is remembered. - Handles token rotation and multiple devices safely: overwrites the saved token when the server rotates it, and if another running Mudlet instance sharing the profile's keychain rotates the token mid-flight, replays the fresh token instead of discarding it. - For a game that is its own OpenID Provider over TLS, optionally runs the client-driven PKCE flow end to end (`Char.Login.AuthCode`). #### Motivation for adding to Mudlet Modern games are moving to browser-based single sign-on; this lets Mudlet players use those accounts directly, and reconnect without re-entering anything — while classic character-name/password logins keep working unchanged. #### Other info (issues closed, discussion etc) - Reworks the approach to resolve @vadi2's UX feedback (https://github.com/Mudlet/Mudlet/pull/9373#issuecomment-4865669499): there is **no client pop-up and no in-client chooser**. The game owns the sign-in screen and offers the choice as text; Mudlet only automates the mechanical hooks around it (autofill, open URL, save token, replay token). - Supersedes and closes #9373. - Implements the revised v2 draft spec, inspired by #9354. - Covered by new tests: `OAuthClientFlowTest` (OIDC discovery, PKCE, loopback capture) and the `GMCPCharLoginTest` functional suite driving a GMCP server stub through the full client flow. - Try it out on StickMUD. https://github.com/user-attachments/assets/566947f6-4f43-4bff-b98c-9328b7a40a2d --------- Signed-off-by: Michael Conley <sousesider@gmail.com>
2026-07-14 08:20:02 -04:00
#include <string>
Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
class SecureStringUtilsTest : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void testProfileBasedEncryption();
void testDifferentProfilesUseDifferentKeys();
void testEncryptedFormatDetection();
void testEmptyStrings();
void testNonDeterministicEncryption();
void testSpecialCharacters();
void testSecureMemoryClearing();
void testProfileKeyPersistence();
void testPortableModeFileStorage();
void testInvalidInputHandling();
void testLargeDataEncryption();
void testCorruptedDataDecryption();
void testVersionCompatibility();
void testXMLImportProxyPasswordLogic();
void testConveniencePasswordMethods();
void cleanupTestCase();
};
void SecureStringUtilsTest::initTestCase()
{
}
void SecureStringUtilsTest::testProfileBasedEncryption()
{
QString plaintext = "mypassword";
QString profileName = "TestProfile";
QString encrypted = SecureStringUtils::encryptStringForProfile(plaintext, profileName);
QVERIFY(!encrypted.isEmpty());
QVERIFY(encrypted != plaintext);
QString decrypted = SecureStringUtils::decryptStringForProfile(encrypted, profileName);
QCOMPARE(decrypted, plaintext);
}
void SecureStringUtilsTest::testDifferentProfilesUseDifferentKeys()
{
QString plaintext = "samepassword";
QString profile1 = "Profile1";
QString profile2 = "Profile2";
QString encrypted1 = SecureStringUtils::encryptStringForProfile(plaintext, profile1);
QString encrypted2 = SecureStringUtils::encryptStringForProfile(plaintext, profile2);
// Should be different due to different profile keys
QVERIFY(encrypted1 != encrypted2);
// Each should decrypt correctly with its own profile
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted1, profile1), plaintext);
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted2, profile2), plaintext);
// Cross-profile decryption should fail
QVERIFY(SecureStringUtils::decryptStringForProfile(encrypted1, profile2) != plaintext);
QVERIFY(SecureStringUtils::decryptStringForProfile(encrypted2, profile1) != plaintext);
}
void SecureStringUtilsTest::testEncryptedFormatDetection()
{
// Test plaintext passwords (should NOT be detected as encrypted)
QVERIFY(!SecureStringUtils::isEncryptedFormat("mypassword"));
QVERIFY(!SecureStringUtils::isEncryptedFormat("secret123!@#"));
QVERIFY(!SecureStringUtils::isEncryptedFormat(""));
// Test actual encrypted passwords (should be detected as encrypted)
QString plaintext = "testpassword";
QString encrypted = SecureStringUtils::encryptStringForProfile(plaintext, "TestProfile");
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted));
// Test invalid formats
QVERIFY(!SecureStringUtils::isEncryptedFormat("not-base64!"));
QVERIFY(!SecureStringUtils::isEncryptedFormat("invalid=base64="));
}
void SecureStringUtilsTest::testEmptyStrings()
{
// Test empty string handling
QCOMPARE(SecureStringUtils::encryptStringForProfile("", "Profile"), QString());
QCOMPARE(SecureStringUtils::encryptStringForProfile("password", ""), QString());
QCOMPARE(SecureStringUtils::decryptStringForProfile("", "Profile"), QString());
QCOMPARE(SecureStringUtils::decryptStringForProfile("encrypted", ""), QString());
QVERIFY(!SecureStringUtils::isEncryptedFormat(""));
}
void SecureStringUtilsTest::testNonDeterministicEncryption()
{
// Same plaintext should produce DIFFERENT encrypted results each time (due to random nonces)
QString plaintext = "consistent_password";
QString profile = "TestProfile";
QString encrypted1 = SecureStringUtils::encryptStringForProfile(plaintext, profile);
QString encrypted2 = SecureStringUtils::encryptStringForProfile(plaintext, profile);
// Should be different due to random nonces
QVERIFY(encrypted1 != encrypted2);
// But both should decrypt to the same plaintext
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted1, profile), plaintext);
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted2, profile), plaintext);
}
void SecureStringUtilsTest::testSpecialCharacters()
{
// Test passwords with special characters
QString specialPassword = "pássw0rd!@#$%^&*()";
QString profile = "TestProfile";
QString encrypted = SecureStringUtils::encryptStringForProfile(specialPassword, profile);
QString decrypted = SecureStringUtils::decryptStringForProfile(encrypted, profile);
QCOMPARE(decrypted, specialPassword);
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted));
}
void SecureStringUtilsTest::testSecureMemoryClearing()
{
QString testString = "sensitive_data";
QString originalContent = testString;
SecureStringUtils::secureStringClear(testString);
QVERIFY(testString.isEmpty());
QVERIFY(testString != originalContent);
Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
QByteArray testArray = "sensitive_bytes";
QByteArray originalArray = testArray;
Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
SecureStringUtils::secureByteArrayClear(testArray);
QVERIFY(testArray.isEmpty());
QVERIFY(testArray != originalArray);
Add: sign in to supported games using your browser (e.g. Google, Discord, or the game's own account) (#9378) #### Brief overview of PR changes/additions Adds client-side support for the GMCP `Char.Login` v2 sign-in flow. When a game offers it, Mudlet automates sign-in *around the game's own login screen* — it renders no sign-in UI of its own: - Hands off to the game's interactive screen with an empty `Char.Login.Credentials {}` when nothing is stored, so the player picks a provider (Google, Discord, GitHub, the game's own account, …) as text on the game's own screen. - Opens the sign-in URL the server pushes (`Char.Login.URL`) in the system browser — but only after the player has acted on this connection, never unprompted. - Autofills a stored character name + password when the profile has them. - Persists and replays the server's reconnect token (`Char.Login.Token` / `Char.Login.Reconnect`) for instant, password-less reconnects, with a "forget saved sign-in" control in Preferences → Connection. - Resumes the *remembered* provider's browser sign-in without a menu when a saved token has expired or been revoked (`Char.Login.Credentials {account, provider}`), falling back to the interactive hand-off only when no provider is remembered. - Handles token rotation and multiple devices safely: overwrites the saved token when the server rotates it, and if another running Mudlet instance sharing the profile's keychain rotates the token mid-flight, replays the fresh token instead of discarding it. - For a game that is its own OpenID Provider over TLS, optionally runs the client-driven PKCE flow end to end (`Char.Login.AuthCode`). #### Motivation for adding to Mudlet Modern games are moving to browser-based single sign-on; this lets Mudlet players use those accounts directly, and reconnect without re-entering anything — while classic character-name/password logins keep working unchanged. #### Other info (issues closed, discussion etc) - Reworks the approach to resolve @vadi2's UX feedback (https://github.com/Mudlet/Mudlet/pull/9373#issuecomment-4865669499): there is **no client pop-up and no in-client chooser**. The game owns the sign-in screen and offers the choice as text; Mudlet only automates the mechanical hooks around it (autofill, open URL, save token, replay token). - Supersedes and closes #9373. - Implements the revised v2 draft spec, inspired by #9354. - Covered by new tests: `OAuthClientFlowTest` (OIDC discovery, PKCE, loopback capture) and the `GMCPCharLoginTest` functional suite driving a GMCP server stub through the full client flow. - Try it out on StickMUD. https://github.com/user-attachments/assets/566947f6-4f43-4bff-b98c-9328b7a40a2d --------- Signed-off-by: Michael Conley <sousesider@gmail.com>
2026-07-14 08:20:02 -04:00
std::string testStdString = "sensitive_std_data";
std::string originalStdString = testStdString;
SecureStringUtils::secureStdStringClear(testStdString);
QVERIFY(testStdString.empty());
QVERIFY(testStdString != originalStdString);
Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
}
void SecureStringUtilsTest::testProfileKeyPersistence()
{
// Test that the same profile uses consistent keys
QString password = "testpassword";
QString profile = "PersistentProfile";
QString encrypted1 = SecureStringUtils::encryptStringForProfile(password, profile);
QString encrypted2 = SecureStringUtils::encryptStringForProfile(password, profile);
// Both should decrypt correctly (proving key consistency)
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted1, profile), password);
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted2, profile), password);
}
void SecureStringUtilsTest::testPortableModeFileStorage()
{
// Test that profile-specific encryption/decryption works consistently
// This exercises the file-based key storage in portable mode
QString profileName = "PortableTestProfile";
QString plaintext = "portable_test_password";
// First encryption - this will trigger key generation and file storage
QString encrypted1 = SecureStringUtils::encryptStringForProfile(plaintext, profileName);
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted1));
// Second encryption with same profile - should use same key from file
QString encrypted2 = SecureStringUtils::encryptStringForProfile(plaintext, profileName);
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted2));
// Both should decrypt correctly
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted1, profileName), plaintext);
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted2, profileName), plaintext);
// Test that different profiles use different keys
QString otherProfile = "AnotherPortableProfile";
QString encrypted3 = SecureStringUtils::encryptStringForProfile(plaintext, otherProfile);
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted3));
// Should decrypt correctly with its own profile
QCOMPARE(SecureStringUtils::decryptStringForProfile(encrypted3, otherProfile), plaintext);
// Cross-profile decryption should fail (different keys)
QString crossDecrypt = SecureStringUtils::decryptStringForProfile(encrypted3, profileName);
QVERIFY(crossDecrypt.isEmpty() || crossDecrypt != plaintext);
}
void SecureStringUtilsTest::testInvalidInputHandling()
{
// Test null/empty profile names
QString password = "testpassword";
QString encrypted1 = SecureStringUtils::encryptStringForProfile(password, "");
QVERIFY(encrypted1.isEmpty()); // Should return empty for empty profile
QString encrypted2 = SecureStringUtils::encryptStringForProfile(password, QString());
QVERIFY(encrypted2.isEmpty()); // Should return empty for null profile
// Test with empty password
QString validProfile = "ValidProfile";
QString encrypted3 = SecureStringUtils::encryptStringForProfile("", validProfile);
QVERIFY(encrypted3.isEmpty()); // Should return empty for empty password
// Test decryption with mismatched profiles
QString profile1 = "Profile1";
QString profile2 = "Profile2";
QString encrypted = SecureStringUtils::encryptStringForProfile(password, profile1);
QString decrypted = SecureStringUtils::decryptStringForProfile(encrypted, profile2);
QVERIFY(decrypted.isEmpty() || decrypted != password); // Should fail or return wrong data
}
void SecureStringUtilsTest::testLargeDataEncryption()
{
// Test with larger strings to ensure robustness
QString largeString = QString("A").repeated(10000); // 10KB string
QString profile = "LargeDataProfile";
QString encrypted = SecureStringUtils::encryptStringForProfile(largeString, profile);
QVERIFY(!encrypted.isEmpty());
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted));
QString decrypted = SecureStringUtils::decryptStringForProfile(encrypted, profile);
QCOMPARE(decrypted, largeString);
}
void SecureStringUtilsTest::testCorruptedDataDecryption()
{
QString password = "testpassword";
QString profile = "CorruptionTestProfile";
QString encrypted = SecureStringUtils::encryptStringForProfile(password, profile);
QVERIFY(SecureStringUtils::isEncryptedFormat(encrypted));
// Test with completely invalid format
QString invalid1 = "notencrypted";
QVERIFY(!SecureStringUtils::isEncryptedFormat(invalid1));
QString decrypted1 = SecureStringUtils::decryptStringForProfile(invalid1, profile);
QVERIFY(decrypted1.isEmpty()); // Should return empty for invalid format
// Test with corrupted encrypted data (corrupt the raw binary data, not Base64)
Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
if (encrypted.length() > 10) {
// Decode to binary, corrupt a byte, re-encode to Base64
QByteArray binaryData = QByteArray::fromBase64(encrypted.toLatin1());
if (binaryData.size() > 10) {
// Corrupt a byte in the middle of the binary data
int corruptIndex = binaryData.size() / 2;
char originalByte = binaryData[corruptIndex];
binaryData[corruptIndex] = static_cast<char>(originalByte ^ 0xFF); // Flip all bits
QString corrupted = QString::fromLatin1(binaryData.toBase64());
QString decrypted2 = SecureStringUtils::decryptStringForProfile(corrupted, profile);
QVERIFY(decrypted2.isEmpty() || decrypted2 != password); // Should fail due to corruption
}
Improve: Secure credential management with system keychain integration and legacy migration (#7956) #### Brief overview of PR changes/additions This pull request implements secure credential management for Mudlet with system keychain integration and encrypted fallback storage: **Core Components:** - **CredentialManager**: High-level API for secure credential storage with QtKeychain integration - **SecureStringUtils**: Qt-based cryptographic utilities for encrypted file storage - **Legacy Migration**: Automatic detection and migration of existing plaintext passwords **Key Features:** - **System Keychain Integration**: Primary storage in macOS Keychain, Windows Credential Store, and Linux Secret Service via QtKeychain - **Encrypted File Fallback**: Qt crypto-based AES encryption for portable mode and keychain unavailability - **Seamless Migration**: Automatic detection and upgrade of legacy password storage formats - **Profile Isolation**: Per-profile encryption keys prevent cross-profile credential access - **Portable Mode Support**: Automatic detection and secure file-based storage for portable installations #### Motivation for adding to Mudlet **Security Enhancement:** - Eliminates plaintext password storage in profile XML files - Provides industry-standard system keychain integration for credential security - Implements authenticated encryption for fallback scenarios **User Experience:** - Zero configuration required - works automatically across all platforms - Seamless migration from existing plaintext passwords to secure storage - Native system integration provides familiar credential management experience **Future-Proofing:** - Extensible architecture supports additional credential types (API keys, tokens, etc.) - Robust fallback ensures functionality in all deployment scenarios - Prepares foundation for OAuth and external service integrations #### Other info (issues closed, discussion etc) **Security Architecture:** - **Keychain-First Strategy**: Prefers system keychain with automatic encrypted file fallback - **Legacy Format Detection**: Automatically migrates passwords from development branch keychain format - **Memory Security**: Secure string clearing and controlled credential lifecycle management - **Input Validation**: Path traversal protection and service name sanitization **Implementation Highlights:** - **Async Operations**: Non-blocking keychain operations with timeout protection - **Thread Safety**: Event-driven architecture prevents UI blocking and race conditions - **Comprehensive Testing**: Full test coverage for encryption, migration, and fallback scenarios - **Cross-Platform**: Unified API across Windows, macOS, and Linux with platform-specific optimizations **Version Compatibility & Migration:** - **Mudlet 4.19.x Compatibility**: Preserves existing plaintext password storage to ensure compatibility when switching between 4.19.x stable and development builds - **Mudlet 4.20.x Migration**: Post-4.20.0 release, automatic migration begins converting plaintext passwords to secure storage - **Bidirectional Safety**: Users can safely run both 4.19.x and development versions without losing access to their passwords during the transition period - **Legacy Format Support**: Automatically detects and migrates passwords from the original development branch keychain format (`service="Mudlet profile"`) to the new secure format # Credential Management Workflows ## 1. Credential Storage Strategy ```mermaid flowchart TD A[Store Password Request] --> B{Portable Mode?} B -->|Yes| C[Encrypt & Store in Profile File] B -->|No| D[Store in System Keychain] D --> E{Keychain Success?} E -->|Yes| F[Remove Encrypted Fallback File] E -->|No| G[Fallback to Encrypted File] F --> H[Success] G --> I{Encryption Success?} I -->|Yes| H I -->|No| J[Failure] C --> I classDef primary fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef decision fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef result fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class D,F primary class C,G fallback class B,E,I decision class H,J result ``` ## 2. Legacy Migration Workflow ```mermaid flowchart TD A[Retrieve Password Request] --> B[Try New Keychain Format] B --> C{Password Found?} C -->|Yes| D[Return Password] C -->|No| E[Check Legacy Keychain Format] E --> F{Legacy Found?} F -->|Yes| G[Migrate to New Format] G --> H[Store in New Format] H --> I[Remove Legacy Entry] I --> J[Return Migrated Password] F -->|No| K[Try Encrypted File] K --> L{File Found?} L -->|Yes| M[Decrypt & Return] L -->|No| N[Return Empty - No Password Stored] classDef newformat fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef legacy fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef migration fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff classDef fallback fill:#607D8B,stroke:#263238,stroke-width:2px,color:#fff classDef result fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff class B,H newformat class E,I legacy class G migration class K fallback class D,J,M,N result ``` ## 3. Cross-Platform Keychain Integration ```mermaid flowchart TD A[QtKeychain Request] --> B{Platform Detection} B -->|macOS| C[Access Keychain Services] B -->|Windows| D[Access Credential Store] B -->|Linux| E[Access Secret Service] C --> F[Store/Retrieve Credential] D --> F E --> F F --> G{Operation Success?} G -->|Yes| H[Return Result] G -->|No| I[Log Error & Fallback] I --> J[Use Encrypted File Storage] J --> K[AES Encryption with Profile Key] K --> L[Store in Profile Directory] classDef platform fill:#2196F3,stroke:#0D47A1,stroke-width:2px,color:#fff classDef keychain fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#000 classDef fallback fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 classDef crypto fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff class C,D,E platform class F,H keychain class I,J fallback class K,L crypto ``` This implementation provides a comprehensive, secure, and user-friendly credential management system that seamlessly upgrades existing Mudlet installations while providing robust security for future credential storage needs. --------- Co-authored-by: Vadim Peretokin <vperetokin@hey.com>
2025-08-17 06:44:38 -04:00
}
// Test with truncated encrypted data
if (encrypted.length() > 5) {
QString truncated = encrypted.left(encrypted.length() - 5);
QString decrypted3 = SecureStringUtils::decryptStringForProfile(truncated, profile);
QVERIFY(decrypted3.isEmpty() || decrypted3 != password); // Should fail due to truncation
}
}
void SecureStringUtilsTest::testVersionCompatibility()
{
// Test that version-based compatibility logic works correctly
// Test version comparison for major version differences
QVERIFY(4 > 3); // Version 4.x should be newer than 3.x
QVERIFY(5 > 4); // Version 5.x should be newer than 4.x
// Test version comparison for minor version differences within major version 4
int majorVersion = 4;
// Test cases for version 4.x.x
struct {
int minorVersion;
bool shouldUseSecureStorage;
QString description;
} testCases[] = {
{19, false, "Version 4.19.x should use legacy mode"},
{20, true, "Version 4.20.x should use secure storage"},
{21, true, "Version 4.21.x should use secure storage"},
{50, true, "Version 4.50.x should use secure storage"}
};
for (const auto& testCase : testCases) {
// Simulate the version check logic from XMLimport
bool useSecureStorage = (majorVersion > 4) || (majorVersion == 4 && testCase.minorVersion >= 20);
if (useSecureStorage != testCase.shouldUseSecureStorage) {
QFAIL(qPrintable(QString("Version compatibility test failed for %1: expected %2, got %3")
.arg(testCase.description)
.arg(testCase.shouldUseSecureStorage ? "true" : "false")
.arg(useSecureStorage ? "true" : "false")));
}
QCOMPARE(useSecureStorage, testCase.shouldUseSecureStorage);
}
// Test major version transitions
QVERIFY((5 > 4) || (5 == 4 && 0 >= 20)); // Version 5.0.x should use secure storage
QVERIFY((6 > 4) || (6 == 4 && 0 >= 20)); // Version 6.0.x should use secure storage
qDebug() << "Version compatibility tests passed";
}
void SecureStringUtilsTest::testXMLImportProxyPasswordLogic()
{
// Test that XMLimport now uses application version, not profile version
// This simulates the fixed logic in XMLimport.cpp
// Simulate different APP_VERSION values
struct TestCase {
QString appVersion;
bool shouldUseSecureStorage;
QString description;
};
const QList<TestCase> testCases = {
{"4.19.0", false, "App version 4.19.0 (before secure storage)"},
{"4.20.0", true, "App version 4.20.0 (secure storage introduced)"},
{"4.21.0", true, "App version 4.21.0 (after secure storage)"},
{"5.0.0", true, "App version 5.0.0 (major version after secure storage)"},
{"3.15.0", false, "App version 3.15.0 (old version)"}
};
for (const auto& testCase : testCases) {
// Simulate the new XMLimport logic
const QVersionNumber appVersion = QVersionNumber::fromString(testCase.appVersion);
const QVersionNumber secureStorageVersion = QVersionNumber(4, 20, 0);
const bool useSecureStorage = appVersion >= secureStorageVersion;
QCOMPARE(useSecureStorage, testCase.shouldUseSecureStorage);
if (useSecureStorage != testCase.shouldUseSecureStorage) {
QFAIL(qPrintable(QString("XMLimport proxy password test failed for %1: expected %2, got %3")
.arg(testCase.description)
.arg(testCase.shouldUseSecureStorage ? "true" : "false")
.arg(useSecureStorage ? "true" : "false")));
}
}
qDebug() << "XMLimport proxy password logic tests passed";
}
void SecureStringUtilsTest::testConveniencePasswordMethods()
{
QString testProfile = "ConvenienceTestProfile";
QString testKey = "test_password";
QString testPassword = "MyConvenienceTestPassword123!";
// Ensure clean state
SecureStringUtils::removePassword(testProfile, testKey);
QVERIFY(!SecureStringUtils::hasPassword(testProfile, testKey));
// Test storing password
bool stored = SecureStringUtils::storePassword(testProfile, testKey, testPassword);
QVERIFY(stored);
// Test password exists
bool exists = SecureStringUtils::hasPassword(testProfile, testKey);
QVERIFY(exists);
// Test retrieving password
QString retrieved = SecureStringUtils::retrievePassword(testProfile, testKey);
QCOMPARE(retrieved, testPassword);
// Test removing password
bool removed = SecureStringUtils::removePassword(testProfile, testKey);
QVERIFY(removed);
// Test password no longer exists
bool existsAfterRemoval = SecureStringUtils::hasPassword(testProfile, testKey);
QVERIFY(!existsAfterRemoval);
// Test retrieving non-existent password
QString nonExistent = SecureStringUtils::retrievePassword(testProfile, testKey);
QVERIFY(nonExistent.isEmpty());
// Test invalid inputs
QVERIFY(!SecureStringUtils::storePassword("", testKey, testPassword));
QVERIFY(!SecureStringUtils::storePassword(testProfile, "", testPassword));
QVERIFY(!SecureStringUtils::storePassword(testProfile, "invalid/key", testPassword));
qDebug() << "Convenience password methods tests passed";
}
void SecureStringUtilsTest::cleanupTestCase()
{
}
#include "SecureStringUtilsTest.moc"
QTEST_MAIN(SecureStringUtilsTest)