diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..a770e55e --- /dev/null +++ b/.clang-format @@ -0,0 +1,114 @@ +# Copyright (C) 2016 Olivier Goffart +# +# You may use this file under the terms of the 3-clause BSD license. +# See the file LICENSE from this package for details. + +# This is the clang-format configuration style to be used by Qt, +# based on the rules from https://wiki.qt.io/Qt_Coding_Style and +# https://wiki.qt.io/Coding_Conventions + +--- +# Webkit style was loosely based on the Qt style +BasedOnStyle: WebKit + +Standard: c++11 + +# Column width is limited to 100 in accordance with Qt Coding Style. +# https://wiki.qt.io/Qt_Coding_Style +# Note that this may be changed at some point in the future. +ColumnLimit: 100 +# How much weight do extra characters after the line length limit have. +# PenaltyExcessCharacter: 4 + +# Disable reflow of some specific comments +# qdoc comments: indentation rules are different. +# Translation comments and SPDX license identifiers are also excluded. +CommentPragmas: "^!|^:|^ SPDX-License-Identifier:" + +# We want a space between the type and the star for pointer types. +PointerBindsToType: false + +# We generally use "template <" with space. +SpaceAfterTemplateKeyword: true + +# We want to break before the operators, but not before a '='. +BreakBeforeBinaryOperators: NonAssignment + +# Braces are usually attached, but not after functions or class declarations. +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + +# When constructor initializers do not fit on one line, put them each on a new line. +ConstructorInitializerAllOnOneLineOrOnePerLine: true +# Indent initializers by 4 spaces +ConstructorInitializerIndentWidth: 4 + +# Indent width for line continuations. +ContinuationIndentWidth: 8 + +# No indentation for namespaces. +NamespaceIndentation: None + +# Allow indentation for preprocessing directives (if/ifdef/endif). https://reviews.llvm.org/rL312125 +IndentPPDirectives: AfterHash +# We only indent with 2 spaces for preprocessor directives +PPIndentWidth: 2 + +# Horizontally align arguments after an open bracket. +# The coding style does not specify the following, but this is what gives +# results closest to the existing code. +AlignAfterOpenBracket: true +AlwaysBreakTemplateDeclarations: true + +# Ideally we should also allow less short function in a single line, but +# clang-format does not handle that. +AllowShortFunctionsOnASingleLine: Inline + +# The coding style specifies some include order categories, but also tells to +# separate categories with an empty line. It does not specify the order within +# the categories. Since the SortInclude feature of clang-format does not +# re-order includes separated by empty lines, the feature is not used. +SortIncludes: false + +# macros for which the opening brace stays attached. +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ] + +# Break constructor initializers before the colon and after the commas. +BreakConstructorInitializers: BeforeColon + +# Add "// namespace " comments on closing brace for a namespace +# Ignored for namespaces that qualify as a short namespace, +# see 'ShortNamespaceLines' +FixNamespaceComments: true + +# Definition of how short a short namespace is, default 1 +ShortNamespaceLines: 1 + +# When escaping newlines in a macro attach the '\' as far left as possible, e.g. +##define a \ +# something; \ +# other; \ +# thelastlineislong; +AlignEscapedNewlines: Left + +# Avoids the addition of a space between an identifier and the +# initializer list in list-initialization. +SpaceBeforeCpp11BracedList: false + +--- +# Use the Google-based style for .proto files. +Language: Proto +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..5f0ac95b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,134 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + compiler: [msvc, gcc, clang] + qt_version: [qt5, qt6] + exclude: + - os: ubuntu-latest + compiler: msvc + - os: macos-latest + compiler: gcc + - os: macos-latest + compiler: msvc + - os: windows-latest + compiler: clang + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup CMake and Ninja + uses: lukka/get-cmake@latest + + - if: matrix.compiler == 'msvc' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Install Dependencies (Windows only) + if: runner.os == 'Windows' + uses: crazy-max/ghaction-chocolatey@v3 + with: + args: install pkgconfiglite + + - name: Install Qt (Mac/Linux only) + if: runner.os != 'Windows' + run: | + if [ '${{ runner.os }}' == 'macOS' ]; then + if [ '${{ matrix.qt_version }}' == 'qt6' ]; then + brew install qt6 pkg-config + brew link qt6 --force + echo "$(brew --prefix qt6)/bin" >> $GITHUB_PATH + else + brew install qt5 pkg-config + brew link qt5 --force + echo "$(brew --prefix qt5)/bin" >> $GITHUB_PATH + fi + elif [ '${{ runner.os }}' == 'Linux' ]; then + sudo apt-get update + if [ '${{ matrix.qt_version }}' == 'qt6' ]; then + sudo apt-get install -y qt6-base-dev qtchooser qmake6 qt6-base-dev-tools qt6-tools-dev pkg-config libsecret-1-dev + else + sudo apt-get install -y qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools qttools5-dev pkg-config libsecret-1-dev + fi + fi + + - name: Set AQT Version and Arch (Windows only) + if: runner.os == 'Windows' + id: set_qt_arch + shell: pwsh + run: | + $qtVer = "${{ matrix.qt_version == 'qt5' && '5.15.2' || '6.5.3' }}" + $qtArch = "${{ matrix.compiler == 'msvc' && 'msvc2019_64' || + matrix.qt_version == 'qt5' && matrix.compiler == 'gcc' && 'mingw81' || + matrix.qt_version == 'qt6' && matrix.compiler == 'gcc' && 'mingw' }}" + echo "QT_VER=$qtVer" >> $env:GITHUB_OUTPUT + echo "QT_ARCH=$qtArch" >> $env:GITHUB_OUTPUT + + - name: Install Qt (Windows only) + if: runner.os == 'Windows' + uses: jurplel/install-qt-action@v4 + with: + version: ${{ steps.set_qt_arch.outputs.QT_VER }} + arch: win64_${{ steps.set_qt_arch.outputs.QT_ARCH }} + dir: 'C:\' + cache: true + tools: 'tools_mingw1310' + + - name: Setup Build Directory + run: | + mkdir -p ${{ github.workspace }}/work/build/${{ github.event.repository.name }} + + - name: Run CMake (Windows only) + if: runner.os == 'Windows' + shell: pwsh + run: | + if ( '${{ matrix.compiler }}' -eq 'msvc' ) { + $generator = "NMake Makefiles" + } else { + $generator = "MinGW Makefiles" + } + if ( '${{ matrix.compiler }}' -eq 'gcc' ) { + echo "C:/Qt/Tools/mingw1310_64/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "C:\Qt\Tools\mingw1310_64\bin;$env:PATH" + } + cd "${{ github.workspace }}/work/build/${{ github.event.repository.name }}" + cmake -G $generator "${{ github.workspace }}" ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install" ` + -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR ` + ${{ matrix.qt_version == 'qt6' && '-DBUILD_WITH_QT6=true' || '' }} + + - name: Run CMake (Mac/Linux Only) + if: runner.os != 'Windows' + run: | + cd ${{ github.workspace }}/work/build/${{ github.event.repository.name }} + cmake -G Ninja ${{ github.workspace }} \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install" \ + ${{ matrix.qt_version == 'qt6' && '-DBUILD_WITH_QT6=true' || '' }} + + - name: Build and Install + run: | + cd ${{ github.workspace }}/work/build/${{ github.event.repository.name }} + cmake --build . + cmake --install . --prefix "${{ github.workspace }}/install" + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-artifact ${{ matrix.os }} ${{ matrix.compiler }} ${{ matrix.qt_version }} + path: ${{ github.workspace }}/work/build/${{ github.event.repository.name }} + diff --git a/.gitignore b/.gitignore index 8fcb5ea4..f3cc1d94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,76 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + #CMake files CMakeCache.txt CMakeFiles @@ -37,7 +110,6 @@ testclient install_manifest.txt *.manifest *.lib -*.exe #Mac build files qtkeychain.xcodeproj @@ -45,6 +117,6 @@ qtkeychain.build #Temporary files *.sw? -*~ - +/build-*/ +/compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 4449bc2d..f07d8b5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,31 +1,49 @@ -cmake_minimum_required(VERSION 2.8.11) -project(qtkeychain) +cmake_minimum_required(VERSION 3.16) -include(FindPkgConfig) +set(QTKEYCHAIN_VERSION 0.15.99) +set(QTKEYCHAIN_SOVERSION 1) -### +project(qtkeychain VERSION ${QTKEYCHAIN_VERSION} LANGUAGES CXX) -set(QTKEYCHAIN_VERSION 0.9.90) -set(QTKEYCHAIN_SOVERSION 1) +# Enable C++11 +SET(CMAKE_CXX_STANDARD 11) + +include(FindPkgConfig) ### +# write binaries and libraries into a shared folder, this simplifies the execution of tests on Windows +# see https://github.com/KDE/extra-cmake-modules/blob/b3a13868f7f54ab0b7ac19fd26b6dfead8907d25/kde-modules/KDECMakeSettings.cmake#L256C1-L258C69 +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${PROJECT_SOURCE_DIR}/cmake/Modules") +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/Modules") include(GNUInstallDirs) include(GenerateExportHeader) -include(ECMPackageConfigHelpers) +include(CMakePackageConfigHelpers) include(ECMSetupVersion) include(ECMGeneratePriFile) +include(CMakeDependentOption) -option(BUILD_WITH_QT4 "Build qtkeychain with Qt4 no matter if Qt5 was found" OFF) -option(BUILD_TEST_APPLICATION "Build test application" ON) +option(BUILD_WITH_QT6 "Build qtkeychain with Qt 6" OFF) +option(BUILD_TEST_APPLICATION "Build test application" OFF) option(BUILD_TRANSLATIONS "Build translations" ON) -option(QTKEYCHAIN_STATIC "Build static library" OFF) +option(BUILD_SHARED_LIBS "Build dynamic library" ON) +if(QTKEYCHAIN_STATIC) + set(BUILD_SHARED_LIBS OFF) + message(WARNING "QTKEYCHAIN_STATIC is deprecated. Use BUILD_SHARED_LIBS=OFF instead.") +endif() +CMAKE_DEPENDENT_OPTION(BUILD_TRANSLATIONS_AS_RESOURCES "Bundle translations with the library" OFF + "BUILD_TRANSLATIONS" OFF) if(CMAKE_SYSTEM_NAME STREQUAL Android) set(ANDROID 1) endif() +if(CMAKE_SYSTEM_NAME STREQUAL Haiku) + set(HAIKU 1) +endif() + if (WIN32) option(USE_CREDENTIAL_STORE "Build with windows CredentialStore support" ON) @@ -34,16 +52,26 @@ if (WIN32) endif() endif() -if( NOT BUILD_WITH_QT4 ) - # try Qt5 first, and prefer that if found - find_package(Qt5Core QUIET) +if( NOT BUILD_WITH_QT6 ) + find_package(Qt5 COMPONENTS Core REQUIRED) endif() -if (Qt5Core_FOUND AND NOT BUILD_WITH_QT4) +if (Qt5Core_FOUND AND NOT BUILD_WITH_QT6) set(QTKEYCHAIN_VERSION_INFIX 5) - if(UNIX AND NOT APPLE AND NOT ANDROID) - find_package(Qt5DBus REQUIRED) + if(ANDROID) + if(Qt5Core_VERSION VERSION_LESS 5.7) + find_package(Qt5 COMPONENTS Core REQUIRED Private) + include_directories(${Qt5Core_PRIVATE_INCLUDE_DIRS}) + endif() + + find_package(Qt5 COMPONENTS AndroidExtras REQUIRED) + include_directories(${Qt5AndroidExtras_INCLUDE_DIRS}) + set(QTANDROIDEXTRAS_LIBRARIES ${Qt5AndroidExtras_LIBRARIES}) + endif() + + if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT HAIKU AND NOT EMSCRIPTEN) + find_package(Qt5 COMPONENTS DBus REQUIRED) include_directories(${Qt5DBus_INCLUDE_DIRS}) set(QTDBUS_LIBRARIES ${Qt5DBus_LIBRARIES}) macro(qt_add_dbus_interface) @@ -52,7 +80,7 @@ if (Qt5Core_FOUND AND NOT BUILD_WITH_QT4) endif() if(BUILD_TRANSLATIONS) - find_package(Qt5LinguistTools REQUIRED) + find_package(Qt5 COMPONENTS LinguistTools REQUIRED) macro(qt_add_translation) qt5_add_translation(${ARGN}) endmacro(qt_add_translation) @@ -67,180 +95,124 @@ if (Qt5Core_FOUND AND NOT BUILD_WITH_QT4) set(QTCORE_LIBRARIES ${Qt5Core_LIBRARIES}) include_directories(${Qt5Core_INCLUDE_DIRS}) - - if (NOT Qt5Core_VERSION VERSION_LESS "5.7.0") - if (CMAKE_COMPILER_IS_GNUCXX) - if ((NOT CMAKE_CXX_COMPILER_VERSION VERSION_LESS "4.7.0") AND (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "6.1.0")) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - elseif(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "4.7.0") - message(FATAL_ERROR "Can't build QtKeychain using g++-${CMAKE_CXX_COMPILER_VERSION} and Qt ${Qt5Core_VERSION}: compiler supporting C++11 is required") - endif() - elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") - if (NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 3.3) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - else() - message(FATAL_ERROR "Can't build QtKeychain using clang++-${CMAKE_CXX_COMPILER_VERSION} and Qt ${Qt5Core_VERSION}: compiler supporting C++11 is required") - endif() - elseif ((CMAKE_CXX_COMPILER_ID MATCHES "MSVC") AND (MSVC_VERSION LESS 1700)) - message(FATAL_ERROR "Can't build QtKeychain using VC++-${MSVC_VERSION} and Qt ${Qt5Core_VERSION}: compiler supporting C++11 is required") - endif() - endif() else() - set(QTKEYCHAIN_VERSION_INFIX "") - if(UNIX AND NOT APPLE) - find_package(Qt4 COMPONENTS QtCore QtDBus REQUIRED) - set(QTDBUS_LIBRARIES ${QT_QTDBUS_LIBRARY}) + find_package(Qt6 COMPONENTS Core REQUIRED) + set(QTKEYCHAIN_VERSION_INFIX 6) + + + if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT HAIKU AND NOT EMSCRIPTEN) + find_package(Qt6 COMPONENTS DBus REQUIRED) + include_directories(${Qt6DBus_INCLUDE_DIRS}) + set(QTDBUS_LIBRARIES ${Qt6DBus_LIBRARIES}) macro(qt_add_dbus_interface) - qt4_add_dbus_interface(${ARGN}) + qt6_add_dbus_interface(${ARGN}) endmacro() - else() - find_package(Qt4 COMPONENTS QtCore REQUIRED) endif() - include_directories(${QT_INCLUDES}) - set(QTCORE_LIBRARIES ${QT_QTCORE_LIBRARY}) - macro(qt_add_translation) - qt4_add_translation(${ARGN}) - endmacro(qt_add_translation) - macro(qt_create_translation) - qt4_create_translation(${ARGN}) - endmacro(qt_create_translation) - macro(qt_wrap_cpp) - qt4_wrap_cpp(${ARGN}) - endmacro() -endif() - - -include_directories(${CMAKE_CURRENT_BINARY_DIR}) -list(APPEND qtkeychain_LIBRARIES ${QTCORE_LIBRARIES}) -set(qtkeychain_SOURCES - keychain.cpp - qkeychain_export.h - keychain.h -) - -add_definitions( -Wall ) - -if(WIN32) - list(APPEND qtkeychain_SOURCES keychain_win.cpp) - if (NOT USE_CREDENTIAL_STORE) - list(APPEND qtkeychain_LIBRARIES crypt32) - list(APPEND qtkeychain_SOURCES plaintextstore.cpp) - endif() - #FIXME: mingw bug; otherwise getting undefined refs to RtlSecureZeroMemory there - if(MINGW) - add_definitions( -O2 ) - endif() -endif() - -if(APPLE) - if(IOS) - list(APPEND qtkeychain_SOURCES keychain_ios.cpp) - else() - list(APPEND qtkeychain_SOURCES keychain_mac.cpp) - endif() + if(BUILD_TRANSLATIONS) + find_package(Qt6 COMPONENTS LinguistTools REQUIRED) + macro(qt_add_translation) + qt6_add_translation(${ARGN}) + endmacro(qt_add_translation) + macro(qt_create_translation) + qt6_create_translation(${ARGN}) + endmacro(qt_create_translation) + endif() - find_library(COREFOUNDATION_LIBRARY CoreFoundation REQUIRED) - list(APPEND qtkeychain_LIBRARIES ${COREFOUNDATION_LIBRARY}) + macro(qt_wrap_cpp) + qt6_wrap_cpp(${ARGN}) + endmacro() - find_library(SECURITY_LIBRARY Security REQUIRED) - list(APPEND qtkeychain_LIBRARIES ${SECURITY_LIBRARY}) + set(QTCORE_LIBRARIES ${Qt6Core_LIBRARIES}) endif() -if(UNIX AND NOT APPLE AND NOT ANDROID) - option(LIBSECRET_SUPPORT "Build with libsecret support" ON) - - if(LIBSECRET_SUPPORT) - pkg_check_modules(LIBSECRET libsecret-1) - - if (LIBSECRET_FOUND) - add_definitions(-DHAVE_LIBSECRET=1) - endif() - INCLUDE_DIRECTORIES(${LIBSECRET_INCLUDE_DIRS}) - list(APPEND qtkeychain_LIBRARIES ${LIBSECRET_LIBRARIES}) - endif() +set(QTKEYCHAIN_TARGET_NAME qt${QTKEYCHAIN_VERSION_INFIX}keychain) - list(APPEND qtkeychain_SOURCES keychain_unix.cpp gnomekeyring.cpp libsecret.cpp plaintextstore.cpp) - qt_add_dbus_interface(qtkeychain_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.KWallet.xml kwallet_interface KWalletInterface) - list(APPEND qtkeychain_LIBRARIES ${QTDBUS_LIBRARIES} ) -endif() +add_subdirectory(qtkeychain) -QT_WRAP_CPP(qtkeychain_MOC_OUTFILES keychain.h keychain_p.h gnomekeyring_p.h) +### +### Translations +### set(qtkeychain_TR_FILES translations/qtkeychain_de.ts + translations/qtkeychain_fr.ts + translations/qtkeychain_ka.ts translations/qtkeychain_ro.ts + translations/qtkeychain_ru.ts + translations/qtkeychain_sv.ts + translations/qtkeychain_zh.ts ) -file(GLOB qtkeychain_TR_SOURCES *.cpp *.h *.ui) +file(GLOB qtkeychain_TR_SOURCES qtkeychain/*.cpp qtkeychain/*.h qtkeychain/*.ui) if ( BUILD_TRANSLATIONS ) qt_create_translation(qtkeychain_MESSAGES ${qtkeychain_TR_SOURCES} ${qtkeychain_TR_FILES}) qt_add_translation(qtkeychain_QM_FILES ${qtkeychain_TR_FILES}) add_custom_target(messages DEPENDS ${qtkeychain_MESSAGES}) - add_custom_target(translations DEPENDS ${qtkeychain_QM_FILES}) - - if(NOT QT_TRANSLATIONS_DIR) - # If this directory is missing, we are in a Qt5 environment. - # Extract the qmake executable location - get_target_property(QT5_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION) - # Ask Qt5 where to put the translations - execute_process( COMMAND ${QT5_QMAKE_EXECUTABLE} -query QT_INSTALL_TRANSLATIONS - OUTPUT_VARIABLE qt_translations_dir OUTPUT_STRIP_TRAILING_WHITESPACE ) - # make sure we have / and not \ as qmake gives on windows - file( TO_CMAKE_PATH "${qt_translations_dir}" qt_translations_dir) - set( QT_TRANSLATIONS_DIR ${qt_translations_dir} CACHE PATH - "The location of the Qt translations" FORCE) - endif() + add_custom_target(translations DEPENDS ${qtkeychain_QM_FILES} messages) + # https://github.com/frankosterfeld/qtkeychain/issues/185 + add_dependencies(${QTKEYCHAIN_TARGET_NAME} translations) + + if (BUILD_TRANSLATIONS_AS_RESOURCES) + set(QM_FILE_LIST "") + foreach(FILE ${qtkeychain_QM_FILES}) + list(APPEND QM_FILE_LIST "${FILE}") + endforeach() + string(REPLACE ";" "" QM_FILE_LIST ${QM_FILE_LIST}) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/translations/translations.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc) + target_sources(${QTKEYCHAIN_TARGET_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc) + else() + if(QTKEYCHAIN_VERSION_INFIX EQUAL 5 AND QT_TRANSLATIONS_DIR AND NOT QTKEYCHAIN_TRANSLATIONS_DIR) + # Back compatibility with pre-0.11 versions + message (WARNING "QT_TRANSLATIONS_DIR is deprecated, use QTKEYCHAIN_TRANSLATIONS_DIR instead") + set(QTKEYCHAIN_TRANSLATIONS_DIR ${QT_TRANSLATIONS_DIR} + CACHE PATH "The location of the QtKeychain translations" FORCE) + else() + set(QTKEYCHAIN_TRANSLATIONS_DIR + ${CMAKE_INSTALL_DATADIR}/qt${QTKEYCHAIN_VERSION_INFIX}keychain/translations + CACHE PATH "The location of the QtKeychain translations" ) + endif() - install(FILES ${qtkeychain_QM_FILES} - DESTINATION ${QT_TRANSLATIONS_DIR}) + install(FILES ${qtkeychain_QM_FILES} DESTINATION ${QTKEYCHAIN_TRANSLATIONS_DIR}) + endif() endif( BUILD_TRANSLATIONS ) -set(QTKEYCHAIN_TARGET_NAME qt${QTKEYCHAIN_VERSION_INFIX}keychain) -if(NOT QTKEYCHAIN_STATIC) - add_library(${QTKEYCHAIN_TARGET_NAME} SHARED ${qtkeychain_SOURCES} ${qtkeychain_MOC_OUTFILES} ${qtkeychain_QM_FILES}) -else() - add_library(${QTKEYCHAIN_TARGET_NAME} STATIC ${qtkeychain_SOURCES} ${qtkeychain_MOC_OUTFILES} ${qtkeychain_QM_FILES}) -endif() -target_link_libraries(${QTKEYCHAIN_TARGET_NAME} PUBLIC ${qtkeychain_LIBRARIES}) -target_include_directories(${QTKEYCHAIN_TARGET_NAME} PUBLIC $) +### +### Test application ("testclient") +### -generate_export_header(${QTKEYCHAIN_TARGET_NAME} - EXPORT_FILE_NAME qkeychain_export.h - EXPORT_MACRO_NAME QKEYCHAIN_EXPORT -) +if(BUILD_TEST_APPLICATION) + set( testclient_LIBRARIES ${QTKEYCHAIN_TARGET_NAME} ) -set_target_properties(${QTKEYCHAIN_TARGET_NAME} PROPERTIES - VERSION ${QTKEYCHAIN_VERSION} - SOVERSION ${QTKEYCHAIN_SOVERSION} - MACOSX_RPATH 1 - INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}" - INSTALL_RPATH_USE_LINK_PATH TRUE -) + if(APPLE) + list(APPEND testclient_LIBRARIES "-framework Cocoa") -install(FILES keychain.h ${CMAKE_CURRENT_BINARY_DIR}/qkeychain_export.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/qt${QTKEYCHAIN_VERSION_INFIX}keychain/ -) - -install(TARGETS ${QTKEYCHAIN_TARGET_NAME} - EXPORT Qt${QTKEYCHAIN_VERSION_INFIX}KeychainLibraryDepends - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} -) + if (BUILD_WITH_QT6) + find_package(Qt6 COMPONENTS Gui REQUIRED) + list(APPEND testclient_LIBRARIES Qt6::Gui) + else() + find_package(Qt5 COMPONENTS Gui REQUIRED) + list(APPEND testclient_LIBRARIES Qt5::Gui) + endif() -if(BUILD_TEST_APPLICATION) + endif() add_executable( testclient testclient.cpp ) - target_link_libraries( testclient ${QTKEYCHAIN_TARGET_NAME}) + target_link_libraries( testclient ${testclient_LIBRARIES}) +endif() + +include(CTest) +if(BUILD_TESTING) + add_subdirectory(autotest) endif() + ### ### CMake config file ### -ecm_configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/QtKeychainConfig.cmake.in" +configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/QtKeychainConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/Qt${QTKEYCHAIN_VERSION_INFIX}KeychainConfig.cmake" INSTALL_DESTINATION Qt${QTKEYCHAIN_VERSION_INFIX}Keychain) @@ -248,7 +220,7 @@ ecm_setup_version("${QTKEYCHAIN_VERSION}" VARIABLE_PREFIX SNORE PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/Qt${QTKEYCHAIN_VERSION_INFIX}KeychainConfigVersion.cmake" SOVERSION ${QTKEYCHAIN_VERSION}) -if(UNIX AND NOT APPLE AND NOT ANDROID) +if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT HAIKU) set(PRI_EXTRA_DEPS "dbus") endif() ecm_generate_pri_file(BASE_NAME Qt${QTKEYCHAIN_VERSION_INFIX}Keychain diff --git a/COPYING b/COPYING index cca2a5c9..69f70fff 100644 --- a/COPYING +++ b/COPYING @@ -7,6 +7,9 @@ are met: 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to + endorse or promote products derived from this software without + specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES diff --git a/ChangeLog b/ChangeLog index d04ac0b2..04b4bdd4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,78 @@ ChangeLog ========= +version 0.15.0 (release 2025-01-16) + + - Windows: Increase size of possible payloads (Hannah von Reth ) + - KWallet: Increase timeout when opening wallet (David Faure ) + - Haiku: Fix reading passwords (Joshua Goins ) + - Add unit tests (Hannah von Reth ) + - Windows build fixes (Nerixyz ) + - Fix translation creation (Christophe Marin ) + +version 0.14.3 (release 2024-05-03) + + - Fix Android build for Qt 6.7 (Volker Krause ) + +version 0.14.2 (release 2023-12-17) + + - Add support for KWallet 6 (Volker Krause ) + +version 0.14.1 (release 2023-06-01) + + - Export QKeychain::isAvailable() to make it usable in a shared build (Volker Krause ) + - Protect against creating the QtKeychain::QtKeychain alias target twice (Volker Krause ) + +version 0.14.0 (release 2023-05-12) + + - Add Qt 6 Android support (Igor Bugaev ) + - Add QtQuick client example ((Igor Bugaev ) + - Added Dutch translation (Heimen Stoffels ) + - Fix potential freezing with Apple keychain (Claudio Cambra ) + - Add API to check whether a secure backend is available at all (Volker Krause ) + +version 0.13.2 (release 2021-11-18) + + - CMake: Deprecate QTKEYCHAIN_STATIC in favor of BUILD_SHARED_LIBS (be@mixxx.org) + +version 0.13.1 (release 2021-11-08) + + - KWallet: Fix deletion of entries (Issue #199) + +version 0.13.0 (release 2021-11-07) + + - Linux: Require libsecret if not explicitly disabled + - Unify implementations for macOS and iOS + - CMake: lots of fixes + +version 0.12.0 (release 2020-12-16) + + * Add Qt 6 support, drop Qt 4 support + * Require C++11 + * Add Android support (Mathias Hasselmann) + +version 0.11.1 (release 2020-09-08) + + * Build system fixes + +version 0.11.0 (release 2020-09-08) + + * Important: Debug builds on Windows now get the "d" suffix + * Various build system fixes + * Add Haiku support (François Revol ) + * Translation: Russian (Alexander Gorishnyak ) + * Translation: Update French (David Geiger ) + +version 0.10.0 (release 2019-12-17) + + * Detect XFCE desktop correctly. (Sandro Knauß ) + * Windows Use CRED_PERSIST_ENTERPRISE (Olivier Goffart ) + * Windows: Improve CredWrite() error handling (Christian Kamm ) + * Fix build with Qt 5.12.x (Sergey Ilinykh ) + * Fix Qt 4 build (Robert-André Mauchin ) + * Translation: Mandarin (Taiwan) (Poren Chiang ) + * Translation: French (François Revol ) + version 0.9.1 (release 2018-08-20) * Windows Credential Store: Use CRED_PERSIST_ENTERPRISE (Olivier Goffart ) * Secret: Don't match the schema name #114 (Christian Kamm ) diff --git a/QtKeychainConfig.cmake.in b/QtKeychainConfig.cmake.in index 3196bffe..9395e362 100644 --- a/QtKeychainConfig.cmake.in +++ b/QtKeychainConfig.cmake.in @@ -2,21 +2,27 @@ # It defines the following variables # QTKEYCHAIN_INCLUDE_DIRS - include directories for QtKeychain # QTKEYCHAIN_LIBRARIES - libraries to link against +# as well as the following imported targets +# qt5keychain / qt6keychain +# Qt5Keychain::Qt5Keychain / Qt6Keychain::Qt6Keychain @PACKAGE_INIT@ include("${CMAKE_CURRENT_LIST_DIR}/Qt@QTKEYCHAIN_VERSION_INFIX@KeychainLibraryDepends.cmake") +include(CMakeFindDependencyMacro) -if("@QTKEYCHAIN_VERSION_INFIX@" STREQUAL "5") - find_dependency(Qt5Core) - - if(UNIX AND NOT APPLE) - find_dependency(Qt5DBus) - endif() -else() - find_dependency(Qt4 COMPONENTS QtCore) +find_dependency(Qt@QTKEYCHAIN_VERSION_INFIX@Core) + +if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT EMSCRIPTEN) + find_dependency(Qt@QTKEYCHAIN_VERSION_INFIX@DBus) endif() set(QTKEYCHAIN_LIBRARIES "@QTKEYCHAIN_TARGET_NAME@") get_target_property(QTKEYCHAIN_INCLUDE_DIRS "@QTKEYCHAIN_TARGET_NAME@" INTERFACE_INCLUDE_DIRECTORIES) + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.18.0 AND NOT TARGET Qt@QTKEYCHAIN_VERSION_INFIX@Keychain::Qt@QTKEYCHAIN_VERSION_INFIX@Keychain) + add_library(Qt@QTKEYCHAIN_VERSION_INFIX@Keychain::Qt@QTKEYCHAIN_VERSION_INFIX@Keychain ALIAS qt@QTKEYCHAIN_VERSION_INFIX@keychain) +endif() + +check_required_components(Qt@QTKEYCHAIN_VERSION_INFIX@Keychain) diff --git a/ReadMe.markdown b/ReadMe.markdown deleted file mode 120000 index 3cefeef2..00000000 --- a/ReadMe.markdown +++ /dev/null @@ -1 +0,0 @@ -ReadMe.txt \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 00000000..cf0a2e13 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,29 @@ +QtKeychain +========== + +QtKeychain is a Qt API to store passwords and other secret data securely. How the data is stored depends on the platform: + + * **macOS:** Passwords are stored in the macOS Keychain. + + * **Linux/Unix:** If running, GNOME Keyring is used, otherwise QtKeychain tries to use KWallet (via D-Bus), if available. Libsecret (common API for desktop-specific solutions) + is also supported. + + * **Windows:** By default, the Windows Credential Store is used (requires Windows 7 or newer). +Pass `-DUSE_CREDENTIAL_STORE=OFF` to cmake to disable it. If disabled, QtKeychain uses the Windows API function +[CryptProtectData](http://msdn.microsoft.com/en-us/library/windows/desktop/aa380261%28v=vs.85%29.aspx "CryptProtectData function") +to encrypt the password with the user's logon credentials. The encrypted data is then persisted via QSettings. + + * **Android and iOS:** Passwords are stored in the Android keystore system and iOS keychain, respectively. + +In unsupported environments QtKeychain will report an error. It will not store any data unencrypted unless explicitly requested (`setInsecureFallback( true )`). + + +Requirements +------------ + +QtKeychain 0.12 and newer supports Qt 5 and Qt 6 and requires a compiler with C++11 support. Older versions support Qt 4 and Qt 5. + +License +------- + +QtKeychain is available under the [Modified BSD License](http://www.gnu.org/licenses/license-list.html#ModifiedBSD). See the file COPYING for details. diff --git a/ReadMe.txt b/ReadMe.txt deleted file mode 100644 index 6fa68b6b..00000000 --- a/ReadMe.txt +++ /dev/null @@ -1,17 +0,0 @@ -QtKeychain -========== - -QtKeychain is a Qt API to store passwords and other secret data securely. How the data is stored depends on the platform: - - * **Mac OS X:** Passwords are stored in the OS X Keychain. - - * **Linux/Unix:** If running, GNOME Keyring is used, otherwise qtkeychain tries to use KWallet (via D-Bus), if available. - - * **Windows:** By default, the Windows Credential Store is used (requires Windows 7 or newer). -Pass -DUSE_CREDENTIAL_STORE=OFF to cmake use disable it. If disabled, QtKeychain uses the Windows API function -[CryptProtectData](http://msdn.microsoft.com/en-us/library/windows/desktop/aa380261%28v=vs.85%29.aspx "CryptProtectData function") -to encrypt the password with the user's logon credentials. The encrypted data is then persisted via QSettings. - -In unsupported environments QtKeychain will report an error. It will not store any data unencrypted unless explicitly requested (setInsecureFallback( true )). - -**License:** QtKeychain is available under the [Modified BSD License](http://www.gnu.org/licenses/license-list.html#ModifiedBSD). See the file COPYING for details. diff --git a/TestAppExample/.gitignore b/TestAppExample/.gitignore new file mode 100644 index 00000000..fab7372d --- /dev/null +++ b/TestAppExample/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/TestAppExample/CMakeLists.txt b/TestAppExample/CMakeLists.txt new file mode 100644 index 00000000..5e3f924c --- /dev/null +++ b/TestAppExample/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) +project(TestAppExample VERSION ${QTKEYCHAIN_VERSION} LANGUAGES CXX) + +include(GNUInstallDirs) + +# Enable C++11 +SET(CMAKE_CXX_STANDARD 11) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +option(BUILD_WITH_QT6 "Build qtkeychain with Qt 6" OFF) +if (BUILD_WITH_QT6) + set(QTx Qt6) +else() + set(QTx Qt5) +endif() + +find_package(${QTx}Keychain REQUIRED) +find_package(${QTx} COMPONENTS Core Network Quick Qml REQUIRED) + +if (BUILD_WITH_QT6) + include_directories(${QTKEYCHAIN_INCLUDE_DIRS}/qt6keychain) +else() + include_directories(${QTKEYCHAIN_INCLUDE_DIRS}/qt5keychain) +endif() + +qt_add_resources(QT_RESOURCES qml.qrc) +add_executable(${PROJECT_NAME} + keychainclass.h + keychainclass.cpp + main.cpp + ${QT_RESOURCES} +) + +target_link_libraries(${PROJECT_NAME} PRIVATE ${QTx}::Core ${QTx}::Network ${QTx}::Quick ${QTx}::Qml ${QTx}Keychain::${QTx}Keychain) +install(TARGETS ${PROJECT_NAME}) diff --git a/TestAppExample/TestAppExample.pro b/TestAppExample/TestAppExample.pro new file mode 100644 index 00000000..3ba33eff --- /dev/null +++ b/TestAppExample/TestAppExample.pro @@ -0,0 +1,29 @@ +QT += quick + +CONFIG += c++11 + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +include(../qtkeychain.pri) + +SOURCES += \ + keychainclass.cpp \ + main.cpp + +RESOURCES += qml.qrc + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +# Additional import path used to resolve QML modules just for Qt Quick Designer +QML_DESIGNER_IMPORT_PATH = + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +HEADERS += \ + keychainclass.h diff --git a/TestAppExample/keychainclass.cpp b/TestAppExample/keychainclass.cpp new file mode 100644 index 00000000..696d5e84 --- /dev/null +++ b/TestAppExample/keychainclass.cpp @@ -0,0 +1,72 @@ +// fix for https://github.com/frankosterfeld/qtkeychain/issues/288 + +#include + +#include "keychainclass.h" + +static const QString service = "keychain.example.project.app"; + +KeyChainClass::KeyChainClass(QObject *parent) + : QObject(parent) +{ + +} + +void KeyChainClass::readKey(const QString &key) +{ + auto readCredentialJob = new QKeychain::ReadPasswordJob(service); + readCredentialJob->setAutoDelete(true); + readCredentialJob->setKey(key); + + QObject::connect(readCredentialJob, &QKeychain::ReadPasswordJob::finished, this, + [this, key](QKeychain::Job *job) { + auto j = static_cast(job); + if (j->error() == QKeychain::NoError) { + emit keyRestored(key, j->textData()); + } else { + emit error(tr("Read key failed: %1").arg(qPrintable(j->errorString()))); + } + // no delete needed, autoDelete takes care of it + }); + + readCredentialJob->start(); +} + +void KeyChainClass::writeKey(const QString &key, const QString &value) +{ + auto writeCredentialJob = new QKeychain::WritePasswordJob(service); + writeCredentialJob->setAutoDelete(true); + writeCredentialJob->setKey(key); + writeCredentialJob->setTextData(value); + + QObject::connect(writeCredentialJob, &QKeychain::WritePasswordJob::finished, this, + [this, key](QKeychain::Job *job) { + auto j = static_cast(job); + if (j->error() == QKeychain::NoError) { + emit keyStored(key); + } else { + emit error(tr("Write key failed: %1").arg(qPrintable(j->errorString()))); + } + }); + + writeCredentialJob->start(); +} + +void KeyChainClass::deleteKey(const QString &key) +{ + auto deleteCredentialJob = new QKeychain::DeletePasswordJob(service); + deleteCredentialJob->setAutoDelete(true); + deleteCredentialJob->setKey(key); + + QObject::connect(deleteCredentialJob, &QKeychain::DeletePasswordJob::finished, this, + [this, key](QKeychain::Job *job) { + auto j = static_cast(job); + if (j->error() == QKeychain::NoError) { + emit keyDeleted(key); + } else { + emit error(tr("Delete key failed: %1").arg(qPrintable(j->errorString()))); + } + }); + + deleteCredentialJob->start(); +} diff --git a/TestAppExample/keychainclass.h b/TestAppExample/keychainclass.h new file mode 100644 index 00000000..100e0ea4 --- /dev/null +++ b/TestAppExample/keychainclass.h @@ -0,0 +1,27 @@ +#ifndef KEYCHAINCLASS_H +#define KEYCHAINCLASS_H + +#include + +#include + +class KeyChainClass : public QObject +{ + Q_OBJECT +public: + KeyChainClass(QObject *parent = nullptr); + + Q_INVOKABLE void readKey(const QString &key); + Q_INVOKABLE void writeKey(const QString &key, const QString &value); + Q_INVOKABLE void deleteKey(const QString &key); + +Q_SIGNALS: + void keyStored(const QString &key); + void keyRestored(const QString &key, const QString &value); + void keyDeleted(const QString &key); + void error(const QString &errorText); + +private: +}; + +#endif // KEYCHAINCLASS_H diff --git a/TestAppExample/main.cpp b/TestAppExample/main.cpp new file mode 100644 index 00000000..3d9f1bd5 --- /dev/null +++ b/TestAppExample/main.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "keychainclass.h" + +int main(int argc, char *argv[]) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + const QUrl url(QStringLiteral("qrc:/main.qml")); + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreated, &app, + [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + }, + Qt::QueuedConnection); + + KeyChainClass keyChainClass; + + engine.rootContext()->setContextProperty("KeyChain", &keyChainClass); + engine.load(url); + + return app.exec(); +} diff --git a/TestAppExample/main.qml b/TestAppExample/main.qml new file mode 100644 index 00000000..4023aca6 --- /dev/null +++ b/TestAppExample/main.qml @@ -0,0 +1,131 @@ +import QtQuick 2.13 +import QtQuick.Window 2.13 +import QtQuick.Controls 2.12 + +Window { + id: root + + width: 640 + height: 480 + visible: true + + Column { + anchors { + fill: parent + margins: 50 + } + spacing: 20 + + Label { + text: 'Key name:' + font.pixelSize: 20 + } + + TextField { + id: keyNameTextField + + width: parent.width + height: 50 + + text: 'default key name' + } + + Label { + text: 'Key value:' + font.pixelSize: 20 + } + + TextField { + id: keyValueTextField + + width: parent.width + height: 50 + + text: 'some value' + } + + Label { + id: infoLabel + + width: parent.width + wrapMode: Text.Wrap + visible: false + + onVisibleChanged: if (visible) hideAnimation.start(); + + SequentialAnimation { + id: hideAnimation + + PauseAnimation { + duration: 10000 + } + + ScriptAction { + script: infoLabel.visible = false + } + } + + Component.onCompleted: { + KeyChain.keyStored.connect((key) => { + infoLabel.text = String("Key '%1' successfully stored").arg(key) + infoLabel.color = 'green' + infoLabel.visible = true + }) + + KeyChain.keyRestored.connect((key, value) => { + infoLabel.text = String("Key '%1' successfully restored with data '%2'").arg(key).arg(value) + infoLabel.color = 'green' + infoLabel.visible = true + }) + + KeyChain.keyDeleted.connect((key) => { + infoLabel.text = String("Key '%1' successfully deleted").arg(key) + infoLabel.color = 'green' + infoLabel.visible = true + }) + + KeyChain.error.connect((errorText) => { + infoLabel.text = errorText + infoLabel.color = 'red' + infoLabel.visible = true + }) + } + } + + Row { + width: parent.width + height: 50 + spacing: 20 + + Button { + width: 80 + height: parent.height + text: 'Store' + + onClicked: { + KeyChain.writeKey(keyNameTextField.text.trim(), keyValueTextField.text.trim()) + } + } + + Button { + width: 80 + height: parent.height + text: 'Restore' + + onClicked: { + KeyChain.readKey(keyNameTextField.text.trim()) + } + } + + Button { + width: 80 + height: parent.height + text: 'Delete' + onClicked: { + KeyChain.deleteKey(keyNameTextField.text.trim()) + } + } + } + + } +} diff --git a/TestAppExample/qml.qrc b/TestAppExample/qml.qrc new file mode 100644 index 00000000..5f6483ac --- /dev/null +++ b/TestAppExample/qml.qrc @@ -0,0 +1,5 @@ + + + main.qml + + diff --git a/autotest/CMakeLists.txt b/autotest/CMakeLists.txt new file mode 100644 index 00000000..ffd82dee --- /dev/null +++ b/autotest/CMakeLists.txt @@ -0,0 +1,10 @@ +include(ECMAddTests) + +if (BUILD_WITH_QT6) + find_package(Qt6 CONFIG COMPONENTS Test REQUIRED) +else() + find_package(Qt5 CONFIG COMPONENTS Test REQUIRED) +endif() + +ecm_add_tests(basic.cpp LINK_LIBRARIES ${QTKEYCHAIN_TARGET_NAME} Qt${QT_MAJOR_VERSION}::Test) +set_property(TARGET basic PROPERTY AUTOMOC ON) diff --git a/autotest/basic.cpp b/autotest/basic.cpp new file mode 100644 index 00000000..6feeea05 --- /dev/null +++ b/autotest/basic.cpp @@ -0,0 +1,99 @@ +#include + +#include "qtkeychain/keychain.h" + +namespace { +QByteArray generateRandomString(qsizetype size) +{ + std::vector buffer(size, 0); + QRandomGenerator::global()->fillRange(buffer.data(), size); + return QByteArray(reinterpret_cast(buffer.data()), + static_cast(size * sizeof(quint32))) + .toBase64(QByteArray::Base64UrlEncoding) + .mid(0, size); +} + +} // namespace +class BasicTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void test_data() + { + QTest::addColumn("password"); + QTest::addColumn("usernames"); + + QTest::newRow("normal password") << QByteArrayLiteral("this is a password") << QStringList{"", "user1", "user2"}; + QTest::newRow("1000") << generateRandomString(1000) << QStringList{"", "user1", "user2"}; + QTest::newRow("2000") << generateRandomString(2000)<< QStringList{"", "user1", "user2"}; + QTest::newRow("3000") << generateRandomString(3000)<< QStringList{"", "user1", "user2"}; + QTest::newRow("10000") << generateRandomString(10000)<< QStringList{"", "user1", "user2"}; + QTest::newRow("18944") << generateRandomString(18944)<< QStringList{"", "user1", "user2"}; + } + + void test() + { +#ifdef Q_OS_MACOS + QSKIP("This test case has no access to the keychain"); +#endif + const QStringList serviceKeys ={"", QStringLiteral("QtKeychainTest-%1").arg(QTest::currentDataTag())}; + QFETCH(QByteArray, password); + QFETCH(QStringList, usernames); + + for (const auto& serviceKey: serviceKeys) + { + for (const auto& username : usernames) + { + QKeychain::WritePasswordJob writeJob(serviceKey); + writeJob.setKey(username); + writeJob.setBinaryData(username.toUtf8()+password); + QSignalSpy writeSpy(&writeJob, &QKeychain::WritePasswordJob::finished); + writeJob.start(); + writeSpy.wait(); +#ifdef Q_OS_WIN + QEXPECT_FAIL("18944", "Maximum for Windows is exceeded", Abort); +#endif + qDebug() << "[write]" << writeJob.error() << ": " << writeJob.errorString(); + const auto expected = (serviceKey.isEmpty() && username.isEmpty()) ? QKeychain::EntryNotFound : QKeychain::NoError; + QCOMPARE(writeJob.error(), expected); + } + } + + for (const auto& serviceKey: serviceKeys) + { + for (const auto& username : usernames) + { + QKeychain::ReadPasswordJob readJob(serviceKey); + readJob.setKey(username); + QSignalSpy readSpy(&readJob, &QKeychain::ReadPasswordJob::finished); + readJob.start(); + readSpy.wait(); + qDebug() << "[read]" << readJob.error() << ": " << readJob.errorString(); + const auto expected = (serviceKey.isEmpty() && username.isEmpty()) ? QKeychain::EntryNotFound : QKeychain::NoError; + QCOMPARE(readJob.error(), expected); + if (expected == QKeychain::NoError) { + QCOMPARE(readJob.binaryData(), username.toUtf8()+password); + } + } + } + + for (const auto& serviceKey: serviceKeys) + { + for (const auto& username : usernames) + { + QKeychain::DeletePasswordJob deleteJob(serviceKey); + deleteJob.setKey(username); + QSignalSpy deleteSpy(&deleteJob, &QKeychain::DeletePasswordJob::finished); + deleteJob.start(); + deleteSpy.wait(); + qDebug() << "[delete]" << deleteJob.error() << ": " << deleteJob.errorString(); + const auto expected = (serviceKey.isEmpty() && username.isEmpty()) ? QKeychain::EntryNotFound : QKeychain::NoError; + QCOMPARE(deleteJob.error(), expected); + } + } + } +}; + +QTEST_MAIN(BasicTest) +#include "basic.moc" diff --git a/cmake/Modules/ECMAddTests.cmake b/cmake/Modules/ECMAddTests.cmake new file mode 100644 index 00000000..47a4d5c1 --- /dev/null +++ b/cmake/Modules/ECMAddTests.cmake @@ -0,0 +1,176 @@ +# SPDX-FileCopyrightText: 2013 Alexander Richardson +# SPDX-FileCopyrightText: 2015 Alex Merry +# +# SPDX-License-Identifier: BSD-3-Clause + +#[=======================================================================[.rst: +ECMAddTests +----------- + +Convenience functions for adding tests. + +:: + + ecm_add_tests( + LINK_LIBRARIES [ [...]] + [NAME_PREFIX ] + [GUI] + [TARGET_NAMES_VAR ] + [TEST_NAMES_VAR ] + [WORKING_DIRECTORY ] # Since 5.111 + ) + +A convenience function for adding multiple tests, each consisting of a +single source file. For each file in , an executable target will be +created (the name of which will be the basename of the source file). This +will be linked against the libraries given with ``LINK_LIBRARIES``. Each +executable will be added as a test with the same name. + +If ``NAME_PREFIX`` is given, this prefix will be prepended to the test names, but +not the target names. As a result, it will not prevent clashes between tests +with the same name in different parts of the project, but it can be used to +give an indication of where to look for a failing test. + +If the flag ``GUI`` is passed the test binaries will be GUI executables, otherwise +the resulting binaries will be console applications (regardless of the value +of ``CMAKE_WIN32_EXECUTABLE`` or ``CMAKE_MACOSX_BUNDLE``). Be aware that this changes +the executable entry point on Windows (although some frameworks, such as Qt, +abstract this difference away). + +The tests will be build with ``-DQT_FORCE_ASSERTS`` to enable assertions in the +test executable even for release builds. + +The ``TARGET_NAMES_VAR`` and ``TEST_NAMES_VAR`` arguments, if given, should specify a +variable name to receive the list of generated target and test names, +respectively. This makes it convenient to apply properties to them as a +whole, for example, using ``set_target_properties()`` or ``set_tests_properties()``. + +The generated target executables will have the effects of ``ecm_mark_as_test()`` +(from the :module:`ECMMarkAsTest` module) applied to it. + +``WORKING_DIRECTORY`` sets the test property `WORKING_DIRECTORY +`_ +in which to execute the test. By default the test will be run in +``${CMAKE_CURRENT_BINARY_DIR}``. The working directory can be specified using +generator expressions. Since 5.111. + +:: + + ecm_add_test( + + LINK_LIBRARIES [ [...]] + [TEST_NAME ] + [NAME_PREFIX ] + [GUI] + [WORKING_DIRECTORY ] # Since 5.111 + ) + +This is a single-test form of ``ecm_add_tests`` that allows multiple source files +to be used for a single test. If using multiple source files, ``TEST_NAME`` must +be given; this will be used for both the target and test names (and, as with +``ecm_add_tests()``, the ``NAME_PREFIX`` argument will be prepended to the test name). + +``WORKING_DIRECTORY`` sets the test property `WORKING_DIRECTORY +`_ +in which to execute the test. By default the test will be run in +``${CMAKE_CURRENT_BINARY_DIR}``. The working directory can be specified using +generator expressions. Since 5.111. + +Since pre-1.0.0. +#]=======================================================================] + +include(ECMMarkAsTest) +include(ECMMarkNonGuiExecutable) + +function(ecm_add_test) + set(options GUI) + # TARGET_NAME_VAR and TEST_NAME_VAR are undocumented args used by + # ecm_add_tests + set(oneValueArgs TEST_NAME NAME_PREFIX TARGET_NAME_VAR TEST_NAME_VAR WORKING_DIRECTORY) + set(multiValueArgs LINK_LIBRARIES) + cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(_sources ${ARG_UNPARSED_ARGUMENTS}) + list(LENGTH _sources _sourceCount) + if(ARG_TEST_NAME) + set(_targetname ${ARG_TEST_NAME}) + elseif(${_sourceCount} EQUAL "1") + #use the source file name without extension as the testname + get_filename_component(_targetname ${_sources} NAME_WE) + else() + #more than one source file passed, but no test name given -> error + message(FATAL_ERROR "ecm_add_test() called with multiple source files but without setting \"TEST_NAME\"") + endif() + + set(_testname ${ARG_NAME_PREFIX}${_targetname}) + set(gui_args) + if(ARG_GUI) + set(gui_args WIN32 MACOSX_BUNDLE) + endif() + add_executable(${_targetname} ${gui_args} ${_sources}) + if(NOT ARG_GUI) + ecm_mark_nongui_executable(${_targetname}) + endif() + set(test_args) + if(DEFINED ARG_WORKING_DIRECTORY) + list(APPEND test_args WORKING_DIRECTORY ${ARG_WORKING_DIRECTORY}) + endif() + add_test(NAME ${_testname} COMMAND ${_targetname} ${test_args}) + target_link_libraries(${_targetname} ${ARG_LINK_LIBRARIES}) + target_compile_definitions(${_targetname} PRIVATE -DQT_FORCE_ASSERTS) + ecm_mark_as_test(${_targetname}) + if (CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set(_plugin_path ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + if (DEFINED ENV{QT_PLUGIN_PATH}) + if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + # https://stackoverflow.com/questions/59862894/how-do-i-make-a-list-in-cmake-with-the-semicolon-value + set(PATHSEP "\\\;") # Don't want cmake to treat it like a list + else() # e.g. Linux + set(PATHSEP ":") + endif() + set(_plugin_path "${_plugin_path}${PATHSEP}$ENV{QT_PLUGIN_PATH}") + endif() + set_property(TEST ${_testname} PROPERTY ENVIRONMENT "QT_PLUGIN_PATH=${_plugin_path}") + endif() + if (ARG_TARGET_NAME_VAR) + set(${ARG_TARGET_NAME_VAR} "${_targetname}" PARENT_SCOPE) + endif() + if (ARG_TEST_NAME_VAR) + set(${ARG_TEST_NAME_VAR} "${_testname}" PARENT_SCOPE) + endif() +endfunction() + +function(ecm_add_tests) + set(options GUI) + set(oneValueArgs NAME_PREFIX TARGET_NAMES_VAR TEST_NAMES_VAR WORKING_DIRECTORY) + set(multiValueArgs LINK_LIBRARIES) + cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if(ARG_GUI) + set(_exe_type GUI) + else() + set(_exe_type "") + endif() + set(test_args) + if(DEFINED ARG_WORKING_DIRECTORY) + list(APPEND test_args WORKING_DIRECTORY ${ARG_WORKING_DIRECTORY}) + endif() + set(test_names) + set(target_names) + foreach(_test_source ${ARG_UNPARSED_ARGUMENTS}) + ecm_add_test(${_test_source} + NAME_PREFIX ${ARG_NAME_PREFIX} + LINK_LIBRARIES ${ARG_LINK_LIBRARIES} + TARGET_NAME_VAR target_name + TEST_NAME_VAR test_name + ${_exe_type} + ${test_args} + ) + list(APPEND _test_names "${test_name}") + list(APPEND _target_names "${target_name}") + endforeach() + if (ARG_TARGET_NAMES_VAR) + set(${ARG_TARGET_NAMES_VAR} "${_target_names}" PARENT_SCOPE) + endif() + if (ARG_TEST_NAMES_VAR) + set(${ARG_TEST_NAMES_VAR} "${_test_names}" PARENT_SCOPE) + endif() +endfunction() diff --git a/cmake/Modules/ECMGeneratePriFile.cmake b/cmake/Modules/ECMGeneratePriFile.cmake index a64fa62f..8910cd9e 100644 --- a/cmake/Modules/ECMGeneratePriFile.cmake +++ b/cmake/Modules/ECMGeneratePriFile.cmake @@ -1,125 +1,124 @@ -#.rst: -# ECMGeneratePriFile -# ------------------ +# SPDX-FileCopyrightText: 2014 David Faure # -# Generate a ``.pri`` file for the benefit of qmake-based projects. -# -# As well as the function below, this module creates the cache variable -# ``ECM_MKSPECS_INSTALL_DIR`` and sets the default value to ``mkspecs/modules``. -# This assumes Qt and the current project are both installed to the same -# non-system prefix. Packagers who use ``-DCMAKE_INSTALL_PREFIX=/usr`` will -# certainly want to set ``ECM_MKSPECS_INSTALL_DIR`` to something like -# ``share/qt5/mkspecs/modules``. -# -# The main thing is that this should be the ``modules`` subdirectory of either -# the default qmake ``mkspecs`` directory or of a directory that will be in the -# ``$QMAKEPATH`` environment variable when qmake is run. -# -# :: -# -# ecm_generate_pri_file(BASE_NAME -# LIB_NAME -# [DEPS " [ [...]]"] -# [FILENAME_VAR ] -# [INCLUDE_INSTALL_DIR ] -# [LIB_INSTALL_DIR ]) -# -# If your CMake project produces a Qt-based library, you may expect there to be -# applications that wish to use it that use a qmake-based build system, rather -# than a CMake-based one. Creating a ``.pri`` file will make use of your -# library convenient for them, in much the same way that CMake config files make -# things convenient for CMake-based applications. -# -# ecm_generate_pri_file() generates just such a file. It requires the -# ``PROJECT_VERSION_STRING`` variable to be set. This is typically set by -# :module:`ECMSetupVersion`, although the project() command in CMake 3.0.0 and -# later can also set this. -# -# BASE_NAME specifies the name qmake project (.pro) files should use to refer to -# the library (eg: KArchive). LIB_NAME is the name of the actual library to -# link to (ie: the first argument to add_library()). DEPS is a space-separated -# list of the base names of other libraries (for Qt libraries, use the same -# names you use with the ``QT`` variable in a qmake project file, such as "core" -# for QtCore). FILENAME_VAR specifies the name of a variable to store the path -# to the generated file in. -# -# INCLUDE_INSTALL_DIR is the path (relative to ``CMAKE_INSTALL_PREFIX``) that -# include files will be installed to. It defaults to -# ``${INCLUDE_INSTALL_DIR}/`` if the ``INCLUDE_INSTALL_DIR`` variable -# is set. If that variable is not set, the ``CMAKE_INSTALL_INCLUDEDIR`` variable -# is used instead, and if neither are set ``include`` is used. LIB_INSTALL_DIR -# operates similarly for the installation location for libraries; it defaults to -# ``${LIB_INSTALL_DIR}``, ``${CMAKE_INSTALL_LIBDIR}`` or ``lib``, in that order. -# -# Example usage: -# -# .. code-block:: cmake -# -# ecm_generate_pri_file( -# BASE_NAME KArchive -# LIB_NAME KF5KArchive -# DEPS "core" -# FILENAME_VAR pri_filename -# ) -# install(FILES ${pri_filename} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) -# -# A qmake-based project that wished to use this would then do:: -# -# QT += KArchive -# -# in their ``.pro`` file. -# -# Since pre-1.0.0. +# SPDX-License-Identifier: BSD-3-Clause -#============================================================================= -# Copyright 2014 David Faure -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#[=======================================================================[.rst: +ECMGeneratePriFile +------------------ + +Generate a ``.pri`` file for the benefit of qmake-based projects. + +As well as the function below, this module creates the cache variable +``ECM_MKSPECS_INSTALL_DIR`` and sets the default value to ``mkspecs/modules``. +This assumes Qt and the current project are both installed to the same +non-system prefix. Packagers who use ``-DCMAKE_INSTALL_PREFIX=/usr`` will +certainly want to set ``ECM_MKSPECS_INSTALL_DIR`` to something like +``share/qt5/mkspecs/modules``. + +The main thing is that this should be the ``modules`` subdirectory of either +the default qmake ``mkspecs`` directory or of a directory that will be in the +``$QMAKEPATH`` environment variable when qmake is run. + +:: + + ecm_generate_pri_file(BASE_NAME + LIB_NAME + [VERSION ] # since 5.83 + [DEPS " [ [...]]"] + [FILENAME_VAR ] + [INCLUDE_INSTALL_DIRS [ [...]]] # since 5.92 + [INCLUDE_INSTALL_DIR ] # deprecated since 5.92 + [LIB_INSTALL_DIR ]) + +If your CMake project produces a Qt-based library, you may expect there to be +applications that wish to use it that use a qmake-based build system, rather +than a CMake-based one. Creating a ``.pri`` file will make use of your +library convenient for them, in much the same way that CMake config files make +things convenient for CMake-based applications. ``ecm_generate_pri_file()`` +generates just such a file. + +``VERSION`` specifies the version of the library the ``.pri`` file describes. If +not set, the value is taken from the context variable ``PROJECT_VERSION``. +This variable is usually set by the ``project(... VERSION ...)`` command or, +if CMake policy CMP0048 is not ``NEW``, by :module:`ECMSetupVersion`. +For backward-compatibility with older ECM versions the +``PROJECT_VERSION_STRING`` variable as set by :module:`ECMSetupVersion` +will be preferred over ``PROJECT_VERSION`` if set, unless the minimum +required version of ECM is 5.83 and newer. Since 5.83. + +``BASE_NAME`` specifies the name qmake project (.pro) files should use to refer to +the library (eg: KArchive). ``LIB_NAME`` is the name of the actual library to +link to (ie: the first argument to add_library()). ``DEPS`` is a space-separated +list of the base names of other libraries (for Qt libraries, use the same +names you use with the ``QT`` variable in a qmake project file, such as "core" +for QtCore). ``FILENAME_VAR`` specifies the name of a variable to store the path +to the generated file in. + +``INCLUDE_INSTALL_DIRS`` are the paths (relative to ``CMAKE_INSTALL_PREFIX``) that +include files will be installed to. It defaults to +``${INCLUDE_INSTALL_DIR}/`` if the ``INCLUDE_INSTALL_DIR`` variable +is set. If that variable is not set, the ``CMAKE_INSTALL_INCLUDEDIR`` variable +is used instead, and if neither are set ``include`` is used. ``LIB_INSTALL_DIR`` +operates similarly for the installation location for libraries; it defaults to +``${LIB_INSTALL_DIR}``, ``${CMAKE_INSTALL_LIBDIR}`` or ``lib``, in that order. + +``INCLUDE_INSTALL_DIR`` is the old variant of ``INCLUDE_INSTALL_DIRS``, taking only one +directory. + +Example usage: + +.. code-block:: cmake + + ecm_generate_pri_file( + BASE_NAME KArchive + LIB_NAME KF5KArchive + DEPS "core" + FILENAME_VAR pri_filename + VERSION 4.2.0 + ) + install(FILES ${pri_filename} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) + +A qmake-based project that wished to use this would then do:: + + QT += KArchive + +in their ``.pro`` file. + +Since pre-1.0.0. +#]=======================================================================] # Replicate the logic from KDEInstallDirs.cmake as we can't depend on it # Ask qmake if we're using the same prefix as Qt -set(_askqmake OFF) +set(_should_query_qt OFF) if(NOT DEFINED KDE_INSTALL_USE_QT_SYS_PATHS) - include(ECMQueryQmake) - query_qmake(qt_install_prefix_dir QT_INSTALL_PREFIX) + include(ECMQueryQt) + ecm_query_qt(qt_install_prefix_dir QT_INSTALL_PREFIX TRY) if(qt_install_prefix_dir STREQUAL "${CMAKE_INSTALL_PREFIX}") - set(_askqmake ON) + set(_should_query_qt ON) endif() endif() -if(KDE_INSTALL_USE_QT_SYS_PATHS OR _askqmake) - include(ECMQueryQmake) - query_qmake(qt_host_data_dir QT_HOST_DATA) - set(ECM_MKSPECS_INSTALL_DIR ${qt_host_data_dir}/mkspecs/modules CACHE PATH "The directory where mkspecs will be installed to.") +if(KDE_INSTALL_USE_QT_SYS_PATHS OR _should_query_qt) + include(ECMQueryQt) + ecm_query_qt(qt_install_prefix_dir QT_INSTALL_PREFIX) + ecm_query_qt(qt_host_data_dir QT_HOST_DATA) + if(qt_install_prefix_dir STREQUAL "${CMAKE_INSTALL_PREFIX}") + file(RELATIVE_PATH qt_host_data_dir ${qt_install_prefix_dir} ${qt_host_data_dir}) + endif() + if(qt_host_data_dir STREQUAL "") + set(mkspecs_install_dir mkspecs/modules) + else() + set(mkspecs_install_dir ${qt_host_data_dir}/mkspecs/modules) + endif() + set(ECM_MKSPECS_INSTALL_DIR ${mkspecs_install_dir} CACHE PATH "The directory where mkspecs will be installed to.") else() set(ECM_MKSPECS_INSTALL_DIR mkspecs/modules CACHE PATH "The directory where mkspecs will be installed to.") endif() function(ECM_GENERATE_PRI_FILE) set(options ) - set(oneValueArgs BASE_NAME LIB_NAME DEPS FILENAME_VAR INCLUDE_INSTALL_DIR LIB_INSTALL_DIR) - set(multiValueArgs ) + set(oneValueArgs BASE_NAME LIB_NAME DEPS FILENAME_VAR INCLUDE_INSTALL_DIR LIB_INSTALL_DIR VERSION) + set(multiValueArgs INCLUDE_INSTALL_DIRS) cmake_parse_arguments(EGPF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -127,22 +126,42 @@ function(ECM_GENERATE_PRI_FILE) message(FATAL_ERROR "Unknown keywords given to ECM_GENERATE_PRI_FILE(): \"${EGPF_UNPARSED_ARGUMENTS}\"") endif() + if(ECM_GLOBAL_FIND_VERSION VERSION_LESS 5.83.0) + set(_support_backward_compat_version_string_var TRUE) + else() + set(_support_backward_compat_version_string_var FALSE) + endif() + if(NOT EGPF_BASE_NAME) message(FATAL_ERROR "Required argument BASE_NAME missing in ECM_GENERATE_PRI_FILE() call") endif() if(NOT EGPF_LIB_NAME) message(FATAL_ERROR "Required argument LIB_NAME missing in ECM_GENERATE_PRI_FILE() call") endif() - if(NOT PROJECT_VERSION_STRING) - message(FATAL_ERROR "Required variable PROJECT_VERSION_STRING not set before ECM_GENERATE_PRI_FILE() call. Did you call ecm_setup_version?") + if(NOT EGPF_VERSION) + if(_support_backward_compat_version_string_var) + if(NOT PROJECT_VERSION_STRING AND NOT PROJECT_VERSION) + message(FATAL_ERROR "Required variable PROJECT_VERSION_STRING or PROJECT_VERSION not set before ECM_GENERATE_PRI_FILE() call. Missing call of ecm_setup_version() or project(VERSION)?") + endif() + else() + if(NOT PROJECT_VERSION) + message(FATAL_ERROR "Required variable PROJECT_VERSION not set before ECM_GENERATE_PRI_FILE() call. Missing call of ecm_setup_version() or project(VERSION)?") + endif() + endif() + endif() + if(EGPF_INCLUDE_INSTALL_DIR) + if(EGPF_INCLUDE_INSTALL_DIRS) + message(FATAL_ERROR "Only one argument of INCLUDE_INSTALL_DIR & INCLUDE_INSTALL_DIRS can be used in ECM_GENERATE_PRI_FILE() call") + endif() + set(EGPF_INCLUDE_INSTALL_DIRS ${EGPF_INCLUDE_INSTALL_DIR}) endif() - if(NOT EGPF_INCLUDE_INSTALL_DIR) + if(NOT EGPF_INCLUDE_INSTALL_DIRS) if(INCLUDE_INSTALL_DIR) - set(EGPF_INCLUDE_INSTALL_DIR "${INCLUDE_INSTALL_DIR}/${EGPF_BASE_NAME}") + set(EGPF_INCLUDE_INSTALL_DIRS "${INCLUDE_INSTALL_DIR}/${EGPF_BASE_NAME}") elseif(CMAKE_INSTALL_INCLUDEDIR) - set(EGPF_INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_INCLUDEDIR}/${EGPF_BASE_NAME}") + set(EGPF_INCLUDE_INSTALL_DIRS "${CMAKE_INSTALL_INCLUDEDIR}/${EGPF_BASE_NAME}") else() - set(EGPF_INCLUDE_INSTALL_DIR "include/${EGPF_BASE_NAME}") + set(EGPF_INCLUDE_INSTALL_DIRS "include/${EGPF_BASE_NAME}") endif() endif() if(NOT EGPF_LIB_INSTALL_DIR) @@ -155,22 +174,48 @@ function(ECM_GENERATE_PRI_FILE) endif() endif() - string(REGEX REPLACE "^([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" PROJECT_VERSION_MAJOR "${PROJECT_VERSION_STRING}") - string(REGEX REPLACE "^[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" PROJECT_VERSION_MINOR "${PROJECT_VERSION_STRING}") - string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" PROJECT_VERSION_PATCH "${PROJECT_VERSION_STRING}") + if(EGPF_VERSION) + set(PRI_VERSION "${EGPF_VERSION}") + else() + if(_support_backward_compat_version_string_var AND PROJECT_VERSION_STRING) + set(PRI_VERSION "${PROJECT_VERSION_STRING}") + if(NOT PROJECT_VERSION_STRING STREQUAL PROJECT_VERSION) + message(DEPRECATION "ECM_GENERATE_PRI_FILE() will no longer support PROJECT_VERSION_STRING when the required minimum version of ECM is 5.83 or newer. Set VERSION parameter or use PROJECT_VERSION instead.") + endif() + else() + set(PRI_VERSION "${PROJECT_VERSION}") + endif() + endif() + + string(REGEX REPLACE "^([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" PRI_VERSION_MAJOR "${PRI_VERSION}") + string(REGEX REPLACE "^[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" PRI_VERSION_MINOR "${PRI_VERSION}") + string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" PRI_VERSION_PATCH "${PRI_VERSION}") + + # Prepare the right number of "../.." to go from ECM_MKSPECS_INSTALL_DIR to the install prefix + # This allows to make the generated pri files relocatable (no absolute paths) + if (IS_ABSOLUTE ${ECM_MKSPECS_INSTALL_DIR}) + set(BASEPATH ${CMAKE_INSTALL_PREFIX}) + else() + string(REGEX REPLACE "[^/]+" ".." PRI_ROOT_RELATIVE_TO_MKSPECS ${ECM_MKSPECS_INSTALL_DIR}) + set(BASEPATH "$$PWD/${PRI_ROOT_RELATIVE_TO_MKSPECS}") + endif() set(PRI_TARGET_BASENAME ${EGPF_BASE_NAME}) set(PRI_TARGET_LIBNAME ${EGPF_LIB_NAME}) set(PRI_TARGET_QTDEPS ${EGPF_DEPS}) - if(IS_ABSOLUTE "${EGPF_INCLUDE_INSTALL_DIR}") - set(PRI_TARGET_INCLUDES "${EGPF_INCLUDE_INSTALL_DIR}") - else() - set(PRI_TARGET_INCLUDES "${CMAKE_INSTALL_PREFIX}/${EGPF_INCLUDE_INSTALL_DIR}") - endif() + set(PRI_TARGET_INCLUDES) + foreach(_dir ${EGPF_INCLUDE_INSTALL_DIRS}) + # separate list entries with space + if(IS_ABSOLUTE "${_dir}") + string(APPEND PRI_TARGET_INCLUDES " ${_dir}") + else() + string(APPEND PRI_TARGET_INCLUDES " ${BASEPATH}/${_dir}") + endif() + endforeach() if(IS_ABSOLUTE "${EGPF_LIB_INSTALL_DIR}") set(PRI_TARGET_LIBS "${EGPF_LIB_INSTALL_DIR}") else() - set(PRI_TARGET_LIBS "${CMAKE_INSTALL_PREFIX}/${EGPF_LIB_INSTALL_DIR}") + set(PRI_TARGET_LIBS "${BASEPATH}/${EGPF_LIB_INSTALL_DIR}") endif() set(PRI_TARGET_DEFINES "") @@ -179,19 +224,33 @@ function(ECM_GENERATE_PRI_FILE) set(${EGPF_FILENAME_VAR} ${PRI_FILENAME} PARENT_SCOPE) endif() + set(PRI_TARGET_MODULE_CONFIG "") + # backward compat: it was not obvious LIB_NAME needs to be a target name, + # and some projects where the target name was not the actual library output name + # passed the output name for LIB_NAME, so .name & .module prperties are correctly set. + # TODO: improve API dox, allow control over module name if target name != output name + if(TARGET ${EGPF_LIB_NAME}) + get_target_property(target_type ${EGPF_LIB_NAME} TYPE) + if (target_type STREQUAL "STATIC_LIBRARY") + set(PRI_TARGET_MODULE_CONFIG "staticlib") + endif() + endif() + file(GENERATE OUTPUT ${PRI_FILENAME} CONTENT - "QT.${PRI_TARGET_BASENAME}.VERSION = ${PROJECT_VERSION_STRING} -QT.${PRI_TARGET_BASENAME}.MAJOR_VERSION = ${PROJECT_VERSION_MAJOR} -QT.${PRI_TARGET_BASENAME}.MINOR_VERSION = ${PROJECT_VERSION_MINOR} -QT.${PRI_TARGET_BASENAME}.PATCH_VERSION = ${PROJECT_VERSION_PATCH} + "QT.${PRI_TARGET_BASENAME}.VERSION = ${PRI_VERSION} +QT.${PRI_TARGET_BASENAME}.MAJOR_VERSION = ${PRI_VERSION_MAJOR} +QT.${PRI_TARGET_BASENAME}.MINOR_VERSION = ${PRI_VERSION_MINOR} +QT.${PRI_TARGET_BASENAME}.PATCH_VERSION = ${PRI_VERSION_PATCH} QT.${PRI_TARGET_BASENAME}.name = ${PRI_TARGET_LIBNAME} +QT.${PRI_TARGET_BASENAME}.module = ${PRI_TARGET_LIBNAME} QT.${PRI_TARGET_BASENAME}.defines = ${PRI_TARGET_DEFINES} QT.${PRI_TARGET_BASENAME}.includes = ${PRI_TARGET_INCLUDES} QT.${PRI_TARGET_BASENAME}.private_includes = QT.${PRI_TARGET_BASENAME}.libs = ${PRI_TARGET_LIBS} QT.${PRI_TARGET_BASENAME}.depends = ${PRI_TARGET_QTDEPS} +QT.${PRI_TARGET_BASENAME}.module_config = ${PRI_TARGET_MODULE_CONFIG} " ) endfunction() diff --git a/cmake/Modules/ECMMarkAsTest.cmake b/cmake/Modules/ECMMarkAsTest.cmake new file mode 100644 index 00000000..6a5373d5 --- /dev/null +++ b/cmake/Modules/ECMMarkAsTest.cmake @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2012 Stephen Kelly +# SPDX-FileCopyrightText: 2012 Alex Neundorf +# +# SPDX-License-Identifier: BSD-3-Clause + +#[=======================================================================[.rst: +ECMMarkAsTest +------------- + +Marks a target as only being required for tests. + +:: + + ecm_mark_as_test( [ [...]]) + +This will cause the specified targets to not be built unless either +``BUILD_TESTING`` is set to ``ON`` or the user invokes the ``buildtests`` target. + +``BUILD_TESTING`` is created as a cache variable by the CTest module and by the +:kde-module:`KDECMakeSettings` module. + +Since pre-1.0.0. +#]=======================================================================] + +if (NOT BUILD_TESTING) + if(NOT TARGET buildtests) + add_custom_target(buildtests) + endif() +endif() + +function(ecm_mark_as_test) + if (NOT BUILD_TESTING) + foreach(_target ${ARGN}) + set_target_properties(${_target} + PROPERTIES + EXCLUDE_FROM_ALL TRUE + ) + add_dependencies(buildtests ${_target}) + endforeach() + endif() +endfunction() diff --git a/cmake/Modules/ECMMarkNonGuiExecutable.cmake b/cmake/Modules/ECMMarkNonGuiExecutable.cmake new file mode 100644 index 00000000..48b4feaa --- /dev/null +++ b/cmake/Modules/ECMMarkNonGuiExecutable.cmake @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2012 Stephen Kelly +# +# SPDX-License-Identifier: BSD-3-Clause + +#[=======================================================================[.rst: +ECMMarkNonGuiExecutable +----------------------- + +Marks an executable target as not being a GUI application. + +:: + + ecm_mark_nongui_executable( [ [...]]) + +This will indicate to CMake that the specified targets should not be included +in a MACOSX_BUNDLE and should not be WIN32_EXECUTABLEs. On platforms other +than MacOS X or Windows, this will have no effect. + +Since pre-1.0.0. +#]=======================================================================] + +function(ecm_mark_nongui_executable) + foreach(_target ${ARGN}) + set_target_properties(${_target} + PROPERTIES + WIN32_EXECUTABLE FALSE + MACOSX_BUNDLE FALSE + ) + endforeach() +endfunction() diff --git a/cmake/Modules/ECMPackageConfigHelpers.cmake b/cmake/Modules/ECMPackageConfigHelpers.cmake index a118feae..8d48772b 100644 --- a/cmake/Modules/ECMPackageConfigHelpers.cmake +++ b/cmake/Modules/ECMPackageConfigHelpers.cmake @@ -6,7 +6,7 @@ # # ``write_basic_package_version_file()`` is the same as the one provided by the # `CMakePackageConfigHelpers -# `_ +# `_ # module in CMake; see that module's documentation for # more information. # @@ -23,7 +23,7 @@ # 2.8.12, except that it adds an extra helper macro: find_dependency(). It is # highly recommended that you read the `documentation for # CMakePackageConfigHelpers -# `_ +# `_ # for more information, particularly with regard to the PATH_VARS argument. # # Note that there is no argument that will disable the find_dependency() macro; @@ -50,31 +50,10 @@ # Since pre-1.0.0. #============================================================================= -# Copyright 2014 Alex Merry -# Copyright 2013 Stephen Kelly +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2013 Stephen Kelly # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-License-Identifier: BSD-3-Clause include(${CMAKE_ROOT}/Modules/CMakePackageConfigHelpers.cmake) diff --git a/cmake/Modules/ECMQueryQmake.cmake b/cmake/Modules/ECMQueryQmake.cmake deleted file mode 100644 index fa0949df..00000000 --- a/cmake/Modules/ECMQueryQmake.cmake +++ /dev/null @@ -1,32 +0,0 @@ -find_package(Qt5Core QUIET) - -if (Qt5Core_FOUND) - set(_qmake_executable_default "qmake-qt5") -endif () -if (TARGET Qt5::qmake) - get_target_property(_qmake_executable_default Qt5::qmake LOCATION) -endif() -set(QMAKE_EXECUTABLE ${_qmake_executable_default} - CACHE FILEPATH "Location of the Qt5 qmake executable") - -# This is not public API (yet)! -function(query_qmake result_variable qt_variable) - if(NOT QMAKE_EXECUTABLE) - set(${result_variable} "" PARENT_SCOPE) - message(WARNING "Should specify a qmake Qt5 binary. Can't check ${qt_variable}") - return() - endif() - execute_process( - COMMAND ${QMAKE_EXECUTABLE} -query "${qt_variable}" - RESULT_VARIABLE return_code - OUTPUT_VARIABLE output - ) - if(return_code EQUAL 0) - string(STRIP "${output}" output) - file(TO_CMAKE_PATH "${output}" output_path) - set(${result_variable} "${output_path}" PARENT_SCOPE) - else() - message(WARNING "Failed call: ${QMAKE_EXECUTABLE} -query \"${qt_variable}\"") - message(FATAL_ERROR "QMake call failed: ${return_code}") - endif() -endfunction() diff --git a/cmake/Modules/ECMQueryQt.cmake b/cmake/Modules/ECMQueryQt.cmake new file mode 100644 index 00000000..98eb5008 --- /dev/null +++ b/cmake/Modules/ECMQueryQt.cmake @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2014 Rohan Garg +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2014-2016 Aleix Pol +# SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau +# SPDX-FileCopyrightText: 2022 Ahmad Samir +# +# SPDX-License-Identifier: BSD-3-Clause +#[=======================================================================[.rst: +ECMQueryQt +--------------- +This module can be used to query the installation paths used by Qt. + +For Qt5 this uses ``qmake``, and for Qt6 this used ``qtpaths`` (the latter has built-in +support to query the paths of a target platform when cross-compiling). + +This module defines the following function: +:: + + ecm_query_qt( [TRY]) + +Passing ``TRY`` will result in the method not making the build fail if the executable +used for querying has not been found, but instead simply print a warning message and +return an empty string. + +Example usage: + +.. code-block:: cmake + + include(ECMQueryQt) + ecm_query_qt(bin_dir QT_INSTALL_BINS) + +If the call succeeds ``${bin_dir}`` will be set to ``/path/to/bin/dir`` (e.g. +``/usr/lib64/qt/bin/``). + +Since: 5.93 +#]=======================================================================] + +include(${CMAKE_CURRENT_LIST_DIR}/QtVersionOption.cmake) +include(CheckLanguage) +check_language(CXX) +if (CMAKE_CXX_COMPILER) + # Enable the CXX language to let CMake look for config files in library dirs. + # See: https://gitlab.kitware.com/cmake/cmake/-/issues/23266 + enable_language(CXX) +endif() + +if (QT_MAJOR_VERSION STREQUAL "5") + # QUIET to accommodate the TRY option + find_package(Qt${QT_MAJOR_VERSION}Core QUIET) + if(TARGET Qt5::qmake) + get_target_property(_qmake_executable_default Qt5::qmake LOCATION) + + set(QUERY_EXECUTABLE ${_qmake_executable_default} + CACHE FILEPATH "Location of the Qt5 qmake executable") + set(_exec_name_text "Qt5 qmake") + set(_cli_option "-query") + endif() +elseif(QT_MAJOR_VERSION STREQUAL "6") + # QUIET to accommodate the TRY option + find_package(Qt6 COMPONENTS CoreTools QUIET CONFIG) + if (TARGET Qt6::qtpaths) + get_target_property(_qtpaths_executable Qt6::qtpaths LOCATION) + + set(QUERY_EXECUTABLE ${_qtpaths_executable} + CACHE FILEPATH "Location of the Qt6 qtpaths executable") + set(_exec_name_text "Qt6 qtpaths") + set(_cli_option "--query") + endif() +endif() + +function(ecm_query_qt result_variable qt_variable) + set(options TRY) + set(oneValueArgs) + set(multiValueArgs) + + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT QUERY_EXECUTABLE) + if(ARGS_TRY) + set(${result_variable} "" PARENT_SCOPE) + message(STATUS "No ${_exec_name_text} executable found. Can't check ${qt_variable}") + return() + else() + message(FATAL_ERROR "No ${_exec_name_text} executable found. Can't check ${qt_variable} as required") + endif() + endif() + execute_process( + COMMAND ${QUERY_EXECUTABLE} ${_cli_option} "${qt_variable}" + RESULT_VARIABLE return_code + OUTPUT_VARIABLE output + ) + if(return_code EQUAL 0) + string(STRIP "${output}" output) + file(TO_CMAKE_PATH "${output}" output_path) + set(${result_variable} "${output_path}" PARENT_SCOPE) + else() + message(WARNING "Failed call: ${_command} \"${qt_variable}\"") + message(FATAL_ERROR "${_exec_name_text} call failed: ${return_code}") + endif() +endfunction() diff --git a/cmake/Modules/ECMSetupVersion.cmake b/cmake/Modules/ECMSetupVersion.cmake index f5a3cce4..65c1688a 100644 --- a/cmake/Modules/ECMSetupVersion.cmake +++ b/cmake/Modules/ECMSetupVersion.cmake @@ -28,7 +28,7 @@ # _SOVERSION - , or if SOVERSION was not given # # If CMake policy CMP0048 is not NEW, the following CMake variables will also -# be set: +# be set:: # # PROJECT_VERSION_MAJOR - # PROJECT_VERSION_MINOR - @@ -75,34 +75,13 @@ # # Since pre-1.0.0. # -# COMPATIBLITY option available since 1.6.0. +# COMPATIBILITY option available since 1.6.0. #============================================================================= -# Copyright 2014 Alex Merry -# Copyright 2012 Alexander Neundorf -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2012 Alexander Neundorf +# +# SPDX-License-Identifier: BSD-3-Clause include(CMakePackageConfigHelpers) @@ -154,9 +133,9 @@ function(ecm_setup_version _version) set(_minor "${PROJECT_VERSION_MINOR}") set(_patch "${PROJECT_VERSION_PATCH}") else() - string(REGEX REPLACE "^([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" _major "${_version}") - string(REGEX REPLACE "^[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" _minor "${_version}") - string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" _patch "${_version}") + string(REGEX REPLACE "^0*([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" _major "${_version}") + string(REGEX REPLACE "^[0-9]+\\.0*([0-9]+)\\.[0-9]+.*" "\\1" _minor "${_version}") + string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.0*([0-9]+).*" "\\1" _patch "${_version}") endif() if(NOT ESV_SOVERSION) diff --git a/cmake/Modules/GNUInstallDirs.cmake b/cmake/Modules/GNUInstallDirs.cmake deleted file mode 100644 index 0302e4be..00000000 --- a/cmake/Modules/GNUInstallDirs.cmake +++ /dev/null @@ -1,188 +0,0 @@ -# - Define GNU standard installation directories -# Provides install directory variables as defined for GNU software: -# http://www.gnu.org/prep/standards/html_node/Directory-Variables.html -# Inclusion of this module defines the following variables: -# CMAKE_INSTALL_ - destination for files of a given type -# CMAKE_INSTALL_FULL_ - corresponding absolute path -# where is one of: -# BINDIR - user executables (bin) -# SBINDIR - system admin executables (sbin) -# LIBEXECDIR - program executables (libexec) -# SYSCONFDIR - read-only single-machine data (etc) -# SHAREDSTATEDIR - modifiable architecture-independent data (com) -# LOCALSTATEDIR - modifiable single-machine data (var) -# LIBDIR - object code libraries (lib or lib64 or lib/ on Debian) -# INCLUDEDIR - C header files (include) -# OLDINCLUDEDIR - C header files for non-gcc (/usr/include) -# DATAROOTDIR - read-only architecture-independent data root (share) -# DATADIR - read-only architecture-independent data (DATAROOTDIR) -# INFODIR - info documentation (DATAROOTDIR/info) -# LOCALEDIR - locale-dependent data (DATAROOTDIR/locale) -# MANDIR - man documentation (DATAROOTDIR/man) -# DOCDIR - documentation root (DATAROOTDIR/doc/PROJECT_NAME) -# Each CMAKE_INSTALL_ value may be passed to the DESTINATION options of -# install() commands for the corresponding file type. If the includer does -# not define a value the above-shown default will be used and the value will -# appear in the cache for editing by the user. -# Each CMAKE_INSTALL_FULL_ value contains an absolute path constructed -# from the corresponding destination by prepending (if necessary) the value -# of CMAKE_INSTALL_PREFIX. - -#============================================================================= -# Copyright 2011 Nikita Krupen'ko -# Copyright 2011 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -# Installation directories -# -if(NOT DEFINED CMAKE_INSTALL_BINDIR) - set(CMAKE_INSTALL_BINDIR "bin" CACHE PATH "user executables (bin)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_SBINDIR) - set(CMAKE_INSTALL_SBINDIR "sbin" CACHE PATH "system admin executables (sbin)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_LIBEXECDIR) - set(CMAKE_INSTALL_LIBEXECDIR "libexec" CACHE PATH "program executables (libexec)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_SYSCONFDIR) - set(CMAKE_INSTALL_SYSCONFDIR "etc" CACHE PATH "read-only single-machine data (etc)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_SHAREDSTATEDIR) - set(CMAKE_INSTALL_SHAREDSTATEDIR "com" CACHE PATH "modifiable architecture-independent data (com)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_LOCALSTATEDIR) - set(CMAKE_INSTALL_LOCALSTATEDIR "var" CACHE PATH "modifiable single-machine data (var)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_LIBDIR) - set(_LIBDIR_DEFAULT "lib") - # Override this default 'lib' with 'lib64' iff: - # - we are on Linux system but NOT cross-compiling - # - we are NOT on debian - # - we are on a 64 bits system - # reason is: amd64 ABI: http://www.x86-64.org/documentation/abi.pdf - # For Debian with multiarch, use 'lib/${CMAKE_LIBRARY_ARCHITECTURE}' if - # CMAKE_LIBRARY_ARCHITECTURE is set (which contains e.g. "i386-linux-gnu" - # See http://wiki.debian.org/Multiarch - if(CMAKE_SYSTEM_NAME MATCHES "Linux" - AND NOT CMAKE_CROSSCOMPILING) - if (EXISTS "/etc/debian_version") # is this a debian system ? - if(CMAKE_LIBRARY_ARCHITECTURE) - set(_LIBDIR_DEFAULT "lib/${CMAKE_LIBRARY_ARCHITECTURE}") - endif() - else() # not debian, rely on CMAKE_SIZEOF_VOID_P: - if(NOT DEFINED CMAKE_SIZEOF_VOID_P) - message(AUTHOR_WARNING - "Unable to determine default CMAKE_INSTALL_LIBDIR directory because no target architecture is known. " - "Please enable at least one language before including GNUInstallDirs.") - else() - if("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") - set(_LIBDIR_DEFAULT "lib64") - endif() - endif() - endif() - endif() - set(CMAKE_INSTALL_LIBDIR "${_LIBDIR_DEFAULT}" CACHE PATH "object code libraries (${_LIBDIR_DEFAULT})") -endif() - -if(NOT DEFINED CMAKE_INSTALL_INCLUDEDIR) - set(CMAKE_INSTALL_INCLUDEDIR "include" CACHE PATH "C header files (include)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_OLDINCLUDEDIR) - set(CMAKE_INSTALL_OLDINCLUDEDIR "/usr/include" CACHE PATH "C header files for non-gcc (/usr/include)") -endif() - -if(NOT DEFINED CMAKE_INSTALL_DATAROOTDIR) - set(CMAKE_INSTALL_DATAROOTDIR "share" CACHE PATH "read-only architecture-independent data root (share)") -endif() - -#----------------------------------------------------------------------------- -# Values whose defaults are relative to DATAROOTDIR. Store empty values in -# the cache and store the defaults in local variables if the cache values are -# not set explicitly. This auto-updates the defaults as DATAROOTDIR changes. - -if(NOT CMAKE_INSTALL_DATADIR) - set(CMAKE_INSTALL_DATADIR "" CACHE PATH "read-only architecture-independent data (DATAROOTDIR)") - set(CMAKE_INSTALL_DATADIR "${CMAKE_INSTALL_DATAROOTDIR}") -endif() - -if(NOT CMAKE_INSTALL_INFODIR) - set(CMAKE_INSTALL_INFODIR "" CACHE PATH "info documentation (DATAROOTDIR/info)") - set(CMAKE_INSTALL_INFODIR "${CMAKE_INSTALL_DATAROOTDIR}/info") -endif() - -if(NOT CMAKE_INSTALL_LOCALEDIR) - set(CMAKE_INSTALL_LOCALEDIR "" CACHE PATH "locale-dependent data (DATAROOTDIR/locale)") - set(CMAKE_INSTALL_LOCALEDIR "${CMAKE_INSTALL_DATAROOTDIR}/locale") -endif() - -if(NOT CMAKE_INSTALL_MANDIR) - set(CMAKE_INSTALL_MANDIR "" CACHE PATH "man documentation (DATAROOTDIR/man)") - set(CMAKE_INSTALL_MANDIR "${CMAKE_INSTALL_DATAROOTDIR}/man") -endif() - -if(NOT CMAKE_INSTALL_DOCDIR) - set(CMAKE_INSTALL_DOCDIR "" CACHE PATH "documentation root (DATAROOTDIR/doc/PROJECT_NAME)") - set(CMAKE_INSTALL_DOCDIR "${CMAKE_INSTALL_DATAROOTDIR}/doc/${PROJECT_NAME}") -endif() - -#----------------------------------------------------------------------------- - -mark_as_advanced( - CMAKE_INSTALL_BINDIR - CMAKE_INSTALL_SBINDIR - CMAKE_INSTALL_LIBEXECDIR - CMAKE_INSTALL_SYSCONFDIR - CMAKE_INSTALL_SHAREDSTATEDIR - CMAKE_INSTALL_LOCALSTATEDIR - CMAKE_INSTALL_LIBDIR - CMAKE_INSTALL_INCLUDEDIR - CMAKE_INSTALL_OLDINCLUDEDIR - CMAKE_INSTALL_DATAROOTDIR - CMAKE_INSTALL_DATADIR - CMAKE_INSTALL_INFODIR - CMAKE_INSTALL_LOCALEDIR - CMAKE_INSTALL_MANDIR - CMAKE_INSTALL_DOCDIR - ) - -# Result directories -# -foreach(dir - BINDIR - SBINDIR - LIBEXECDIR - SYSCONFDIR - SHAREDSTATEDIR - LOCALSTATEDIR - LIBDIR - INCLUDEDIR - OLDINCLUDEDIR - DATAROOTDIR - DATADIR - INFODIR - LOCALEDIR - MANDIR - DOCDIR - ) - if(NOT IS_ABSOLUTE ${CMAKE_INSTALL_${dir}}) - set(CMAKE_INSTALL_FULL_${dir} "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_${dir}}") - else() - set(CMAKE_INSTALL_FULL_${dir} "${CMAKE_INSTALL_${dir}}") - endif() -endforeach() diff --git a/cmake/Modules/QtVersionOption.cmake b/cmake/Modules/QtVersionOption.cmake new file mode 100644 index 00000000..ea37da22 --- /dev/null +++ b/cmake/Modules/QtVersionOption.cmake @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2021 Volker Krause +# +# SPDX-License-Identifier: BSD-3-Clause + +#[=======================================================================[.rst: +QtVersionOption +--------------- + +Adds a build option to select the major Qt version if necessary, +that is, if the major Qt version has not yet been determined otherwise +(e.g. by a corresponding ``find_package()`` call). +This module is typically included by other modules requiring knowledge +about the major Qt version. + +``QT_MAJOR_VERSION`` is defined to either be "5" or "6". + +Since 5.82.0. +#]=======================================================================] + +if (DEFINED QT_MAJOR_VERSION) + return() +endif() + +if (TARGET Qt5::Core) + set(QT_MAJOR_VERSION 5) +elseif (TARGET Qt6::Core) + set(QT_MAJOR_VERSION 6) +else() + option(BUILD_WITH_QT6 "Build against Qt 6" OFF) + + if (BUILD_WITH_QT6) + set(QT_MAJOR_VERSION 6) + else() + set(QT_MAJOR_VERSION 5) + endif() +endif() diff --git a/gnomekeyring.cpp b/gnomekeyring.cpp deleted file mode 100644 index dd35670c..00000000 --- a/gnomekeyring.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "gnomekeyring_p.h" - -const char* GnomeKeyring::GNOME_KEYRING_DEFAULT = NULL; - -bool GnomeKeyring::isAvailable() -{ - const GnomeKeyring& keyring = instance(); - return keyring.isLoaded() && - keyring.NETWORK_PASSWORD && - keyring.is_available && - keyring.find_password && - keyring.store_password && - keyring.delete_password && - keyring.is_available(); -} - -GnomeKeyring::gpointer GnomeKeyring::store_network_password( - const gchar* keyring, - const gchar* display_name, - const gchar* user, - const gchar* server, - const gchar* type, - const gchar* password, - OperationDoneCallback callback, - gpointer data, - GDestroyNotify destroy_data ) -{ - if ( !isAvailable() ) - return 0; - return instance().store_password( instance().NETWORK_PASSWORD, - keyring, display_name, password, callback, - data, destroy_data, - "user", user, - "server", server, - "type", type, - static_cast(0) ); -} - -GnomeKeyring::gpointer GnomeKeyring::find_network_password( - const gchar* user, const gchar* server, const gchar* type, - OperationGetStringCallback callback, gpointer data, GDestroyNotify destroy_data ) -{ - if ( !isAvailable() ) - return 0; - - return instance().find_password( instance().NETWORK_PASSWORD, - callback, data, destroy_data, - "user", user, "server", server, "type", type, - static_cast(0) ); -} - -GnomeKeyring::gpointer GnomeKeyring::delete_network_password( const gchar* user, - const gchar* server, - OperationDoneCallback callback, - gpointer data, - GDestroyNotify destroy_data ) -{ - if ( !isAvailable() ) - return 0; - return instance().delete_password( instance().NETWORK_PASSWORD, - callback, data, destroy_data, - "user", user, "server", server, static_cast(0) ); -} - -GnomeKeyring::GnomeKeyring() - : QLibrary(QLatin1String("gnome-keyring"), 0) -{ - static const PasswordSchema schema = { - ITEM_NETWORK_PASSWORD, - {{ "user", ATTRIBUTE_TYPE_STRING }, - { "server", ATTRIBUTE_TYPE_STRING }, - { "type", ATTRIBUTE_TYPE_STRING }, - { 0, static_cast( 0 ) }} - }; - - NETWORK_PASSWORD = &schema; - is_available = reinterpret_cast( resolve( "gnome_keyring_is_available" ) ); - find_password = reinterpret_cast( resolve( "gnome_keyring_find_password" ) ); - store_password = reinterpret_cast( resolve( "gnome_keyring_store_password" ) ); - delete_password = reinterpret_cast( resolve( "gnome_keyring_delete_password" ) ); -} - -GnomeKeyring& GnomeKeyring::instance() { - static GnomeKeyring keyring; - return keyring; -} diff --git a/gnomekeyring_p.h b/gnomekeyring_p.h deleted file mode 100644 index 87c062c3..00000000 --- a/gnomekeyring_p.h +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef QTKEYCHAIN_GNOME_P_H -#define QTKEYCHAIN_GNOME_P_H - -#include - -class GnomeKeyring : private QLibrary { - Q_OBJECT - -public: - enum Result { - RESULT_OK, - RESULT_DENIED, - RESULT_NO_KEYRING_DAEMON, - RESULT_ALREADY_UNLOCKED, - RESULT_NO_SUCH_KEYRING, - RESULT_BAD_ARGUMENTS, - RESULT_IO_ERROR, - RESULT_CANCELLED, - RESULT_KEYRING_ALREADY_EXISTS, - RESULT_NO_MATCH - }; - - enum ItemType { - ITEM_GENERIC_SECRET = 0, - ITEM_NETWORK_PASSWORD, - ITEM_NOTE, - ITEM_CHAINED_KEYRING_PASSWORD, - ITEM_ENCRYPTION_KEY_PASSWORD, - ITEM_PK_STORAGE = 0x100 - }; - - enum AttributeType { - ATTRIBUTE_TYPE_STRING, - ATTRIBUTE_TYPE_UINT32 - }; - - typedef char gchar; - typedef void* gpointer; - typedef bool gboolean; - typedef struct { - ItemType item_type; - struct { - const gchar* name; - AttributeType type; - } attributes[32]; - } PasswordSchema; - - typedef void ( *OperationGetStringCallback )( Result result, bool binary, - const char* string, gpointer data ); - typedef void ( *OperationDoneCallback )( Result result, gpointer data ); - typedef void ( *GDestroyNotify )( gpointer data ); - - static const char* GNOME_KEYRING_DEFAULT; - - static bool isAvailable(); - - static gpointer store_network_password( const gchar* keyring, const gchar* display_name, - const gchar* user, const gchar* server, - const gchar* type, const gchar* password, - OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data ); - - static gpointer find_network_password( const gchar* user, const gchar* server, - const gchar* type, - OperationGetStringCallback callback, - gpointer data, GDestroyNotify destroy_data ); - - static gpointer delete_network_password( const gchar* user, const gchar* server, - OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data ); -private: - GnomeKeyring(); - - static GnomeKeyring& instance(); - - const PasswordSchema* NETWORK_PASSWORD; - typedef gboolean ( is_available_fn )( void ); - typedef gpointer ( store_password_fn )( const PasswordSchema* schema, const gchar* keyring, - const gchar* display_name, const gchar* password, - OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data, - ... ); - typedef gpointer ( find_password_fn )( const PasswordSchema* schema, - OperationGetStringCallback callback, gpointer data, GDestroyNotify destroy_data, - ... ); - typedef gpointer ( delete_password_fn )( const PasswordSchema* schema, - OperationDoneCallback callback, gpointer data, GDestroyNotify destroy_data, - ... ); - - is_available_fn* is_available; - find_password_fn* find_password; - store_password_fn* store_password; - delete_password_fn* delete_password; -}; - - -#endif diff --git a/keychain.cpp b/keychain.cpp deleted file mode 100644 index 90ee4eb5..00000000 --- a/keychain.cpp +++ /dev/null @@ -1,235 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2011-2015 Frank Osterfeld * - * * - * 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. For licensing and distribution * - * details, check the accompanying file 'COPYING'. * - *****************************************************************************/ -#include "keychain.h" -#include "keychain_p.h" - -using namespace QKeychain; - -Job::Job( JobPrivate *q, QObject *parent ) - : QObject( parent ) - , d ( q ) { -} - -Job::~Job() { - delete d; -} - -QString Job::service() const { - return d->service; -} - -QSettings* Job::settings() const { - return d->settings; -} - -void Job::setSettings( QSettings* settings ) { - d->settings = settings; -} - -void Job::start() { - QMetaObject::invokeMethod( this, "doStart", Qt::QueuedConnection ); -} - -bool Job::autoDelete() const { - return d->autoDelete; -} - -void Job::setAutoDelete( bool autoDelete ) { - d->autoDelete = autoDelete; -} - -bool Job::insecureFallback() const { - return d->insecureFallback; -} - -void Job::setInsecureFallback( bool insecureFallback ) { - d->insecureFallback = insecureFallback; -} - -void Job::doStart() { - JobExecutor::instance()->enqueue( this ); -} - -void Job::emitFinished() { - emit finished( this ); - if ( d->autoDelete ) - deleteLater(); -} - -void Job::emitFinishedWithError( Error error, const QString& errorString ) { - d->error = error; - d->errorString = errorString; - emitFinished(); -} - -void Job::scheduledStart() { - d->scheduledStart(); -} - -Error Job::error() const { - return d->error; -} - -QString Job::errorString() const { - return d->errorString; -} - -void Job::setError( Error error ) { - d->error = error; -} - -void Job::setErrorString( const QString& errorString ) { - d->errorString = errorString; -} - -ReadPasswordJob::ReadPasswordJob( const QString& service, QObject* parent ) - : Job( new ReadPasswordJobPrivate( service, this ), parent ) { - -} - -ReadPasswordJob::~ReadPasswordJob() { -} - -QString ReadPasswordJob::textData() const { - return QString::fromUtf8( d->data ); -} - -QByteArray ReadPasswordJob::binaryData() const { - return d->data; -} - -QString Job::key() const { - return d->key; -} - -void Job::setKey( const QString& key_ ) { - d->key = key_; -} - -WritePasswordJob::WritePasswordJob( const QString& service, QObject* parent ) - : Job( new WritePasswordJobPrivate( service, this ), parent ) { -} - -WritePasswordJob::~WritePasswordJob() { -} - -void WritePasswordJob::setBinaryData( const QByteArray& data ) { - d->data = data; - d->mode = JobPrivate::Binary; -} - -void WritePasswordJob::setTextData( const QString& data ) { - d->data = data.toUtf8(); - d->mode = JobPrivate::Text; -} - -DeletePasswordJob::DeletePasswordJob( const QString& service, QObject* parent ) - : Job( new DeletePasswordJobPrivate( service, this ), parent ) { -} - -DeletePasswordJob::~DeletePasswordJob() { -} - -DeletePasswordJobPrivate::DeletePasswordJobPrivate(const QString &service_, DeletePasswordJob *qq) : - JobPrivate(service_, qq) { - -} - -JobExecutor::JobExecutor() - : QObject( 0 ) - , m_jobRunning( false ) { -} - -void JobExecutor::enqueue( Job* job ) { - m_queue.enqueue( job ); - startNextIfNoneRunning(); -} - -void JobExecutor::startNextIfNoneRunning() { - if ( m_queue.isEmpty() || m_jobRunning ) - return; - QPointer next; - while ( !next && !m_queue.isEmpty() ) { - next = m_queue.dequeue(); - } - if ( next ) { - connect( next, SIGNAL(finished(QKeychain::Job*)), this, SLOT(jobFinished(QKeychain::Job*)) ); - connect( next, SIGNAL(destroyed(QObject*)), this, SLOT(jobDestroyed(QObject*)) ); - m_jobRunning = true; - next->scheduledStart(); - } -} - -void JobExecutor::jobDestroyed( QObject* object ) { - Job* job = static_cast(object); - Q_UNUSED( object ) // for release mode - job->disconnect( this ); - m_jobRunning = false; - startNextIfNoneRunning(); -} - -void JobExecutor::jobFinished( Job* job ) { - Q_UNUSED( job ) // for release mode - job->disconnect( this ); - m_jobRunning = false; - startNextIfNoneRunning(); -} - -JobExecutor* JobExecutor::s_instance = 0; - -JobExecutor* JobExecutor::instance() { - if ( !s_instance ) - s_instance = new JobExecutor; - return s_instance; -} - -ReadPasswordJobPrivate::ReadPasswordJobPrivate(const QString &service_, ReadPasswordJob *qq) : - JobPrivate(service_, qq) { - -} - -JobPrivate::JobPrivate(const QString &service_, Job *qq) - : q(qq) - , mode( Text ) - , error( NoError ) - , service( service_ ) - , autoDelete( true ) - , insecureFallback( false ) -{ -} - -QString JobPrivate::modeToString(Mode m) -{ - switch (m) { - case Text: - return QLatin1String("Text"); - case Binary: - return QLatin1String("Binary"); - } - - Q_ASSERT_X(false, Q_FUNC_INFO, "Unhandled Mode value"); - return QString(); -} - -JobPrivate::Mode JobPrivate::stringToMode(const QString& s) -{ - if (s == QLatin1String("Text") || s == QLatin1String("1")) - return Text; - if (s == QLatin1String("Binary") || s == QLatin1String("2")) - return Binary; - - qCritical("Unexpected mode string '%s'", qPrintable(s)); - - return Text; -} - -WritePasswordJobPrivate::WritePasswordJobPrivate(const QString &service_, WritePasswordJob *qq) : - JobPrivate(service_, qq) { - -} diff --git a/keychain_ios.mm b/keychain_ios.mm deleted file mode 100644 index 4662e40e..00000000 --- a/keychain_ios.mm +++ /dev/null @@ -1,146 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2016 Mathias Hasselmann * - * * - * 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. For licensing and distribution * - * details, check the accompanying file 'COPYING'. * - *****************************************************************************/ - -#include "keychain_p.h" - -#import -#import - -using namespace QKeychain; - -struct ErrorDescription -{ - QKeychain::Error code; - QString message; - - ErrorDescription(QKeychain::Error code, const QString &message) - : code(code), message(message) {} - - static ErrorDescription fromStatus(OSStatus status) - { - switch(status) { - case errSecSuccess: - return ErrorDescription(QKeychain::NoError, Job::tr("No error")); - case errSecItemNotFound: - return ErrorDescription(QKeychain::EntryNotFound, Job::tr("The specified item could not be found in the keychain")); - case errSecUserCanceled: - return ErrorDescription(QKeychain::AccessDeniedByUser, Job::tr("User canceled the operation")); - case errSecInteractionNotAllowed: - return ErrorDescription(QKeychain::AccessDenied, Job::tr("User interaction is not allowed")); - case errSecNotAvailable: - return ErrorDescription(QKeychain::AccessDenied, Job::tr("No keychain is available. You may need to restart your computer")); - case errSecAuthFailed: - return ErrorDescription(QKeychain::AccessDenied, Job::tr("The user name or passphrase you entered is not correct")); - case errSecVerifyFailed: - return ErrorDescription(QKeychain::AccessDenied, Job::tr("A cryptographic verification failure has occurred")); - case errSecUnimplemented: - return ErrorDescription(QKeychain::NotImplemented, Job::tr("Function or operation not implemented")); - case errSecIO: - return ErrorDescription(QKeychain::OtherError, Job::tr("I/O error")); - case errSecOpWr: - return ErrorDescription(QKeychain::OtherError, Job::tr("Already open with with write permission")); - case errSecParam: - return ErrorDescription(QKeychain::OtherError, Job::tr("Invalid parameters passed to a function")); - case errSecAllocate: - return ErrorDescription(QKeychain::OtherError, Job::tr("Failed to allocate memory")); - case errSecBadReq: - return ErrorDescription(QKeychain::OtherError, Job::tr("Bad parameter or invalid state for operation")); - case errSecInternalComponent: - return ErrorDescription(QKeychain::OtherError, Job::tr("An internal component failed")); - case errSecDuplicateItem: - return ErrorDescription(QKeychain::OtherError, Job::tr("The specified item already exists in the keychain")); - case errSecDecode: - return ErrorDescription(QKeychain::OtherError, Job::tr("Unable to decode the provided data")); - } - - return ErrorDescription(QKeychain::OtherError, Job::tr("Unknown error")); - } -}; - -void ReadPasswordJobPrivate::scheduledStart() -{ - NSDictionary *const query = @{ - (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, - (__bridge id) kSecAttrService: (__bridge NSString *) service.toCFString(), - (__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(), - (__bridge id) kSecReturnData: @YES, - }; - - CFTypeRef dataRef = nil; - const OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &dataRef); - - data.clear(); - mode = Binary; - - if (status == errSecSuccess) { - if (dataRef) - data = QByteArray::fromCFData((CFDataRef) dataRef); - - q->emitFinished(); - } else { - const ErrorDescription error = ErrorDescription::fromStatus(status); - q->emitFinishedWithError(error.code, Job::tr("Could not retrieve private key from keystore: %1").arg(error.message)); - } - - if (dataRef) - [dataRef release]; -} - -void WritePasswordJobPrivate::scheduledStart() -{ - NSDictionary *const query = @{ - (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, - (__bridge id) kSecAttrService: (__bridge NSString *) service.toCFString(), - (__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(), - }; - - OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, nil); - - if (status == errSecSuccess) { - NSDictionary *const update = @{ - (__bridge id) kSecValueData: (__bridge NSData *) data.toCFData(), - }; - - status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update); - } else { - NSDictionary *const insert = @{ - (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, - (__bridge id) kSecAttrService: (__bridge NSString *) service.toCFString(), - (__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(), - (__bridge id) kSecValueData: (__bridge NSData *) data.toCFData(), - }; - - status = SecItemAdd((__bridge CFDictionaryRef) insert, nil); - } - - if (status == errSecSuccess) { - q->emitFinished(); - } else { - const ErrorDescription error = ErrorDescription::fromStatus(status); - q->emitFinishedWithError(error.code, tr("Could not store data in settings: %1").arg(error.message)); - } -} - -void DeletePasswordJobPrivate::scheduledStart() -{ - const NSDictionary *const query = @{ - (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, - (__bridge id) kSecAttrService: (__bridge NSString *) service.toCFString(), - (__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(), - }; - - const OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query); - - if (status == errSecSuccess) { - q->emitFinished(); - } else { - const ErrorDescription error = ErrorDescription::fromStatus(status); - q->emitFinishedWithError(error.code, Job::tr("Could not remove private key from keystore: %1").arg(error.message)); - } -} diff --git a/keychain_mac.cpp b/keychain_mac.cpp deleted file mode 100644 index dc8365b1..00000000 --- a/keychain_mac.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2011-2015 Frank Osterfeld * - * * - * 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. For licensing and distribution * - * details, check the accompanying file 'COPYING'. * - *****************************************************************************/ -#include "keychain_p.h" - -#include -#include -#include - -using namespace QKeychain; - -template -struct Releaser { - explicit Releaser( const T& v ) : value( v ) {} - ~Releaser() { - CFRelease( value ); - } - - const T value; -}; - -static QString strForStatus( OSStatus os ) { - const Releaser str( SecCopyErrorMessageString( os, 0 ) ); - const char * const buf = CFStringGetCStringPtr( str.value, kCFStringEncodingUTF8 ); - if ( !buf ) - return QObject::tr( "OS X Keychain error (OSStatus %1)" ).arg( os ); - return QObject::tr( "%1 (OSStatus %2)" ) - .arg( QString::fromUtf8( buf, strlen( buf ) ) ).arg( os ); -} - -static OSStatus readPw( QByteArray* pw, - const QString& service, - const QString& account, - SecKeychainItemRef* ref ) { - Q_ASSERT( pw ); - pw->clear(); - const QByteArray serviceData = service.toUtf8(); - const QByteArray accountData = account.toUtf8(); - - void* data = 0; - UInt32 len = 0; - - const OSStatus ret = SecKeychainFindGenericPassword( NULL, // default keychain - serviceData.size(), - serviceData.constData(), - accountData.size(), - accountData.constData(), - &len, - &data, - ref ); - if ( ret == noErr ) { - *pw = QByteArray( reinterpret_cast( data ), len ); - const OSStatus ret2 = SecKeychainItemFreeContent ( 0, data ); - if ( ret2 != noErr ) - qWarning() << "Could not free item content: " << strForStatus( ret2 ); - } - return ret; -} - -void ReadPasswordJobPrivate::scheduledStart() -{ - QString errorString; - Error error = NoError; - const OSStatus ret = readPw( &data, q->service(), q->key(), 0 ); - - switch ( ret ) { - case noErr: - break; - case errSecItemNotFound: - errorString = tr("Password not found"); - error = EntryNotFound; - break; - default: - errorString = strForStatus( ret ); - error = OtherError; - break; - } - q->emitFinishedWithError( error, errorString ); -} - - -static QKeychain::Error deleteEntryImpl( const QString& service, const QString& account, QString* err ) { - SecKeychainItemRef ref; - QByteArray pw; - const OSStatus ret1 = readPw( &pw, service, account, &ref ); - if ( ret1 == errSecItemNotFound ) - return NoError; // No item stored, we're done - if ( ret1 != noErr ) { - *err = strForStatus( ret1 ); - //TODO map error code, set errstr - return OtherError; - } - const Releaser releaser( ref ); - - const OSStatus ret2 = SecKeychainItemDelete( ref ); - - if ( ret2 == noErr ) - return NoError; - //TODO map error code - *err = strForStatus( ret2 ); - return CouldNotDeleteEntry; -} - -static QKeychain::Error writeEntryImpl( const QString& service, - const QString& account, - const QByteArray& data, - QString* err ) { - Q_ASSERT( err ); - err->clear(); - const QByteArray serviceData = service.toUtf8(); - const QByteArray accountData = account.toUtf8(); - const OSStatus ret = SecKeychainAddGenericPassword( NULL, //default keychain - serviceData.size(), - serviceData.constData(), - accountData.size(), - accountData.constData(), - data.size(), - data.constData(), - NULL //item reference - ); - if ( ret != noErr ) { - switch ( ret ) { - case errSecDuplicateItem: - { - Error derr = deleteEntryImpl( service, account, err ); - if ( derr != NoError ) - return CouldNotDeleteEntry; - else - return writeEntryImpl( service, account, data, err ); - } - default: - *err = strForStatus( ret ); - return OtherError; - } - } - - return NoError; -} - -void WritePasswordJobPrivate::scheduledStart() -{ - QString errorString; - Error error = NoError; - - error = writeEntryImpl( q->service(), key, data, &errorString ); - q->emitFinishedWithError( error, errorString ); -} - -void DeletePasswordJobPrivate::scheduledStart() -{ - QString errorString; - Error error = NoError; - - const Error derr = deleteEntryImpl( q->service(), key, &errorString ); - if ( derr != NoError ) - error = CouldNotDeleteEntry; - q->emitFinishedWithError( error, errorString ); -} diff --git a/keychain_p.h b/keychain_p.h deleted file mode 100644 index ab7f0be5..00000000 --- a/keychain_p.h +++ /dev/null @@ -1,167 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2011-2015 Frank Osterfeld * - * * - * 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. For licensing and distribution * - * details, check the accompanying file 'COPYING'. * - *****************************************************************************/ -#ifndef KEYCHAIN_P_H -#define KEYCHAIN_P_H - -#include -#include -#include -#include -#include - -#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) - -#include - -#include "kwallet_interface.h" -#else - -class QDBusPendingCallWatcher; - -#endif - -#include "keychain.h" - -namespace QKeychain { - -class JobExecutor; - -class JobPrivate : public QObject { - Q_OBJECT -public: - enum Mode { - Text, - Binary - }; - - virtual void scheduledStart() = 0; - - static QString modeToString(Mode m); - static Mode stringToMode(const QString& s); - - Job* const q; - Mode mode; - QByteArray data; - -#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) - org::kde::KWallet* iface; - int walletHandle; - - static void gnomeKeyring_readCb( int result, const char* string, JobPrivate* data ); - static void gnomeKeyring_writeCb( int result, JobPrivate* self ); - - virtual void fallbackOnError(const QDBusError& err) = 0; - -protected Q_SLOTS: - void kwalletWalletFound( QDBusPendingCallWatcher* watcher ); - virtual void kwalletFinished( QDBusPendingCallWatcher* watcher ); - virtual void kwalletOpenFinished( QDBusPendingCallWatcher* watcher ); -#else - void kwalletWalletFound( QDBusPendingCallWatcher* ) {} - virtual void kwalletFinished( QDBusPendingCallWatcher* ) {} - virtual void kwalletOpenFinished( QDBusPendingCallWatcher* ) {} -#endif - -protected: - JobPrivate( const QString& service_, Job *q ); - -protected: - QKeychain::Error error; - QString errorString; - QString service; - bool autoDelete; - bool insecureFallback; - QPointer settings; - QString key; - -friend class Job; -friend class JobExecutor; -friend class ReadPasswordJob; -friend class WritePasswordJob; -friend class PlainTextStore; -}; - -class ReadPasswordJobPrivate : public JobPrivate { - Q_OBJECT -public: - explicit ReadPasswordJobPrivate( const QString &service_, ReadPasswordJob* qq ); - void scheduledStart(); - -#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) - void fallbackOnError(const QDBusError& err); - -private Q_SLOTS: - void kwalletOpenFinished( QDBusPendingCallWatcher* watcher ); - void kwalletEntryTypeFinished( QDBusPendingCallWatcher* watcher ); - void kwalletFinished( QDBusPendingCallWatcher* watcher ); -#else //moc's too dumb to respect above macros, so just define empty slot implementations -private Q_SLOTS: - void kwalletOpenFinished( QDBusPendingCallWatcher* ) {} - void kwalletEntryTypeFinished( QDBusPendingCallWatcher* ) {} - void kwalletFinished( QDBusPendingCallWatcher* ) {} -#endif - - friend class ReadPasswordJob; -}; - -class WritePasswordJobPrivate : public JobPrivate { - Q_OBJECT -public: - explicit WritePasswordJobPrivate( const QString &service_, WritePasswordJob* qq ); - void scheduledStart(); - -#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) - void fallbackOnError(const QDBusError& err); -#endif - - friend class WritePasswordJob; -}; - -class DeletePasswordJobPrivate : public JobPrivate { - Q_OBJECT -public: - explicit DeletePasswordJobPrivate( const QString &service_, DeletePasswordJob* qq ); - - void scheduledStart(); - -#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) - void fallbackOnError(const QDBusError& err); -#endif - -protected: - void doStart(); - - friend class DeletePasswordJob; -}; - -class JobExecutor : public QObject { - Q_OBJECT -public: - - static JobExecutor* instance(); - - void enqueue( Job* job ); - -private: - explicit JobExecutor(); - void startNextIfNoneRunning(); - -private Q_SLOTS: - void jobFinished( QKeychain::Job* ); - void jobDestroyed( QObject* object ); - -private: - static JobExecutor* s_instance; - QQueue > m_queue; - bool m_jobRunning; -}; - -} - -#endif // KEYCHAIN_P_H diff --git a/keychain_unix.cpp b/keychain_unix.cpp deleted file mode 100644 index 958927ab..00000000 --- a/keychain_unix.cpp +++ /dev/null @@ -1,586 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2011-2015 Frank Osterfeld * - * * - * 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. For licensing and distribution * - * details, check the accompanying file 'COPYING'. * - *****************************************************************************/ -#include "keychain_p.h" -#include "gnomekeyring_p.h" -#include "libsecret_p.h" -#include "plaintextstore_p.h" - -#include - -using namespace QKeychain; - -enum KeyringBackend { - Backend_LibSecretKeyring, - Backend_GnomeKeyring, - Backend_Kwallet4, - Backend_Kwallet5 -}; - -enum DesktopEnvironment { - DesktopEnv_Gnome, - DesktopEnv_Kde4, - DesktopEnv_Plasma5, - DesktopEnv_Unity, - DesktopEnv_Xfce, - DesktopEnv_Other -}; - -// the following detection algorithm is derived from chromium, -// licensed under BSD, see base/nix/xdg_util.cc - -static DesktopEnvironment getKdeVersion() { - QByteArray value = qgetenv("KDE_SESSION_VERSION"); - if ( value == "5" ) { - return DesktopEnv_Plasma5; - } else if (value == "4" ) { - return DesktopEnv_Kde4; - } else { - // most likely KDE3 - return DesktopEnv_Other; - } -} - -static DesktopEnvironment detectDesktopEnvironment() { - QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP"); - if ( xdgCurrentDesktop == "GNOME" ) { - return DesktopEnv_Gnome; - } else if ( xdgCurrentDesktop == "Unity" ) { - return DesktopEnv_Unity; - } else if ( xdgCurrentDesktop == "KDE" ) { - return getKdeVersion(); - } else if ( xdgCurrentDesktop == "XFCE" ) { - return DesktopEnv_Xfce; - } - - QByteArray desktopSession = qgetenv("DESKTOP_SESSION"); - if ( desktopSession == "gnome" ) { - return DesktopEnv_Gnome; - } else if ( desktopSession == "kde" ) { - return getKdeVersion(); - } else if ( desktopSession == "kde4" ) { - return DesktopEnv_Kde4; - } else if ( desktopSession.contains("xfce") || desktopSession == "xubuntu" ) { - return DesktopEnv_Xfce; - } - - if ( !qgetenv("GNOME_DESKTOP_SESSION_ID").isEmpty() ) { - return DesktopEnv_Gnome; - } else if ( !qgetenv("KDE_FULL_SESSION").isEmpty() ) { - return getKdeVersion(); - } - - return DesktopEnv_Other; -} - -static bool isKwallet5Available() -{ - if (!QDBusConnection::sessionBus().isConnected()) - return false; - - org::kde::KWallet iface( - QLatin1String("org.kde.kwalletd5"), - QLatin1String("/modules/kwalletd5"), - QDBusConnection::sessionBus()); - - // At this point iface.isValid() can return false even though the - // interface is activatable by making a call. Hence we check whether - // a wallet can be opened. - - iface.setTimeout(500); - QDBusMessage reply = iface.call(QStringLiteral("networkWallet")); - return reply.type() == QDBusMessage::ReplyMessage; -} - -static KeyringBackend detectKeyringBackend() -{ - /* The secret service dbus api, accessible through libsecret, is supposed - * to unify password services. - * - * Unfortunately at the time of Kubuntu 18.04 the secret service backend - * in KDE is gnome-keyring-daemon - using it has several complications: - * - the default collection isn't opened on session start, so users need - * to manually unlock it when the first application uses it - * - it's separate from the kwallet5 keyring, so switching to it means the - * existing keyring data can't be accessed anymore - * - * Thus we still prefer kwallet backends on KDE even if libsecret is - * available. - */ - - switch (detectDesktopEnvironment()) { - case DesktopEnv_Kde4: - return Backend_Kwallet4; - - case DesktopEnv_Plasma5: - if (isKwallet5Available()) { - return Backend_Kwallet5; - } - if (LibSecretKeyring::isAvailable()) { - return Backend_LibSecretKeyring; - } - if (GnomeKeyring::isAvailable()) { - return Backend_GnomeKeyring; - } - // During startup the keychain backend might just not have started yet - return Backend_Kwallet5; - - case DesktopEnv_Gnome: - case DesktopEnv_Unity: - case DesktopEnv_Xfce: - case DesktopEnv_Other: - default: - if (LibSecretKeyring::isAvailable()) { - return Backend_LibSecretKeyring; - } - if (GnomeKeyring::isAvailable()) { - return Backend_GnomeKeyring; - } - if (isKwallet5Available()) { - return Backend_Kwallet5; - } - // During startup the keychain backend might just not have started yet - // - // This doesn't need to be libsecret because LibSecretKeyring::isAvailable() - // only fails if the libsecret shared library couldn't be loaded. In contrast - // to that GnomeKeyring::isAvailable() can return false if the shared library - // *was* loaded but its libgnome_keyring::is_available() returned false. - // - // In the future there should be a difference between "API available" and - // "keychain available". - return Backend_GnomeKeyring; - } - -} - -static KeyringBackend getKeyringBackend() -{ - static KeyringBackend backend = detectKeyringBackend(); - return backend; -} - -static void kwalletReadPasswordScheduledStartImpl(const char * service, const char * path, ReadPasswordJobPrivate * priv) { - if ( QDBusConnection::sessionBus().isConnected() ) - { - priv->iface = new org::kde::KWallet( QLatin1String(service), QLatin1String(path), QDBusConnection::sessionBus(), priv ); - const QDBusPendingReply reply = priv->iface->networkWallet(); - QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher( reply, priv ); - priv->connect( watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), priv, SLOT(kwalletWalletFound(QDBusPendingCallWatcher*)) ); - } - else - { - // D-Bus is not reachable so none can tell us something about KWalletd - QDBusError err( QDBusError::NoServer, ReadPasswordJobPrivate::tr("D-Bus is not running") ); - priv->fallbackOnError( err ); - } -} - -void ReadPasswordJobPrivate::scheduledStart() { - switch ( getKeyringBackend() ) { - case Backend_LibSecretKeyring: { - if ( !LibSecretKeyring::findPassword(key, q->service(), this) ) { - q->emitFinishedWithError( OtherError, tr("Unknown error") ); - } - } break; - case Backend_GnomeKeyring: - this->mode = JobPrivate::Text; - if ( !GnomeKeyring::find_network_password( key.toUtf8().constData(), - q->service().toUtf8().constData(), - "plaintext", - reinterpret_cast( &JobPrivate::gnomeKeyring_readCb ), - this, 0 ) ) - q->emitFinishedWithError( OtherError, tr("Unknown error") ); - break; - - case Backend_Kwallet4: - kwalletReadPasswordScheduledStartImpl("org.kde.kwalletd", "/modules/kwalletd", this); - break; - case Backend_Kwallet5: - kwalletReadPasswordScheduledStartImpl("org.kde.kwalletd5", "/modules/kwalletd5", this); - break; - } -} - -void JobPrivate::kwalletWalletFound(QDBusPendingCallWatcher *watcher) -{ - watcher->deleteLater(); - const QDBusPendingReply reply = *watcher; - const QDBusPendingReply pendingReply = iface->open( reply.value(), 0, q->service() ); - QDBusPendingCallWatcher* pendingWatcher = new QDBusPendingCallWatcher( pendingReply, this ); - connect( pendingWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), - this, SLOT(kwalletOpenFinished(QDBusPendingCallWatcher*)) ); -} - -static QPair mapGnomeKeyringError( int result ) -{ - Q_ASSERT( result != GnomeKeyring::RESULT_OK ); - - switch ( result ) { - case GnomeKeyring::RESULT_DENIED: - return qMakePair( AccessDenied, QObject::tr("Access to keychain denied") ); - case GnomeKeyring::RESULT_NO_KEYRING_DAEMON: - return qMakePair( NoBackendAvailable, QObject::tr("No keyring daemon") ); - case GnomeKeyring::RESULT_ALREADY_UNLOCKED: - return qMakePair( OtherError, QObject::tr("Already unlocked") ); - case GnomeKeyring::RESULT_NO_SUCH_KEYRING: - return qMakePair( OtherError, QObject::tr("No such keyring") ); - case GnomeKeyring::RESULT_BAD_ARGUMENTS: - return qMakePair( OtherError, QObject::tr("Bad arguments") ); - case GnomeKeyring::RESULT_IO_ERROR: - return qMakePair( OtherError, QObject::tr("I/O error") ); - case GnomeKeyring::RESULT_CANCELLED: - return qMakePair( OtherError, QObject::tr("Cancelled") ); - case GnomeKeyring::RESULT_KEYRING_ALREADY_EXISTS: - return qMakePair( OtherError, QObject::tr("Keyring already exists") ); - case GnomeKeyring::RESULT_NO_MATCH: - return qMakePair( EntryNotFound, QObject::tr("No match") ); - default: - break; - } - - return qMakePair( OtherError, QObject::tr("Unknown error") ); -} - -void JobPrivate::gnomeKeyring_readCb( int result, const char* string, JobPrivate* self ) -{ - if ( result == GnomeKeyring::RESULT_OK ) { - if (self->mode == JobPrivate::Text) - self->data = QByteArray(string); - else - self->data = QByteArray::fromBase64(string); - - self->q->emitFinished(); - } else if (self->mode == JobPrivate::Text) { - self->mode = JobPrivate::Binary; - if ( !GnomeKeyring::find_network_password( self->key.toUtf8().constData(), - self->q->service().toUtf8().constData(), - "base64", - reinterpret_cast( &JobPrivate::gnomeKeyring_readCb ), - self, 0 ) ) - self->q->emitFinishedWithError( OtherError, tr("Unknown error") ); - } else { - const QPair errorResult = mapGnomeKeyringError( result ); - self->q->emitFinishedWithError( errorResult.first, errorResult.second ); - } -} - -void ReadPasswordJobPrivate::fallbackOnError(const QDBusError& err ) -{ - PlainTextStore plainTextStore( q->service(), q->settings() ); - - if ( q->insecureFallback() && plainTextStore.contains( key ) ) { - mode = plainTextStore.readMode( key ); - data = plainTextStore.readData( key ); - - if ( plainTextStore.error() != NoError ) - q->emitFinishedWithError( plainTextStore.error(), plainTextStore.errorString() ); - else - q->emitFinished(); - } else { - if ( err.type() == QDBusError::ServiceUnknown ) //KWalletd not running - q->emitFinishedWithError( NoBackendAvailable, tr("No keychain service available") ); - else - q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); - } -} - -void ReadPasswordJobPrivate::kwalletOpenFinished( QDBusPendingCallWatcher* watcher ) { - watcher->deleteLater(); - const QDBusPendingReply reply = *watcher; - - if ( reply.isError() ) { - fallbackOnError( reply.error() ); - return; - } - - PlainTextStore plainTextStore( q->service(), q->settings() ); - - if ( plainTextStore.contains( key ) ) { - // We previously stored data in the insecure QSettings, but now have KWallet available. - // Do the migration - - data = plainTextStore.readData( key ); - const WritePasswordJobPrivate::Mode mode = plainTextStore.readMode( key ); - plainTextStore.remove( key ); - - q->emitFinished(); - - - WritePasswordJob* j = new WritePasswordJob( q->service(), 0 ); - j->setSettings( q->settings() ); - j->setKey( key ); - j->setAutoDelete( true ); - if ( mode == WritePasswordJobPrivate::Binary ) - j->setBinaryData( data ); - else if ( mode == WritePasswordJobPrivate::Text ) - j->setTextData( QString::fromUtf8( data ) ); - else - Q_ASSERT( false ); - - j->start(); - - return; - } - - walletHandle = reply.value(); - - if ( walletHandle < 0 ) { - q->emitFinishedWithError( AccessDenied, tr("Access to keychain denied") ); - return; - } - - const QDBusPendingReply nextReply = iface->entryType( walletHandle, q->service(), key, q->service() ); - QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); - connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletEntryTypeFinished(QDBusPendingCallWatcher*)) ); -} - -//Must be in sync with KWallet::EntryType (kwallet.h) -enum KWalletEntryType { - Unknown=0, - Password, - Stream, - Map -}; - -void ReadPasswordJobPrivate::kwalletEntryTypeFinished( QDBusPendingCallWatcher* watcher ) { - watcher->deleteLater(); - if ( watcher->isError() ) { - const QDBusError err = watcher->error(); - q->emitFinishedWithError( OtherError, tr("Could not determine data type: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); - return; - } - - const QDBusPendingReply reply = *watcher; - const int value = reply.value(); - - switch ( value ) { - case Unknown: - q->emitFinishedWithError( EntryNotFound, tr("Entry not found") ); - return; - case Password: - mode = Text; - break; - case Stream: - mode = Binary; - break; - case Map: - q->emitFinishedWithError( EntryNotFound, tr("Unsupported entry type 'Map'") ); - return; - default: - q->emitFinishedWithError( OtherError, tr("Unknown kwallet entry type '%1'").arg( value ) ); - return; - } - - const QDBusPendingCall nextReply = (mode == Text) - ? QDBusPendingCall( iface->readPassword( walletHandle, q->service(), key, q->service() ) ) - : QDBusPendingCall( iface->readEntry( walletHandle, q->service(), key, q->service() ) ); - QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); - connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletFinished(QDBusPendingCallWatcher*)) ); -} - -void ReadPasswordJobPrivate::kwalletFinished( QDBusPendingCallWatcher* watcher ) { - if ( !watcher->isError() ) { - if ( mode == Binary ) { - QDBusPendingReply reply = *watcher; - if (reply.isValid()) { - data = reply.value(); - } - } else { - QDBusPendingReply reply = *watcher; - if (reply.isValid()) { - data = reply.value().toUtf8(); - } - } - } - - JobPrivate::kwalletFinished(watcher); -} - -static void kwalletWritePasswordScheduledStart( const char * service, const char * path, JobPrivate * priv ) { - if ( QDBusConnection::sessionBus().isConnected() ) - { - priv->iface = new org::kde::KWallet( QLatin1String(service), QLatin1String(path), QDBusConnection::sessionBus(), priv ); - const QDBusPendingReply reply = priv->iface->networkWallet(); - QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher( reply, priv ); - priv->connect( watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), priv, SLOT(kwalletWalletFound(QDBusPendingCallWatcher*)) ); - } - else - { - // D-Bus is not reachable so none can tell us something about KWalletd - QDBusError err( QDBusError::NoServer, WritePasswordJobPrivate::tr("D-Bus is not running") ); - priv->fallbackOnError( err ); - } -} - -void WritePasswordJobPrivate::scheduledStart() { - switch ( getKeyringBackend() ) { - case Backend_LibSecretKeyring: { - if ( !LibSecretKeyring::writePassword(service, key, service, mode, - data, this) ) { - q->emitFinishedWithError( OtherError, tr("Unknown error") ); - } - } break; - case Backend_GnomeKeyring: { - QString type; - QByteArray password; - - switch(mode) { - case JobPrivate::Text: - type = QLatin1String("plaintext"); - password = data; - break; - default: - type = QLatin1String("base64"); - password = data.toBase64(); - break; - } - - QByteArray service = q->service().toUtf8(); - if ( !GnomeKeyring::store_network_password( GnomeKeyring::GNOME_KEYRING_DEFAULT, - service.constData(), - key.toUtf8().constData(), - service.constData(), - type.toUtf8().constData(), - password.constData(), - reinterpret_cast( &JobPrivate::gnomeKeyring_writeCb ), - this, 0 ) ) - q->emitFinishedWithError( OtherError, tr("Unknown error") ); - } - break; - - case Backend_Kwallet4: - kwalletWritePasswordScheduledStart("org.kde.kwalletd", "/modules/kwalletd", this); - break; - case Backend_Kwallet5: - kwalletWritePasswordScheduledStart("org.kde.kwalletd5", "/modules/kwalletd5", this); - break; - } -} - -void WritePasswordJobPrivate::fallbackOnError(const QDBusError &err) -{ - if ( !q->insecureFallback() ) { - q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2").arg( QDBusError::errorString( err.type() ), err.message() ) ); - return; - } - - PlainTextStore plainTextStore( q->service(), q->settings() ); - plainTextStore.write( key, data, mode ); - - if ( plainTextStore.error() != NoError ) - q->emitFinishedWithError( plainTextStore.error(), plainTextStore.errorString() ); - else - q->emitFinished(); -} - -void JobPrivate::gnomeKeyring_writeCb(int result, JobPrivate* self ) -{ - if ( result == GnomeKeyring::RESULT_OK ) { - self->q->emitFinished(); - } else { - const QPair errorResult = mapGnomeKeyringError( result ); - self->q->emitFinishedWithError( errorResult.first, errorResult.second ); - } -} - -void JobPrivate::kwalletOpenFinished( QDBusPendingCallWatcher* watcher ) { - watcher->deleteLater(); - QDBusPendingReply reply = *watcher; - - if ( reply.isError() ) { - fallbackOnError( reply.error() ); - return; - } - - PlainTextStore plainTextStore( q->service(), q->settings() ); - if ( plainTextStore.contains( key ) ) { - // If we had previously written to QSettings, but we now have a kwallet available, migrate and delete old insecure data - plainTextStore.remove( key ); - } - - const int handle = reply.value(); - - if ( handle < 0 ) { - q->emitFinishedWithError( AccessDenied, tr("Access to keychain denied") ); - return; - } - - QDBusPendingReply nextReply; - - if ( mode == Text ) - nextReply = iface->writePassword( handle, q->service(), key, QString::fromUtf8(data), q->service() ); - else if ( mode == Binary ) - nextReply = iface->writeEntry( handle, q->service(), key, data, q->service() ); - else - nextReply = iface->removeEntry( handle, q->service(), key, q->service() ); - - QDBusPendingCallWatcher* nextWatcher = new QDBusPendingCallWatcher( nextReply, this ); - connect( nextWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(kwalletFinished(QDBusPendingCallWatcher*)) ); -} - -void JobPrivate::kwalletFinished( QDBusPendingCallWatcher* watcher ) { - if ( !watcher->isError() ) { - if ( mode == Binary ) { - QDBusPendingReply reply = *watcher; - if (reply.isValid()) { - data = reply.value(); - } - } else { - QDBusPendingReply reply = *watcher; - if (reply.isValid()) { - data = reply.value().toUtf8(); - } - } - } - - q->emitFinished(); -} - -void DeletePasswordJobPrivate::scheduledStart() { - switch ( getKeyringBackend() ) { - case Backend_LibSecretKeyring: { - if ( !LibSecretKeyring::deletePassword(key, q->service(), this) ) { - q->emitFinishedWithError( OtherError, tr("Unknown error") ); - } - } break; - case Backend_GnomeKeyring: { - if ( !GnomeKeyring::delete_network_password( - key.toUtf8().constData(), q->service().toUtf8().constData(), - reinterpret_cast( &JobPrivate::gnomeKeyring_writeCb ), - this, 0 ) ) - q->emitFinishedWithError( OtherError, tr("Unknown error") ); - } - break; - - case Backend_Kwallet4: - kwalletWritePasswordScheduledStart("org.kde.kwalletd", "/modules/kwalletd", this); - break; - case Backend_Kwallet5: - kwalletWritePasswordScheduledStart("org.kde.kwalletd5", "/modules/kwalletd5", this); - break; - } -} - -void DeletePasswordJobPrivate::fallbackOnError(const QDBusError &err) { - QScopedPointer local( !q->settings() ? new QSettings( q->service() ) : 0 ); - QSettings* actual = q->settings() ? q->settings() : local.data(); - - if ( !q->insecureFallback() ) { - q->emitFinishedWithError( OtherError, tr("Could not open wallet: %1; %2") - .arg( QDBusError::errorString( err.type() ), err.message() ) ); - return; - } - - actual->remove( key ); - actual->sync(); - - q->emitFinished(); - - - q->emitFinished(); -} diff --git a/keychain_win.cpp b/keychain_win.cpp deleted file mode 100644 index 2366b472..00000000 --- a/keychain_win.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2011-2015 Frank Osterfeld * - * * - * 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. For licensing and distribution * - * details, check the accompanying file 'COPYING'. * - *****************************************************************************/ -#include "keychain_p.h" -#include "plaintextstore_p.h" - -#include -#include - -#include - -using namespace QKeychain; - -#if defined(USE_CREDENTIAL_STORE) -#include - -void ReadPasswordJobPrivate::scheduledStart() { - LPCWSTR name = (LPCWSTR)key.utf16(); - PCREDENTIALW cred; - - if (!CredReadW(name, CRED_TYPE_GENERIC, 0, &cred)) { - Error error; - QString msg; - switch(GetLastError()) { - case ERROR_NOT_FOUND: - error = EntryNotFound; - msg = tr("Password entry not found"); - break; - default: - error = OtherError; - msg = tr("Could not decrypt data"); - break; - } - - q->emitFinishedWithError( error, msg ); - return; - } - - data = QByteArray((char*)cred->CredentialBlob, cred->CredentialBlobSize); - CredFree(cred); - - q->emitFinished(); -} - -void WritePasswordJobPrivate::scheduledStart() { - CREDENTIALW cred; - char *pwd = data.data(); - LPWSTR name = (LPWSTR)key.utf16(); - - memset(&cred, 0, sizeof(cred)); - cred.Comment = const_cast(L"QtKeychain"); - cred.Type = CRED_TYPE_GENERIC; - cred.TargetName = name; - cred.CredentialBlobSize = data.size(); - cred.CredentialBlob = (LPBYTE)pwd; - cred.Persist = CRED_PERSIST_ENTERPRISE; - - if (CredWriteW(&cred, 0)) { - q->emitFinished(); - return; - } - - DWORD err = GetLastError(); - - // Detect size-exceeded errors and provide nicer messages. - // Unfortunately these error codes aren't documented. - // Found empirically on Win10 1803 build 17134.523. - if (err == RPC_X_BAD_STUB_DATA) { - const size_t maxBlob = CRED_MAX_CREDENTIAL_BLOB_SIZE; - if (cred.CredentialBlobSize > maxBlob) { - q->emitFinishedWithError( - OtherError, - tr("Credential size exceeds maximum size of %1").arg(maxBlob)); - return; - } - } - if (err == RPC_S_INVALID_BOUND) { - const size_t maxTargetName = CRED_MAX_GENERIC_TARGET_NAME_LENGTH; - if (key.size() > maxTargetName) { - q->emitFinishedWithError( - OtherError, - tr("Credential key exceeds maximum size of %1").arg(maxTargetName)); - return; - } - } - - q->emitFinishedWithError( OtherError, tr("Writing credentials failed: Win32 error code %1").arg(err) ); -} - -void DeletePasswordJobPrivate::scheduledStart() { - LPCWSTR name = (LPCWSTR)key.utf16(); - - if (!CredDeleteW(name, CRED_TYPE_GENERIC, 0)) { - Error error; - QString msg; - switch(GetLastError()) { - case ERROR_NOT_FOUND: - error = EntryNotFound; - msg = tr("Password entry not found"); - break; - default: - error = OtherError; - msg = tr("Could not decrypt data"); - break; - } - - q->emitFinishedWithError( error, msg ); - } else { - q->emitFinished(); - } -} -#else -void ReadPasswordJobPrivate::scheduledStart() { - PlainTextStore plainTextStore( q->service(), q->settings() ); - QByteArray encrypted = plainTextStore.readData( key ); - if ( plainTextStore.error() != NoError ) { - q->emitFinishedWithError( plainTextStore.error(), plainTextStore.errorString() ); - return; - } - - DATA_BLOB blob_in, blob_out; - - blob_in.pbData = reinterpret_cast( encrypted.data() ); - blob_in.cbData = encrypted.size(); - - const BOOL ret = CryptUnprotectData( &blob_in, - NULL, - NULL, - NULL, - NULL, - 0, - &blob_out ); - if ( !ret ) { - q->emitFinishedWithError( OtherError, tr("Could not decrypt data") ); - return; - } - - data = QByteArray( reinterpret_cast( blob_out.pbData ), blob_out.cbData ); - SecureZeroMemory( blob_out.pbData, blob_out.cbData ); - LocalFree( blob_out.pbData ); - - q->emitFinished(); -} - -void WritePasswordJobPrivate::scheduledStart() { - DATA_BLOB blob_in, blob_out; - blob_in.pbData = reinterpret_cast( data.data() ); - blob_in.cbData = data.size(); - const BOOL res = CryptProtectData( &blob_in, - L"QKeychain-encrypted data", - NULL, - NULL, - NULL, - 0, - &blob_out ); - if ( !res ) { - q->emitFinishedWithError( OtherError, tr("Encryption failed") ); //TODO more details available? - return; - } - - const QByteArray encrypted( reinterpret_cast( blob_out.pbData ), blob_out.cbData ); - LocalFree( blob_out.pbData ); - - PlainTextStore plainTextStore( q->service(), q->settings() ); - plainTextStore.write( key, encrypted, Binary ); - if ( plainTextStore.error() != NoError ) { - q->emitFinishedWithError( plainTextStore.error(), plainTextStore.errorString() ); - return; - } - - q->emitFinished(); -} - -void DeletePasswordJobPrivate::scheduledStart() { - PlainTextStore plainTextStore( q->service(), q->settings() ); - plainTextStore.remove( key ); - if ( plainTextStore.error() != NoError ) { - q->emitFinishedWithError( plainTextStore.error(), plainTextStore.errorString() ); - } else { - q->emitFinished(); - } -} -#endif diff --git a/libsecret.cpp b/libsecret.cpp deleted file mode 100644 index aed91a07..00000000 --- a/libsecret.cpp +++ /dev/null @@ -1,336 +0,0 @@ -#if defined(HAVE_LIBSECRET) -#include -#endif - -#include "libsecret_p.h" - -#include -#include - -#if defined(HAVE_LIBSECRET) -const SecretSchema* qtkeychainSchema(void) { - static const SecretSchema schema = { - "org.qt.keychain", SECRET_SCHEMA_DONT_MATCH_NAME, - { - { "user", SECRET_SCHEMA_ATTRIBUTE_STRING }, - { "server", SECRET_SCHEMA_ATTRIBUTE_STRING }, - { "type", SECRET_SCHEMA_ATTRIBUTE_STRING } - } - }; - - return &schema; -} - -typedef struct { - QKeychain::JobPrivate *self; - QString user; - QString server; -} callbackArg; - -typedef void (*secret_password_lookup_t) (const SecretSchema *schema, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data, - ...) G_GNUC_NULL_TERMINATED; -typedef gchar *(*secret_password_lookup_finish_t) (GAsyncResult *result, - GError **error); -typedef void (*secret_password_store_t) (const SecretSchema *schema, - const gchar *collection, - const gchar *label, - const gchar *password, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data, - ...) G_GNUC_NULL_TERMINATED; -typedef gboolean (*secret_password_store_finish_t) (GAsyncResult *result, - GError **error); -typedef void (*secret_password_clear_t) (const SecretSchema *schema, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data, - ...) G_GNUC_NULL_TERMINATED; -typedef gboolean (*secret_password_clear_finish_t) (GAsyncResult *result, - GError **error); -typedef void (*secret_password_free_t) (gchar *password); -typedef GQuark (*secret_error_get_quark_t) (void) G_GNUC_CONST; - -static secret_password_lookup_t secret_password_lookup_fn = NULL; -static secret_password_lookup_finish_t secret_password_lookup_finish_fn = NULL; -static secret_password_store_t secret_password_store_fn = NULL; -static secret_password_store_finish_t secret_password_store_finish_fn = NULL; -static secret_password_clear_t secret_password_clear_fn = NULL; -static secret_password_clear_finish_t secret_password_clear_finish_fn = NULL; -static secret_password_free_t secret_password_free_fn = NULL; -static secret_error_get_quark_t secret_error_get_quark_fn = NULL; - -static QKeychain::Error gerrorToCode(const GError *error) { - if (error->domain != secret_error_get_quark_fn()) { - return QKeychain::OtherError; - } - - switch(error->code) { - case SECRET_ERROR_NO_SUCH_OBJECT: - return QKeychain::EntryNotFound; - case SECRET_ERROR_IS_LOCKED: - return QKeychain::AccessDenied; - default: - return QKeychain::OtherError; - } -} - -static void -on_password_lookup (GObject *source, - GAsyncResult *result, - gpointer inst) -{ - GError *error = NULL; - callbackArg *arg = (callbackArg*)inst; - gchar *password = secret_password_lookup_finish_fn (result, &error); - - Q_UNUSED(source); - - if (arg) { - if (error) { - QKeychain::Error code = gerrorToCode(error); - - arg->self->q->emitFinishedWithError( code, QString::fromUtf8(error->message) ); - } else { - if (password != NULL) { - QByteArray raw = QByteArray(password); - switch(arg->self->mode) { - case QKeychain::JobPrivate::Binary: - arg->self->data = QByteArray::fromBase64(raw); - break; - case QKeychain::JobPrivate::Text: - default: - arg->self->data = raw; - } - - arg->self->q->emitFinished(); - } else if (arg->self->mode == QKeychain::JobPrivate::Text) { - arg->self->mode = QKeychain::JobPrivate::Binary; - secret_password_lookup_fn (qtkeychainSchema(), NULL, - on_password_lookup, arg, - "user", arg->user.toUtf8().constData(), - "server", arg->server.toUtf8().constData(), - "type", "base64", - NULL); - return; - } else { - arg->self->q->emitFinishedWithError( QKeychain::EntryNotFound, QObject::tr("Entry not found") ); - } - } - } - if (error) { - g_error_free (error); - } - - if (password) { - secret_password_free_fn (password); - } - - if (arg) { - delete arg; - } -} - -static void -on_password_stored (GObject *source, - GAsyncResult *result, - gpointer inst) -{ - GError *error = NULL; - QKeychain::JobPrivate *self = (QKeychain::JobPrivate*)inst; - - Q_UNUSED(source); - - secret_password_store_finish_fn (result, &error); - - if (self) { - if (error != NULL) { - self->q->emitFinishedWithError( gerrorToCode(error), - QString::fromUtf8(error->message) ); - } else { - self->q->emitFinished(); - } - } - if (error != NULL) { - g_error_free (error); - } -} - -static void -on_password_cleared (GObject *source, - GAsyncResult *result, - gpointer inst) -{ - GError *error = NULL; - QKeychain::JobPrivate *self = (QKeychain::JobPrivate*)inst; - gboolean removed = secret_password_clear_finish_fn (result, &error); - - Q_UNUSED(source); - if (self) { - if ( error ) { - self->q->emitFinishedWithError( gerrorToCode(error), - QString::fromUtf8(error->message) ); - } else { - Q_UNUSED(removed); - self->q->emitFinished(); - } - } - if (error != NULL) { - g_error_free (error); - } -} - -static QString modeToString(QKeychain::JobPrivate::Mode mode) { - switch(mode) { - case QKeychain::JobPrivate::Binary: - return "base64"; - default: - return "plaintext"; - } -} -#endif - -bool LibSecretKeyring::isAvailable() { -#if defined(HAVE_LIBSECRET) - const LibSecretKeyring& keyring = instance(); - if (!keyring.isLoaded()) - return false; - if (secret_password_lookup_fn == NULL) - return false; - if (secret_password_lookup_finish_fn == NULL) - return false; - if (secret_password_store_fn == NULL) - return false; - if (secret_password_store_finish_fn == NULL) - return false; - if (secret_password_clear_fn == NULL) - return false; - if (secret_password_clear_finish_fn == NULL) - return false; - if (secret_password_free_fn == NULL) - return false; - if (secret_error_get_quark_fn == NULL) - return false; - return true; -#else - return false; -#endif -} - -bool LibSecretKeyring::findPassword(const QString &user, const QString &server, - QKeychain::JobPrivate *self) -{ -#if defined(HAVE_LIBSECRET) - if (!isAvailable()) { - return false; - } - - self->mode = QKeychain::JobPrivate::Text; - self->data = QByteArray(); - - callbackArg *arg = new callbackArg; - arg->self = self; - arg->user = user; - arg->server = server; - - qDebug() << Q_FUNC_INFO; - secret_password_lookup_fn (qtkeychainSchema(), NULL, on_password_lookup, arg, - "user", user.toUtf8().constData(), - "server", server.toUtf8().constData(), - "type", "plaintext", - NULL); - return true; -#else - return false; -#endif -} - -bool LibSecretKeyring::writePassword(const QString &display_name, - const QString &user, - const QString &server, - const QKeychain::JobPrivate::Mode mode, - const QByteArray &password, - QKeychain::JobPrivate *self) -{ -#if defined(HAVE_LIBSECRET) - if (!isAvailable()) { - return false; - } - - QString type = modeToString(mode); - QByteArray pwd; - switch(mode) { - case QKeychain::JobPrivate::Binary: - pwd = password.toBase64(); - break; - default: - pwd = password; - break; - } - - qDebug() << Q_FUNC_INFO; - secret_password_store_fn (qtkeychainSchema(), SECRET_COLLECTION_DEFAULT, - display_name.toUtf8().constData(), - pwd.constData(), NULL, on_password_stored, self, - "user", user.toUtf8().constData(), - "server", server.toUtf8().constData(), - "type", type.toUtf8().constData(), - NULL); - return true; -#else - return false; -#endif -} - -bool LibSecretKeyring::deletePassword(const QString &key, const QString &service, - QKeychain::JobPrivate* self) -{ -#if defined(HAVE_LIBSECRET) - if (!isAvailable()) { - return false; - } - - qDebug() << Q_FUNC_INFO; - secret_password_clear_fn (qtkeychainSchema(), NULL, on_password_cleared, self, - "user", key.toUtf8().constData(), - "server", service.toUtf8().constData(), - NULL); - return true; -#else - return false; -#endif -} - -LibSecretKeyring::LibSecretKeyring() - : QLibrary(QStringLiteral("secret-1"), 0) -{ -#ifdef HAVE_LIBSECRET - if (load()) { - secret_password_lookup_fn = - (secret_password_lookup_t)resolve("secret_password_lookup"); - secret_password_lookup_finish_fn = - (secret_password_lookup_finish_t)resolve("secret_password_lookup_finish"); - secret_password_store_fn = - (secret_password_store_t)resolve("secret_password_store"); - secret_password_store_finish_fn = - (secret_password_store_finish_t)resolve("secret_password_store_finish"); - secret_password_clear_fn = - (secret_password_clear_t)resolve("secret_password_clear"); - secret_password_clear_finish_fn = - (secret_password_clear_finish_t)resolve("secret_password_clear_finish"); - secret_password_free_fn = - (secret_password_free_t)resolve("secret_password_free"); - secret_error_get_quark_fn = - (secret_error_get_quark_t)resolve("secret_error_get_quark"); - } -#endif -} - -LibSecretKeyring &LibSecretKeyring::instance() { - static LibSecretKeyring instance; - - return instance; -} diff --git a/libsecret_p.h b/libsecret_p.h deleted file mode 100644 index bd966fe0..00000000 --- a/libsecret_p.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef QTKEYCHAIN_LIBSECRET_P_H -#define QTKEYCHAIN_LIBSECRET_P_H - -#include - -#include "keychain_p.h" - -class LibSecretKeyring : public QLibrary { -public: - static bool isAvailable(); - - static bool findPassword(const QString& user, - const QString& server, - QKeychain::JobPrivate* self); - - static bool writePassword(const QString& display_name, - const QString& user, - const QString& server, - const QKeychain::JobPrivate::Mode type, - const QByteArray& password, - QKeychain::JobPrivate* self); - - static bool deletePassword(const QString &key, const QString &service, - QKeychain::JobPrivate* self); - -private: - LibSecretKeyring(); - - static LibSecretKeyring &instance(); -}; - - -#endif diff --git a/qt5keychain.pri b/qt5keychain.pri deleted file mode 100644 index ee6d4e88..00000000 --- a/qt5keychain.pri +++ /dev/null @@ -1,83 +0,0 @@ -# Minimal qmake support. -# This file is provided as is without any warranty. -# It can break at anytime or be removed without notice. - -QT5KEYCHAIN_PWD = $$PWD - -CONFIG += depend_includepath -DEFINES += QTKEYCHAIN_NO_EXPORT - -INCLUDEPATH += \ - $$PWD/.. \ - $$QT5KEYCHAIN_PWD - -HEADERS += \ - $$QT5KEYCHAIN_PWD/keychain_p.h \ - $$QT5KEYCHAIN_PWD/keychain.h - -SOURCES += \ - $$QT5KEYCHAIN_PWD/keychain.cpp - -unix:!macx:!ios { - # Remove the following LIBSECRET_SUPPORT line - # to build without libsecret support. - DEFINES += LIBSECRET_SUPPORT - contains(DEFINES, LIBSECRET_SUPPORT) { - packagesExist(libsecret-1) { - !build_pass:message("Libsecret support: on") - CONFIG += link_pkgconfig - PKGCONFIG += libsecret-1 - DEFINES += HAVE_LIBSECRET - } else { - !build_pass:warning("Libsecret not found.") - !build_pass:message("Libsecret support: off") - } - } else { - !build_pass:message("Libsecret support: off") - } - - # Generate D-Bus interface: - QT += dbus - kwallet_interface.files = $$PWD/org.kde.KWallet.xml - DBUS_INTERFACES += kwallet_interface - - HEADERS += \ - $$QT5KEYCHAIN_PWD/gnomekeyring_p.h \ - $$QT5KEYCHAIN_PWD/plaintextstore_p.h \ - $$QT5KEYCHAIN_PWD/libsecret_p.h - SOURCES += \ - $$QT5KEYCHAIN_PWD/keychain_unix.cpp \ - $$QT5KEYCHAIN_PWD/plaintextstore.cpp \ - $$QT5KEYCHAIN_PWD/gnomekeyring.cpp \ - $$QT5KEYCHAIN_PWD/libsecret.cpp -} - -win32 { - # Remove the following USE_CREDENTIAL_STORE line - # to use the CryptProtectData Windows API function - # instead of the Windows Credential Store. - DEFINES += USE_CREDENTIAL_STORE - contains(DEFINES, USE_CREDENTIAL_STORE) { - !build_pass:message("Windows Credential Store support: on") - LIBS += -lAdvapi32 - } else { - !build_pass:message("Windows Credential Store support: off") - LIBS += -lCrypt32 - HEADERS += $$QT5KEYCHAIN_PWD/plaintextstore_p.h - SOURCES += $$QT5KEYCHAIN_PWD/plaintextstore.cpp - } - HEADERS += $$QT5KEYCHAIN_PWD/libsecret_p.h - SOURCES += \ - $$QT5KEYCHAIN_PWD/keychain_win.cpp \ - $$QT5KEYCHAIN_PWD/libsecret.cpp -} - -macx:!ios { - LIBS += "-framework Security" "-framework Foundation" - SOURCES += $$QT5KEYCHAIN_PWD/keychain_mac.cpp -} - -ios { - LIBS += "-framework Security" "-framework Foundation" - OBJECTIVE_SOURCES += $$QT5KEYCHAIN_PWD/keychain_ios.mm -} diff --git a/qtkeychain.pri b/qtkeychain.pri new file mode 100644 index 00000000..caf434e2 --- /dev/null +++ b/qtkeychain.pri @@ -0,0 +1,97 @@ +# Minimal qmake support. +# This file is provided as is without any warranty. +# It can break at anytime or be removed without notice. + +lessThan(QT_MAJOR_VERSION, 5) { + error("qtkeychain requires Qt 5 or later") +} + +QTKEYCHAIN_PWD = $$PWD/qtkeychain + +CONFIG += depend_includepath +DEFINES += QTKEYCHAIN_NO_EXPORT + +INCLUDEPATH += \ + $$PWD/.. \ + $$QTKEYCHAIN_PWD + +HEADERS += \ + $$QTKEYCHAIN_PWD/keychain_p.h \ + $$QTKEYCHAIN_PWD/keychain.h + +SOURCES += \ + $$QTKEYCHAIN_PWD/keychain.cpp + +unix:!android:!macx:!ios { + # Remove the following LIBSECRET_SUPPORT line + # to build without libsecret support. + DEFINES += LIBSECRET_SUPPORT + contains(DEFINES, LIBSECRET_SUPPORT) { + packagesExist(libsecret-1) { + !build_pass:message("Libsecret support: on") + CONFIG += link_pkgconfig + PKGCONFIG += libsecret-1 + DEFINES += HAVE_LIBSECRET + } else { + !build_pass:warning("Libsecret not found.") + !build_pass:message("Libsecret support: off") + } + } else { + !build_pass:message("Libsecret support: off") + } + + # Generate D-Bus interface: + DEFINES += KEYCHAIN_DBUS + QT += dbus + kwallet_interface.files = $$QTKEYCHAIN_PWD/org.kde.KWallet.xml + DBUS_INTERFACES += kwallet_interface + + HEADERS += \ + $$QTKEYCHAIN_PWD/gnomekeyring_p.h \ + $$QTKEYCHAIN_PWD/plaintextstore_p.h \ + $$QTKEYCHAIN_PWD/libsecret_p.h + SOURCES += \ + $$QTKEYCHAIN_PWD/keychain_unix.cpp \ + $$QTKEYCHAIN_PWD/plaintextstore.cpp \ + $$QTKEYCHAIN_PWD/gnomekeyring.cpp \ + $$QTKEYCHAIN_PWD/libsecret.cpp +} + +android { + lessThan(QT_MAJOR_VERSION, 6) { + QT += androidextras + } + + HEADERS += \ + $$QTKEYCHAIN_PWD/androidkeystore_p.h \ + $$QTKEYCHAIN_PWD/plaintextstore_p.h + SOURCES += \ + $$QTKEYCHAIN_PWD/androidkeystore.cpp \ + $$QTKEYCHAIN_PWD/keychain_android.cpp \ + $$QTKEYCHAIN_PWD/plaintextstore.cpp +} + +win32 { + # Remove the following USE_CREDENTIAL_STORE line + # to use the CryptProtectData Windows API function + # instead of the Windows Credential Store. + DEFINES += USE_CREDENTIAL_STORE + contains(DEFINES, USE_CREDENTIAL_STORE) { + !build_pass:message("Windows Credential Store support: on") + LIBS += -ladvapi32 -lcrypt32 + } else { + !build_pass:message("Windows Credential Store support: off") + LIBS += -lcrypt32 + HEADERS += $$QTKEYCHAIN_PWD/plaintextstore_p.h + SOURCES += $$QTKEYCHAIN_PWD/plaintextstore.cpp + } + HEADERS += $$QTKEYCHAIN_PWD/libsecret_p.h + SOURCES += \ + $$QTKEYCHAIN_PWD/keychain_win.cpp \ + $$QTKEYCHAIN_PWD/libsecret.cpp +} + +macx|ios { + LIBS += -framework Security -framework Foundation + OBJECTIVE_SOURCES += $$QTKEYCHAIN_PWD/keychain_apple.mm +} diff --git a/qtkeychain/CMakeLists.txt b/qtkeychain/CMakeLists.txt new file mode 100644 index 00000000..8534b6c1 --- /dev/null +++ b/qtkeychain/CMakeLists.txt @@ -0,0 +1,118 @@ + +list(APPEND qtkeychain_LIBRARIES ${QTCORE_LIBRARIES}) +set(qtkeychain_SOURCES + keychain.cpp + qkeychain_export.h + keychain.h +) + +if(MSVC) + # CMake < 3.15 sneaks in /W# flags for us, so we need a replacement, + # or we'll get a warning (cf. CMP0092) + if (CMAKE_CXX_FLAGS MATCHES "/W[0-4]") + string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") + endif() +else() + # MSVC's STL / Qt headers are not MSVC -Wall clean, so don't enable it there + add_definitions( -Wall -Werror=return-type ) +endif() + +if(WIN32) + list(APPEND qtkeychain_SOURCES keychain_win.cpp) + list(APPEND qtkeychain_LIBRARIES crypt32) + if (NOT USE_CREDENTIAL_STORE) + list(APPEND qtkeychain_SOURCES plaintextstore.cpp) + endif() + #FIXME: mingw bug; otherwise getting undefined refs to RtlSecureZeroMemory there + if(MINGW) + add_definitions( -O2 ) + endif() + + set(CMAKE_CXX_STANDARD 17) + add_definitions(-DUNICODE) + if (MSVC) + add_definitions(/utf-8) + elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + add_definitions(-finput-charset=UTF-8 -fexec-charset=UTF-8) + endif() +endif() + +if(APPLE) + list(APPEND qtkeychain_SOURCES keychain_apple.mm) + list(APPEND qtkeychain_LIBRARIES "-framework Foundation" "-framework Security") +endif() + +if(HAIKU) + list(APPEND qtkeychain_SOURCES keychain_haiku.cpp) + + find_library(BE_LIBRARY be REQUIRED) + list(APPEND qtkeychain_LIBRARIES ${BE_LIBRARY}) +endif() + +if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT HAIKU AND NOT EMSCRIPTEN) + option(LIBSECRET_SUPPORT "Build with libsecret support" ON) + + if(LIBSECRET_SUPPORT) + pkg_check_modules(LIBSECRET REQUIRED libsecret-1) + add_definitions(-DHAVE_LIBSECRET=1) + INCLUDE_DIRECTORIES(${LIBSECRET_INCLUDE_DIRS}) + LINK_DIRECTORIES(${LIBSECRET_LIBRARY_DIRS}) + list(APPEND qtkeychain_LIBRARIES_PRIVATE ${LIBSECRET_LIBRARIES}) + endif() + + add_definitions(-DKEYCHAIN_DBUS=1) + list(APPEND qtkeychain_SOURCES keychain_unix.cpp gnomekeyring.cpp libsecret.cpp plaintextstore.cpp) + qt_add_dbus_interface(qtkeychain_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.KWallet.xml kwallet_interface KWalletInterface) + list(APPEND qtkeychain_LIBRARIES ${QTDBUS_LIBRARIES} ) +endif() + +if(ANDROID) + list(APPEND qtkeychain_SOURCES keychain_android.cpp androidkeystore.cpp plaintextstore.cpp) + list(APPEND qtkeychain_LIBRARIES_PRIVATE ${QTANDROIDEXTRAS_LIBRARIES} ) +endif() + +QT_WRAP_CPP(qtkeychain_MOC_OUTFILES keychain.h keychain_p.h gnomekeyring_p.h) + +add_library(${QTKEYCHAIN_TARGET_NAME} ${qtkeychain_SOURCES} ${qtkeychain_MOC_OUTFILES} ${qtkeychain_QM_FILES}) +if(WIN32) + set_target_properties( ${QTKEYCHAIN_TARGET_NAME} PROPERTIES DEBUG_POSTFIX "d" ) +endif() + +target_link_libraries(${QTKEYCHAIN_TARGET_NAME} PUBLIC ${qtkeychain_LIBRARIES} PRIVATE ${qtkeychain_LIBRARIES_PRIVATE}) +if(NOT INTERFACE_INCLUDE_SUFFIX) + set(INTERFACE_INCLUDE_SUFFIX include) +endif() +# Where to find includes when building the library and using it uninstalled: in the parent dir, so that the code has to use +target_include_directories(${QTKEYCHAIN_TARGET_NAME} PUBLIC $ $) +# Where to find includes when using the installed qtkeychain +target_include_directories(${QTKEYCHAIN_TARGET_NAME} INTERFACE $) + +generate_export_header(${QTKEYCHAIN_TARGET_NAME} + EXPORT_FILE_NAME qkeychain_export.h + EXPORT_MACRO_NAME QKEYCHAIN_EXPORT +) + +set_target_properties(${QTKEYCHAIN_TARGET_NAME} PROPERTIES + VERSION ${QTKEYCHAIN_VERSION} + SOVERSION ${QTKEYCHAIN_SOVERSION} + INSTALL_RPATH_USE_LINK_PATH TRUE +) + +if (NOT APPLE) + set_target_properties(${QTKEYCHAIN_TARGET_NAME} PROPERTIES + INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}" + ) +endif() + +install(FILES keychain.h ${CMAKE_CURRENT_BINARY_DIR}/qkeychain_export.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/qt${QTKEYCHAIN_VERSION_INFIX}keychain/ +) + +install(TARGETS ${QTKEYCHAIN_TARGET_NAME} + EXPORT Qt${QTKEYCHAIN_VERSION_INFIX}KeychainLibraryDepends + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +) diff --git a/qtkeychain/androidkeystore.cpp b/qtkeychain/androidkeystore.cpp new file mode 100644 index 00000000..c831b3bc --- /dev/null +++ b/qtkeychain/androidkeystore.cpp @@ -0,0 +1,312 @@ +#include "androidkeystore_p.h" + +#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) +# include "private/qjni_p.h" +#endif + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +# include +#endif + +using namespace QKeychain; + +using namespace android::content; +using namespace android::security; + +using namespace java::io; +using namespace java::lang; +using namespace java::math; +using namespace java::util; +using namespace java::security; +using namespace java::security::spec; + +using namespace javax::crypto; +using namespace javax::security::auth::x500; +using namespace javax::security::cert; + +const BigInteger BigInteger::ONE = + BigInteger::getStaticObjectField("java/math/BigInteger", "ONE", "Ljava/math/BigInteger;"); + +const int Calendar::YEAR = Calendar::getStaticField("java/util/Calendar", "YEAR"); + +const int Cipher::DECRYPT_MODE = + Cipher::getStaticField("javax/crypto/Cipher", "DECRYPT_MODE"); +const int Cipher::ENCRYPT_MODE = + Cipher::getStaticField("javax/crypto/Cipher", "ENCRYPT_MODE"); + +namespace { + +#if QT_VERSION < QT_VERSION_CHECK(5, 7, 0) + +struct JNIObject +{ + JNIObject(QSharedPointer d) : d(d) { } + + static JNIObject fromLocalRef(jobject o) + { + return JNIObject( + QSharedPointer::create(QJNIObjectPrivate::fromLocalRef(o))); + } + + jobject object() const { return d->object(); } + QSharedPointer d; +}; + +#else + +using JNIObject = QAndroidJniObject; + +#endif + +QByteArray fromArray(const jbyteArray array) +{ + QAndroidJniEnvironment env; + jbyte *const bytes = env->GetByteArrayElements(array, nullptr); + const QByteArray result(reinterpret_cast(bytes), env->GetArrayLength(array)); + env->ReleaseByteArrayElements(array, bytes, JNI_ABORT); + return result; +} + +JNIObject toArray(const QByteArray &bytes) +{ + QAndroidJniEnvironment env; + const int length = bytes.length(); + JNIObject array = JNIObject::fromLocalRef(env->NewByteArray(length)); + env->SetByteArrayRegion(static_cast(array.object()), 0, length, + reinterpret_cast(bytes.constData())); + return array; +} + +} // namespace + +bool Object::handleExceptions() +{ + QAndroidJniEnvironment env; + + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return false; + } + + return true; +} + +KeyPairGenerator KeyPairGenerator::getInstance(const QString &algorithm, const QString &provider) +{ + return handleExceptions(callStaticObjectMethod( + "java/security/KeyPairGenerator", "getInstance", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/security/KeyPairGenerator;", + fromString(algorithm).object(), fromString(provider).object())); +} + +KeyPair KeyPairGenerator::generateKeyPair() const +{ + return handleExceptions(callObjectMethod("generateKeyPair", "()Ljava/security/KeyPair;")); +} + +bool KeyPairGenerator::initialize(const AlgorithmParameterSpec &spec) const +{ + callMethod("initialize", "(Ljava/security/spec/AlgorithmParameterSpec;)V", spec.object()); + return handleExceptions(); +} + +bool KeyStore::containsAlias(const QString &alias) const +{ + return handleExceptions(callMethod("containsAlias", "(Ljava/lang/String;)Z", + fromString(alias).object())); +} + +bool KeyStore::deleteEntry(const QString &alias) const +{ + callMethod("deleteEntry", "(Ljava/lang/String;)V", fromString(alias).object()); + return handleExceptions(); +} + +KeyStore KeyStore::getInstance(const QString &type) +{ + return handleExceptions(callStaticObjectMethod("java/security/KeyStore", "getInstance", + "(Ljava/lang/String;)Ljava/security/KeyStore;", + fromString(type).object())); +} + +KeyStore::Entry KeyStore::getEntry(const QString &alias, + const KeyStore::ProtectionParameter ¶m) const +{ + return handleExceptions( + callObjectMethod("getEntry", + "(Ljava/lang/String;Ljava/security/" + "KeyStore$ProtectionParameter;)Ljava/security/KeyStore$Entry;", + fromString(alias).object(), param.object())); +} + +bool KeyStore::load(const KeyStore::LoadStoreParameter ¶m) const +{ + callMethod("load", "(Ljava/security/KeyStore$LoadStoreParameter;)V", param.object()); + return handleExceptions(); +} + +Calendar Calendar::getInstance() +{ + return handleExceptions( + callStaticObjectMethod("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;")); +} + +bool Calendar::add(int field, int amount) const +{ + callMethod("add", "(II)V", field, amount); + return handleExceptions(); +} + +Date Calendar::getTime() const +{ + return handleExceptions(callObjectMethod("getTime", "()Ljava/util/Date;")); +} + +KeyPairGeneratorSpec::Builder::Builder(const Context &context) + : Object(QAndroidJniObject("android/security/KeyPairGeneratorSpec$Builder", + "(Landroid/content/Context;)V", context.object())) +{ + handleExceptions(); +} + +KeyPairGeneratorSpec::Builder KeyPairGeneratorSpec::Builder::setAlias(const QString &alias) const +{ + return handleExceptions(callObjectMethod( + "setAlias", "(Ljava/lang/String;)Landroid/security/KeyPairGeneratorSpec$Builder;", + fromString(alias).object())); +} + +KeyPairGeneratorSpec::Builder +KeyPairGeneratorSpec::Builder::setSubject(const X500Principal &subject) const +{ + return handleExceptions(callObjectMethod("setSubject", + "(Ljavax/security/auth/x500/X500Principal;)Landroid/" + "security/KeyPairGeneratorSpec$Builder;", + subject.object())); +} + +KeyPairGeneratorSpec::Builder +KeyPairGeneratorSpec::Builder::setSerialNumber(const BigInteger &serial) const +{ + return handleExceptions(callObjectMethod( + "setSerialNumber", + "(Ljava/math/BigInteger;)Landroid/security/KeyPairGeneratorSpec$Builder;", + serial.object())); +} + +KeyPairGeneratorSpec::Builder KeyPairGeneratorSpec::Builder::setStartDate(const Date &date) const +{ + return handleExceptions(callObjectMethod( + "setStartDate", "(Ljava/util/Date;)Landroid/security/KeyPairGeneratorSpec$Builder;", + date.object())); +} + +KeyPairGeneratorSpec::Builder KeyPairGeneratorSpec::Builder::setEndDate(const Date &date) const +{ + return handleExceptions(callObjectMethod( + "setEndDate", "(Ljava/util/Date;)Landroid/security/KeyPairGeneratorSpec$Builder;", + date.object())); +} + +KeyPairGeneratorSpec KeyPairGeneratorSpec::Builder::build() const +{ + return handleExceptions(callObjectMethod("build", "()Landroid/security/KeyPairGeneratorSpec;")); +} + +X500Principal::X500Principal(const QString &name) + : Object(QAndroidJniObject("javax/security/auth/x500/X500Principal", "(Ljava/lang/String;)V", + fromString(name).object())) +{ + handleExceptions(); +} + +Certificate KeyStore::PrivateKeyEntry::getCertificate() const +{ + return handleExceptions( + callObjectMethod("getCertificate", "()Ljava/security/cert/Certificate;")); +} + +PrivateKey KeyStore::PrivateKeyEntry::getPrivateKey() const +{ + return handleExceptions(callObjectMethod("getPrivateKey", "()Ljava/security/PrivateKey;")); +} + +PublicKey Certificate::getPublicKey() const +{ + return handleExceptions(callObjectMethod("getPublicKey", "()Ljava/security/PublicKey;")); +} + +ByteArrayInputStream::ByteArrayInputStream(const QByteArray &bytes) + : InputStream( + QAndroidJniObject("java/io/ByteArrayInputStream", "([B)V", toArray(bytes).object())) +{ +} + +ByteArrayOutputStream::ByteArrayOutputStream() + : OutputStream(QAndroidJniObject("java/io/ByteArrayOutputStream")) +{ + handleExceptions(); +} + +QByteArray ByteArrayOutputStream::toByteArray() const +{ + const QAndroidJniObject wrapper = callObjectMethod("toByteArray"); + + if (!handleExceptions()) + return QByteArray(); + + return fromArray(static_cast(wrapper.object())); +} + +int InputStream::read() const +{ + return handleExceptions(callMethod("read"), -1); +} + +bool OutputStream::write(const QByteArray &bytes) const +{ + callMethod("write", "([B)V", toArray(bytes).object()); + return handleExceptions(); +} + +bool OutputStream::close() const +{ + callMethod("close"); + return handleExceptions(); +} + +bool OutputStream::flush() const +{ + callMethod("flush"); + return handleExceptions(); +} + +Cipher Cipher::getInstance(const QString &transformation) +{ + return handleExceptions(callStaticObjectMethod("javax/crypto/Cipher", "getInstance", + "(Ljava/lang/String;)Ljavax/crypto/Cipher;", + fromString(transformation).object())); +} + +bool Cipher::init(int opMode, const Key &key) const +{ + callMethod("init", "(ILjava/security/Key;)V", opMode, key.object()); + return handleExceptions(); +} + +CipherOutputStream::CipherOutputStream(const OutputStream &stream, const Cipher &cipher) + : FilterOutputStream(QAndroidJniObject("javax/crypto/CipherOutputStream", + "(Ljava/io/OutputStream;Ljavax/crypto/Cipher;)V", + stream.object(), cipher.object())) +{ + handleExceptions(); +} + +CipherInputStream::CipherInputStream(const InputStream &stream, const Cipher &cipher) + : FilterInputStream(QAndroidJniObject("javax/crypto/CipherInputStream", + "(Ljava/io/InputStream;Ljavax/crypto/Cipher;)V", + stream.object(), cipher.object())) +{ + handleExceptions(); +} diff --git a/qtkeychain/androidkeystore_p.h b/qtkeychain/androidkeystore_p.h new file mode 100644 index 00000000..91eec253 --- /dev/null +++ b/qtkeychain/androidkeystore_p.h @@ -0,0 +1,386 @@ +/****************************************************************************** + * Copyright (C) 2016 Mathias Hasselmann * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ + +#ifndef QTKEYCHAIN_ANDROIDKEYSTORE_P_H +#define QTKEYCHAIN_ANDROIDKEYSTORE_P_H + +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +# include +#else +# include +# include + +typedef QJniObject QAndroidJniObject; +typedef QJniEnvironment QAndroidJniEnvironment; + +#endif + +namespace QKeychain { + +namespace javax { +namespace security { + +namespace auth { +namespace x500 { +class X500Principal; +} +} // namespace auth +namespace cert { +class Certificate; +} + +} // namespace security +} // namespace javax + +namespace java { +namespace lang { + +class Object : protected QAndroidJniObject +{ +public: + inline Object(jobject object) : QAndroidJniObject(object) { } + inline Object(const QAndroidJniObject &object) : QAndroidJniObject(object) { } + inline operator bool() const { return isValid(); } + + using QAndroidJniObject::object; + using QAndroidJniObject::toString; + +protected: + static bool handleExceptions(); + + template + static T handleExceptions(const T &result, const T &resultOnError = T()); +}; + +template +inline T Object::handleExceptions(const T &result, const T &resultOnError) +{ + if (!handleExceptions()) + return resultOnError; + + return result; +} + +} // namespace lang + +namespace io { + +class InputStream : public java::lang::Object +{ +public: + using Object::Object; + + int read() const; +}; + +class ByteArrayInputStream : public InputStream +{ +public: + using InputStream::InputStream; + + explicit ByteArrayInputStream(const QByteArray &bytes); +}; + +class FilterInputStream : public InputStream +{ +public: + using InputStream::InputStream; +}; + +class OutputStream : public java::lang::Object +{ +public: + using Object::Object; + + bool write(const QByteArray &bytes) const; + bool flush() const; + bool close() const; +}; + +class ByteArrayOutputStream : public OutputStream +{ +public: + using OutputStream::OutputStream; + + ByteArrayOutputStream(); + + QByteArray toByteArray() const; +}; + +class FilterOutputStream : public OutputStream +{ +public: + using OutputStream::OutputStream; +}; + +} // namespace io + +namespace math { + +class BigInteger : public java::lang::Object +{ +public: + using Object::Object; + + static const BigInteger ZERO; + static const BigInteger ONE; + static const BigInteger TEN; +}; + +} // namespace math + +namespace util { + +class Date : public java::lang::Object +{ +public: + using Object::Object; +}; + +class Calendar : public java::lang::Object +{ +public: + using Object::Object; + + static const int YEAR; + static const int MONTH; + static const int DAY; + static const int HOUR; + static const int MINUTE; + static const int SECOND; + static const int MILLISECOND; + + static Calendar getInstance(); + + bool add(int field, int amount) const; + Date getTime() const; +}; + +} // namespace util + +namespace security { +namespace spec { + +class AlgorithmParameterSpec : public java::lang::Object +{ +public: + using Object::Object; +}; + +} // namespace spec + +class Key : public java::lang::Object +{ +public: + using Object::Object; +}; + +class PrivateKey : public Key +{ +public: + using Key::Key; + + PrivateKey(const Key &init) : Key(init) { } +}; + +class PublicKey : public Key +{ +public: + using Key::Key; + + PublicKey(const Key &init) : Key(init) { } +}; + +class KeyPair : public java::lang::Object +{ +public: + using Object::Object; +}; + +class KeyPairGenerator : public java::lang::Object +{ +public: + using Object::Object; + + static KeyPairGenerator getInstance(const QString &algorithm, const QString &provider); + KeyPair generateKeyPair() const; + bool initialize(const spec::AlgorithmParameterSpec &spec) const; +}; + +class KeyStore : public java::lang::Object +{ +public: + class Entry : public java::lang::Object + { + public: + using Object::Object; + }; + + class PrivateKeyEntry : public Entry + { + public: + using Entry::Entry; + + inline PrivateKeyEntry(const Entry &init) : Entry(init) { } + + javax::security::cert::Certificate getCertificate() const; + java::security::PrivateKey getPrivateKey() const; + }; + + class LoadStoreParameter : public java::lang::Object + { + public: + using Object::Object; + }; + + class ProtectionParameter : public java::lang::Object + { + public: + using Object::Object; + }; + + using Object::Object; + + bool containsAlias(const QString &alias) const; + bool deleteEntry(const QString &alias) const; + static KeyStore getInstance(const QString &type); + Entry getEntry(const QString &alias, const ProtectionParameter ¶m = nullptr) const; + bool load(const LoadStoreParameter ¶m = nullptr) const; +}; + +namespace interfaces { + +class RSAPrivateKey : public PrivateKey +{ +public: + using PrivateKey::PrivateKey; + + RSAPrivateKey(const PrivateKey &init) : PrivateKey(init) { } +}; + +class RSAPublicKey : public PublicKey +{ +public: + using PublicKey::PublicKey; + + RSAPublicKey(const PublicKey &init) : PublicKey(init) { } +}; + +} // namespace interfaces + +} // namespace security +} // namespace java + +namespace android { +namespace content { + +class Context : public java::lang::Object +{ +public: + using Object::Object; +}; + +} // namespace content + +namespace security { + +class KeyPairGeneratorSpec : public java::security::spec::AlgorithmParameterSpec +{ +public: + class Builder : public java::lang::Object + { + public: + using Object::Object; + + explicit Builder(const android::content::Context &context); + + Builder setAlias(const QString &alias) const; + Builder setSubject(const javax::security::auth::x500::X500Principal &subject) const; + Builder setSerialNumber(const java::math::BigInteger &serial) const; + Builder setStartDate(const java::util::Date &date) const; + Builder setEndDate(const java::util::Date &date) const; + KeyPairGeneratorSpec build() const; + }; + + using AlgorithmParameterSpec::AlgorithmParameterSpec; +}; + +} // namespace security +} // namespace android + +namespace javax { +namespace crypto { + +class Cipher : public java::lang::Object +{ +public: + static const int DECRYPT_MODE; + static const int ENCRYPT_MODE; + + using Object::Object; + + static Cipher getInstance(const QString &transformation); + bool init(int opMode, const java::security::Key &key) const; +}; + +class CipherInputStream : public java::io::FilterInputStream +{ +public: + using FilterInputStream::FilterInputStream; + + explicit CipherInputStream(const InputStream &stream, const Cipher &cipher); +}; + +class CipherOutputStream : public java::io::FilterOutputStream +{ +public: + using FilterOutputStream::FilterOutputStream; + + explicit CipherOutputStream(const OutputStream &stream, const Cipher &cipher); +}; + +} // namespace crypto + +namespace security { +namespace auth { +namespace x500 { + +class X500Principal; + +class X500Principal : public java::lang::Object +{ +public: + using Object::Object; + + explicit X500Principal(const QString &name); +}; + +} // namespace x500 +} // namespace auth + +namespace cert { + +class Certificate : public java::lang::Object +{ +public: + using Object::Object; + + java::security::PublicKey getPublicKey() const; +}; + +} // namespace cert + +} // namespace security +} // namespace javax + +} // namespace QKeychain + +#endif // QTKEYCHAIN_ANDROIDKEYSTORE_P_H diff --git a/qtkeychain/gnomekeyring.cpp b/qtkeychain/gnomekeyring.cpp new file mode 100644 index 00000000..663d6d11 --- /dev/null +++ b/qtkeychain/gnomekeyring.cpp @@ -0,0 +1,71 @@ +#include "gnomekeyring_p.h" + +const char *GnomeKeyring::GNOME_KEYRING_DEFAULT = nullptr; + +bool GnomeKeyring::isAvailable() +{ + const GnomeKeyring &keyring = instance(); + return keyring.isLoaded() && keyring.NETWORK_PASSWORD && keyring.is_available + && keyring.find_password && keyring.store_password && keyring.delete_password + && keyring.is_available(); +} + +GnomeKeyring::gpointer +GnomeKeyring::store_network_password(const gchar *keyring, const gchar *display_name, + const gchar *user, const gchar *server, const gchar *type, + const gchar *password, OperationDoneCallback callback, + gpointer data, GDestroyNotify destroy_data) +{ + if (!isAvailable()) + return nullptr; + return instance().store_password(instance().NETWORK_PASSWORD, keyring, display_name, password, + callback, data, destroy_data, "user", user, "server", server, + "type", type, static_cast(nullptr)); +} + +GnomeKeyring::gpointer GnomeKeyring::find_network_password(const gchar *user, const gchar *server, + const gchar *type, + OperationGetStringCallback callback, + gpointer data, + GDestroyNotify destroy_data) +{ + if (!isAvailable()) + return nullptr; + + return instance().find_password(instance().NETWORK_PASSWORD, callback, data, destroy_data, + "user", user, "server", server, "type", type, + static_cast(0)); +} + +GnomeKeyring::gpointer GnomeKeyring::delete_network_password(const gchar *user, const gchar *server, + OperationDoneCallback callback, + gpointer data, + GDestroyNotify destroy_data) +{ + if (!isAvailable()) + return nullptr; + return instance().delete_password(instance().NETWORK_PASSWORD, callback, data, destroy_data, + "user", user, "server", server, static_cast(0)); +} + +GnomeKeyring::GnomeKeyring() : QLibrary(QLatin1String("gnome-keyring"), 0) +{ + static const PasswordSchema schema = { ITEM_NETWORK_PASSWORD, + { { "user", ATTRIBUTE_TYPE_STRING }, + { "server", ATTRIBUTE_TYPE_STRING }, + { "type", ATTRIBUTE_TYPE_STRING }, + { nullptr, static_cast(0) } } }; + + NETWORK_PASSWORD = &schema; + is_available = reinterpret_cast(resolve("gnome_keyring_is_available")); + find_password = reinterpret_cast(resolve("gnome_keyring_find_password")); + store_password = reinterpret_cast(resolve("gnome_keyring_store_password")); + delete_password = + reinterpret_cast(resolve("gnome_keyring_delete_password")); +} + +GnomeKeyring &GnomeKeyring::instance() +{ + static GnomeKeyring keyring; + return keyring; +} diff --git a/qtkeychain/gnomekeyring_p.h b/qtkeychain/gnomekeyring_p.h new file mode 100644 index 00000000..57fcdc5a --- /dev/null +++ b/qtkeychain/gnomekeyring_p.h @@ -0,0 +1,95 @@ +#ifndef QTKEYCHAIN_GNOME_P_H +#define QTKEYCHAIN_GNOME_P_H + +#include + +class GnomeKeyring : private QLibrary +{ + Q_OBJECT + +public: + enum Result { + RESULT_OK, + RESULT_DENIED, + RESULT_NO_KEYRING_DAEMON, + RESULT_ALREADY_UNLOCKED, + RESULT_NO_SUCH_KEYRING, + RESULT_BAD_ARGUMENTS, + RESULT_IO_ERROR, + RESULT_CANCELLED, + RESULT_KEYRING_ALREADY_EXISTS, + RESULT_NO_MATCH + }; + + enum ItemType { + ITEM_GENERIC_SECRET = 0, + ITEM_NETWORK_PASSWORD, + ITEM_NOTE, + ITEM_CHAINED_KEYRING_PASSWORD, + ITEM_ENCRYPTION_KEY_PASSWORD, + ITEM_PK_STORAGE = 0x100 + }; + + enum AttributeType { ATTRIBUTE_TYPE_STRING, ATTRIBUTE_TYPE_UINT32 }; + + typedef char gchar; + typedef void *gpointer; + typedef bool gboolean; + typedef struct + { + ItemType item_type; + struct + { + const gchar *name; + AttributeType type; + } attributes[32]; + } PasswordSchema; + + typedef void (*OperationGetStringCallback)(Result result, bool binary, const char *string, + gpointer data); + typedef void (*OperationDoneCallback)(Result result, gpointer data); + typedef void (*GDestroyNotify)(gpointer data); + + static const char *GNOME_KEYRING_DEFAULT; + + static bool isAvailable(); + + static gpointer store_network_password(const gchar *keyring, const gchar *display_name, + const gchar *user, const gchar *server, + const gchar *type, const gchar *password, + OperationDoneCallback callback, gpointer data, + GDestroyNotify destroy_data); + + static gpointer find_network_password(const gchar *user, const gchar *server, const gchar *type, + OperationGetStringCallback callback, gpointer data, + GDestroyNotify destroy_data); + + static gpointer delete_network_password(const gchar *user, const gchar *server, + OperationDoneCallback callback, gpointer data, + GDestroyNotify destroy_data); + +private: + GnomeKeyring(); + + static GnomeKeyring &instance(); + + const PasswordSchema *NETWORK_PASSWORD; + typedef gboolean(is_available_fn)(void); + typedef gpointer(store_password_fn)(const PasswordSchema *schema, const gchar *keyring, + const gchar *display_name, const gchar *password, + OperationDoneCallback callback, gpointer data, + GDestroyNotify destroy_data, ...); + typedef gpointer(find_password_fn)(const PasswordSchema *schema, + OperationGetStringCallback callback, gpointer data, + GDestroyNotify destroy_data, ...); + typedef gpointer(delete_password_fn)(const PasswordSchema *schema, + OperationDoneCallback callback, gpointer data, + GDestroyNotify destroy_data, ...); + + is_available_fn *is_available; + find_password_fn *find_password; + store_password_fn *store_password; + delete_password_fn *delete_password; +}; + +#endif diff --git a/qtkeychain/keychain.cpp b/qtkeychain/keychain.cpp new file mode 100644 index 00000000..6179a62f --- /dev/null +++ b/qtkeychain/keychain.cpp @@ -0,0 +1,260 @@ +/****************************************************************************** + * Copyright (C) 2011-2015 Frank Osterfeld * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ +#include "keychain.h" +#include "keychain_p.h" + +using namespace QKeychain; + +Job::Job(JobPrivate *q, QObject *parent) : QObject(parent), d(q) { } + +Job::~Job() +{ + delete d; +} + +QString Job::service() const +{ + return d->service; +} + +QSettings *Job::settings() const +{ + return d->settings; +} + +void Job::setSettings(QSettings *settings) +{ + d->settings = settings; +} + +void Job::start() +{ + QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection); +} + +bool Job::autoDelete() const +{ + return d->autoDelete; +} + +void Job::setAutoDelete(bool autoDelete) +{ + d->autoDelete = autoDelete; +} + +bool Job::insecureFallback() const +{ + return d->insecureFallback; +} + +void Job::setInsecureFallback(bool insecureFallback) +{ + d->insecureFallback = insecureFallback; +} + +void Job::doStart() +{ + JobExecutor::instance()->enqueue(this); +} + +void Job::emitFinished() +{ + emit finished(this); + if (d->autoDelete) + deleteLater(); +} + +void Job::emitFinishedWithError(Error error, const QString &errorString) +{ + d->error = error; + d->errorString = errorString; + emitFinished(); +} + +void Job::scheduledStart() +{ + if (d->service.isEmpty() && d->key.isEmpty()) { + emitFinishedWithError(EntryNotFound, tr("Both service name and key are empty")); + return; + } + d->scheduledStart(); +} + +Error Job::error() const +{ + return d->error; +} + +QString Job::errorString() const +{ + return d->errorString; +} + +void Job::setError(Error error) +{ + d->error = error; +} + +void Job::setErrorString(const QString &errorString) +{ + d->errorString = errorString; +} + +ReadPasswordJob::ReadPasswordJob(const QString &service, QObject *parent) + : Job(new ReadPasswordJobPrivate(service, this), parent) +{ +} + +ReadPasswordJob::~ReadPasswordJob() { } + +QString ReadPasswordJob::textData() const +{ + return QString::fromUtf8(d->data); +} + +QByteArray ReadPasswordJob::binaryData() const +{ + return d->data; +} + +QString Job::key() const +{ + return d->key; +} + +void Job::setKey(const QString &key_) +{ + d->key = key_; +} + +WritePasswordJob::WritePasswordJob(const QString &service, QObject *parent) + : Job(new WritePasswordJobPrivate(service, this), parent) +{ +} + +WritePasswordJob::~WritePasswordJob() { } + +void WritePasswordJob::setBinaryData(const QByteArray &data) +{ + d->data = data; + d->mode = JobPrivate::Binary; +} + +void WritePasswordJob::setTextData(const QString &data) +{ + d->data = data.toUtf8(); + d->mode = JobPrivate::Text; +} + +DeletePasswordJob::DeletePasswordJob(const QString &service, QObject *parent) + : Job(new DeletePasswordJobPrivate(service, this), parent) +{ +} + +DeletePasswordJob::~DeletePasswordJob() { } + +DeletePasswordJobPrivate::DeletePasswordJobPrivate(const QString &service_, DeletePasswordJob *qq) + : JobPrivate(service_, qq) +{ +} + +JobExecutor::JobExecutor() : QObject(nullptr), m_jobRunning(false) { } + +void JobExecutor::enqueue(Job *job) +{ + m_queue.enqueue(job); + startNextIfNoneRunning(); +} + +void JobExecutor::startNextIfNoneRunning() +{ + if (m_queue.isEmpty() || m_jobRunning) + return; + QPointer next; + while (!next && !m_queue.isEmpty()) { + next = m_queue.dequeue(); + } + if (next) { + connect(next, &Job::finished, this, &JobExecutor::jobFinished); + connect(next, &Job::destroyed, this, &JobExecutor::jobDestroyed); + m_jobRunning = true; + next->scheduledStart(); + } +} + +void JobExecutor::jobDestroyed(QObject *object) +{ + Job *job = static_cast(object); + Q_UNUSED(object) // for release mode + job->disconnect(this); + m_jobRunning = false; + startNextIfNoneRunning(); +} + +void JobExecutor::jobFinished(Job *job) +{ + Q_UNUSED(job) // for release mode + job->disconnect(this); + m_jobRunning = false; + startNextIfNoneRunning(); +} + +JobExecutor *JobExecutor::s_instance = nullptr; + +JobExecutor *JobExecutor::instance() +{ + if (!s_instance) + s_instance = new JobExecutor; + return s_instance; +} + +ReadPasswordJobPrivate::ReadPasswordJobPrivate(const QString &service_, ReadPasswordJob *qq) + : JobPrivate(service_, qq) +{ +} + +JobPrivate::JobPrivate(const QString &service_, Job *qq) + : q(qq), + mode(Text), + error(NoError), + service(service_), + autoDelete(true), + insecureFallback(false) +{ +} + +QString JobPrivate::modeToString(Mode m) +{ + switch (m) { + case Text: + return QLatin1String("Text"); + case Binary: + return QLatin1String("Binary"); + } + + Q_ASSERT_X(false, Q_FUNC_INFO, "Unhandled Mode value"); + return QString(); +} + +JobPrivate::Mode JobPrivate::stringToMode(const QString &s) +{ + if (s == QLatin1String("Text") || s == QLatin1String("1")) + return Text; + if (s == QLatin1String("Binary") || s == QLatin1String("2")) + return Binary; + + qCritical("Unexpected mode string '%s'", qPrintable(s)); + + return Text; +} + +WritePasswordJobPrivate::WritePasswordJobPrivate(const QString &service_, WritePasswordJob *qq) + : JobPrivate(service_, qq) +{ +} diff --git a/keychain.h b/qtkeychain/keychain.h similarity index 76% rename from keychain.h rename to qtkeychain/keychain.h index 1d7a5fd6..3c6f4810 100644 --- a/keychain.h +++ b/qtkeychain/keychain.h @@ -10,9 +10,9 @@ #define KEYCHAIN_H #if !defined(QTKEYCHAIN_NO_EXPORT) -#include "qkeychain_export.h" +# include "qkeychain_export.h" #else -#define QKEYCHAIN_EXPORT +# define QKEYCHAIN_EXPORT #endif #include @@ -28,7 +28,7 @@ namespace QKeychain { * Error codes */ enum Error { - NoError=0, /**< No error occurred, operation was successful */ + NoError = 0, /**< No error occurred, operation was successful */ EntryNotFound, /**< For the given key no data was found */ CouldNotDeleteEntry, /**< Could not delete existing secret data */ AccessDeniedByUser, /**< User denied access to keychain */ @@ -44,24 +44,26 @@ class JobPrivate; /** * @brief Abstract base class for all QKeychain jobs. */ -class QKEYCHAIN_EXPORT Job : public QObject { +class QKEYCHAIN_EXPORT Job : public QObject +{ Q_OBJECT public: - ~Job(); + ~Job() override; /** * @return The QSettings instance used as plaintext storage if insecureFallback() is true. * @see setSettings() * @see insecureFallback() */ - QSettings* settings() const; + QSettings *settings() const; /** - * @return Set the QSettings instance that will be used as plaintext storage if insecureFallback() is true. + * @return Set the QSettings instance that will be used as plaintext storage if + * insecureFallback() is true. * @see settings() * @see insecureFallback() */ - void setSettings( QSettings* settings ); + void setSettings(QSettings *settings); /** * Call this method to start the job. @@ -102,7 +104,8 @@ class QKEYCHAIN_EXPORT Job : public QObject { QString errorString() const; /** - * @return Whether this job autodeletes itself once finished() has been emitted. Default is true. + * @return Whether this job autodeletes itself once finished() has been emitted. Default is + * true. * @see setAutoDelete() */ bool autoDelete() const; @@ -111,10 +114,11 @@ class QKEYCHAIN_EXPORT Job : public QObject { * Set whether this job should autodelete itself once finished() has been emitted. * @see autoDelete() */ - void setAutoDelete( bool autoDelete ); + void setAutoDelete(bool autoDelete); /** - * @return Whether this job will use plaintext storage on unsupported platforms. Default is false. + * @return Whether this job will use plaintext storage on unsupported platforms. Default is + * false. * @see setInsecureFallback() */ bool insecureFallback() const; @@ -123,7 +127,7 @@ class QKEYCHAIN_EXPORT Job : public QObject { * Set whether this job should use plaintext storage on unsupported platforms. * @see insecureFallback() */ - void setInsecureFallback( bool insecureFallback ); + void setInsecureFallback(bool insecureFallback); /** * @return The string used as key by this job. @@ -136,10 +140,10 @@ class QKEYCHAIN_EXPORT Job : public QObject { * The key can be an empty string. * @see key() */ - void setKey( const QString& key ); + void setKey(const QString &key); void emitFinished(); - void emitFinishedWithError(Error, const QString& errorString); + void emitFinishedWithError(Error, const QString &errorString); Q_SIGNALS: /** @@ -147,26 +151,26 @@ class QKEYCHAIN_EXPORT Job : public QObject { * You can connect to this signal to be notified about the job's completion. * @see start() */ - void finished( QKeychain::Job* ); + void finished(QKeychain::Job *); protected: - explicit Job( JobPrivate *q, QObject* parent=0 ); + explicit Job(JobPrivate *q, QObject *parent = nullptr); Q_INVOKABLE void doStart(); private: - void setError( Error error ); - void setErrorString( const QString& errorString ); + void setError(Error error); + void setErrorString(const QString &errorString); void scheduledStart(); protected: - JobPrivate* const d; + JobPrivate *const d; -friend class JobExecutor; -friend class JobPrivate; -friend class ReadPasswordJobPrivate; -friend class WritePasswordJobPrivate; -friend class DeletePasswordJobPrivate; + friend class JobExecutor; + friend class JobPrivate; + friend class ReadPasswordJobPrivate; + friend class WritePasswordJobPrivate; + friend class DeletePasswordJobPrivate; }; class ReadPasswordJobPrivate; @@ -177,7 +181,8 @@ class ReadPasswordJobPrivate; * This job requires a "service" string, which is basically a namespace of keys within the keychain. * This means that you can read all the pairs stored in the same service string. */ -class QKEYCHAIN_EXPORT ReadPasswordJob : public Job { +class QKEYCHAIN_EXPORT ReadPasswordJob : public Job +{ Q_OBJECT public: /** @@ -185,8 +190,8 @@ class QKEYCHAIN_EXPORT ReadPasswordJob : public Job { * @param service The service string used by this job (can be empty). * @param parent The parent of this job. */ - explicit ReadPasswordJob( const QString& service, QObject* parent=0 ); - ~ReadPasswordJob(); + explicit ReadPasswordJob(const QString &service, QObject *parent = nullptr); + ~ReadPasswordJob() override; /** * @return The binary data stored as value of this job's key(). @@ -214,7 +219,8 @@ class WritePasswordJobPrivate; * This job requires a "service" string, which is basically a namespace of keys within the keychain. * This means that you can store different pairs under the same service string. */ -class QKEYCHAIN_EXPORT WritePasswordJob : public Job { +class QKEYCHAIN_EXPORT WritePasswordJob : public Job +{ Q_OBJECT public: /** @@ -222,24 +228,23 @@ class QKEYCHAIN_EXPORT WritePasswordJob : public Job { * @param service The service string used by this job (can be empty). * @param parent The parent of this job. */ - explicit WritePasswordJob( const QString& service, QObject* parent=0 ); - ~WritePasswordJob(); + explicit WritePasswordJob(const QString &service, QObject *parent = nullptr); + ~WritePasswordJob() override; /** * Set the @p data that the job will store in the keychain as binary data. * @warning setBinaryData() and setTextData() are mutually exclusive. */ - void setBinaryData( const QByteArray& data ); + void setBinaryData(const QByteArray &data); /** * Set the @p data that the job will store in the keychain as string. * Typically @p data is a password. * @warning setBinaryData() and setTextData() are mutually exclusive. */ - void setTextData( const QString& data ); + void setTextData(const QString &data); private: - friend class QKeychain::WritePasswordJobPrivate; }; @@ -251,7 +256,8 @@ class DeletePasswordJobPrivate; * This job requires a "service" string, which is basically a namespace of keys within the keychain. * This means that you can delete all the pairs stored in the same service string. */ -class QKEYCHAIN_EXPORT DeletePasswordJob : public Job { +class QKEYCHAIN_EXPORT DeletePasswordJob : public Job +{ Q_OBJECT public: /** @@ -259,13 +265,24 @@ class QKEYCHAIN_EXPORT DeletePasswordJob : public Job { * @param service The service string used by this job (can be empty). * @param parent The parent of this job. */ - explicit DeletePasswordJob( const QString& service, QObject* parent=0 ); - ~DeletePasswordJob(); + explicit DeletePasswordJob(const QString &service, QObject *parent = nullptr); + ~DeletePasswordJob() override; private: friend class QKeychain::DeletePasswordJobPrivate; }; -} // namespace QtKeychain +/** + * Checks whether there is a viable secure backend available. + * This particularly matters on UNIX platforms where multiple different backends + * exist and none might be available. + * + * Note that using the insecure fallback will work even if no secure backend is available. + * + * @since 0.14.0 + */ +QKEYCHAIN_EXPORT bool isAvailable(); + +} // namespace QKeychain #endif diff --git a/qtkeychain/keychain_android.cpp b/qtkeychain/keychain_android.cpp new file mode 100644 index 00000000..317d55f8 --- /dev/null +++ b/qtkeychain/keychain_android.cpp @@ -0,0 +1,209 @@ +/****************************************************************************** + * Copyright (C) 2016 Mathias Hasselmann * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ + +#include "keychain_p.h" + +#include "androidkeystore_p.h" +#include "plaintextstore_p.h" + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +# include +#endif + +using namespace QKeychain; + +using android::content::Context; +using android::security::KeyPairGeneratorSpec; + +using java::io::ByteArrayInputStream; +using java::io::ByteArrayOutputStream; +using java::security::KeyPair; +using java::security::KeyPairGenerator; +using java::security::KeyStore; +using java::security::interfaces::RSAPrivateKey; +using java::security::interfaces::RSAPublicKey; +using java::util::Calendar; + +using javax::crypto::Cipher; +using javax::crypto::CipherInputStream; +using javax::crypto::CipherOutputStream; +using javax::security::auth::x500::X500Principal; + +namespace { + +inline QString makeAlias(const QString &service, const QString &key) +{ + return service + QLatin1Char('/') + key; +} + +} // namespace + +void ReadPasswordJobPrivate::scheduledStart() +{ + PlainTextStore plainTextStore(q->service(), q->settings()); + + if (!plainTextStore.contains(q->key())) { + q->emitFinishedWithError(Error::EntryNotFound, tr("Entry not found")); + return; + } + + const QByteArray &encryptedData = plainTextStore.readData(q->key()); + const auto keyStore = KeyStore::getInstance(QStringLiteral("AndroidKeyStore")); + + if (!keyStore || !keyStore.load()) { + q->emitFinishedWithError(Error::AccessDenied, tr("Could not open keystore")); + return; + } + + const auto &alias = makeAlias(q->service(), q->key()); + const KeyStore::PrivateKeyEntry entry = keyStore.getEntry(alias); + + if (!entry) { + q->emitFinishedWithError(Error::AccessDenied, + tr("Could not retrieve private key from keystore")); + return; + } + + const auto cipher = Cipher::getInstance(QStringLiteral("RSA/ECB/PKCS1Padding")); + + if (!cipher || !cipher.init(Cipher::DECRYPT_MODE, entry.getPrivateKey())) { + q->emitFinishedWithError(Error::OtherError, tr("Could not create decryption cipher")); + return; + } + + QByteArray plainData; + const CipherInputStream inputStream(ByteArrayInputStream(encryptedData), cipher); + + for (int nextByte; (nextByte = inputStream.read()) != -1;) + plainData.append(nextByte); + + mode = plainTextStore.readMode(q->key()); + data = plainData; + q->emitFinished(); +} + +void WritePasswordJobPrivate::scheduledStart() +{ + const KeyStore keyStore = KeyStore::getInstance(QStringLiteral("AndroidKeyStore")); + + if (!keyStore || !keyStore.load()) { + q->emitFinishedWithError(Error::AccessDenied, tr("Could not open keystore")); + return; + } + + const auto &alias = makeAlias(q->service(), q->key()); + if (!keyStore.containsAlias(alias)) { + const auto start = Calendar::getInstance(); + const auto end = Calendar::getInstance(); + end.add(Calendar::YEAR, 99); + + const KeyPairGeneratorSpec spec = +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + KeyPairGeneratorSpec::Builder(Context(QtAndroid::androidActivity())) + . +#elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0) + KeyPairGeneratorSpec::Builder( + Context(QNativeInterface::QAndroidApplication::context())) + . +#elif QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + KeyPairGeneratorSpec::Builder( + Context((jobject)QNativeInterface::QAndroidApplication::context())) + . +#else + KeyPairGeneratorSpec::Builder( + Context(QNativeInterface::QAndroidApplication::context().object())) + . +#endif + setAlias(alias) + .setSubject( + X500Principal(QStringLiteral("CN=QtKeychain, O=Android Authority"))) + .setSerialNumber(java::math::BigInteger::ONE) + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + + const auto generator = KeyPairGenerator::getInstance(QStringLiteral("RSA"), + QStringLiteral("AndroidKeyStore")); + + if (!generator) { + q->emitFinishedWithError(Error::OtherError, + tr("Could not create private key generator")); + return; + } + + generator.initialize(spec); + + if (!generator.generateKeyPair()) { + q->emitFinishedWithError(Error::OtherError, tr("Could not generate new private key")); + return; + } + } + + const KeyStore::PrivateKeyEntry entry = keyStore.getEntry(alias); + + if (!entry) { + q->emitFinishedWithError(Error::AccessDenied, + tr("Could not retrieve private key from keystore")); + return; + } + + const RSAPublicKey publicKey = entry.getCertificate().getPublicKey(); + const auto cipher = Cipher::getInstance(QStringLiteral("RSA/ECB/PKCS1Padding")); + + if (!cipher || !cipher.init(Cipher::ENCRYPT_MODE, publicKey)) { + q->emitFinishedWithError(Error::OtherError, tr("Could not create encryption cipher")); + return; + } + + ByteArrayOutputStream outputStream; + CipherOutputStream cipherOutputStream(outputStream, cipher); + + if (!cipherOutputStream.write(data) || !cipherOutputStream.close()) { + q->emitFinishedWithError(Error::OtherError, tr("Could not encrypt data")); + return; + } + + PlainTextStore plainTextStore(q->service(), q->settings()); + plainTextStore.write(q->key(), outputStream.toByteArray(), mode); + + if (plainTextStore.error() != NoError) + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + else + q->emitFinished(); +} + +void DeletePasswordJobPrivate::scheduledStart() +{ + const auto keyStore = KeyStore::getInstance(QStringLiteral("AndroidKeyStore")); + + if (!keyStore || !keyStore.load()) { + q->emitFinishedWithError(Error::AccessDenied, tr("Could not open keystore")); + return; + } + + const auto &alias = makeAlias(q->service(), q->key()); + if (!keyStore.deleteEntry(alias)) { + q->emitFinishedWithError(Error::OtherError, + tr("Could not remove private key from keystore")); + return; + } + + PlainTextStore plainTextStore(q->service(), q->settings()); + plainTextStore.remove(q->key()); + + if (plainTextStore.error() != NoError) + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + else + q->emitFinished(); +} + +bool QKeychain::isAvailable() +{ + return true; +} diff --git a/qtkeychain/keychain_apple.mm b/qtkeychain/keychain_apple.mm new file mode 100644 index 00000000..2b0abeee --- /dev/null +++ b/qtkeychain/keychain_apple.mm @@ -0,0 +1,291 @@ +/****************************************************************************** + * Copyright (C) 2016 Mathias Hasselmann * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ + +#include "keychain_p.h" + +#import +#import + +using namespace QKeychain; + +struct ErrorDescription +{ + QKeychain::Error code; + QString message; + + ErrorDescription(QKeychain::Error code, const QString &message) : code(code), message(message) + { + } + + static ErrorDescription fromStatus(OSStatus status) + { + switch (status) { + case errSecSuccess: + return ErrorDescription(QKeychain::NoError, Job::tr("No error")); + case errSecItemNotFound: + return ErrorDescription( + QKeychain::EntryNotFound, + Job::tr("The specified item could not be found in the keychain")); + case errSecUserCanceled: + return ErrorDescription(QKeychain::AccessDeniedByUser, + Job::tr("User canceled the operation")); + case errSecInteractionNotAllowed: + return ErrorDescription(QKeychain::AccessDenied, + Job::tr("User interaction is not allowed")); + case errSecNotAvailable: + return ErrorDescription( + QKeychain::AccessDenied, + Job::tr("No keychain is available. You may need to restart your computer")); + case errSecAuthFailed: + return ErrorDescription( + QKeychain::AccessDenied, + Job::tr("The user name or passphrase you entered is not correct")); + case errSecVerifyFailed: + return ErrorDescription(QKeychain::AccessDenied, + Job::tr("A cryptographic verification failure has occurred")); + case errSecUnimplemented: + return ErrorDescription(QKeychain::NotImplemented, + Job::tr("Function or operation not implemented")); + case errSecIO: + return ErrorDescription(QKeychain::OtherError, Job::tr("I/O error")); + case errSecOpWr: + return ErrorDescription(QKeychain::OtherError, + Job::tr("Already open with with write permission")); + case errSecParam: + return ErrorDescription(QKeychain::OtherError, + Job::tr("Invalid parameters passed to a function")); + case errSecAllocate: + return ErrorDescription(QKeychain::OtherError, Job::tr("Failed to allocate memory")); + case errSecBadReq: + return ErrorDescription(QKeychain::OtherError, + Job::tr("Bad parameter or invalid state for operation")); + case errSecInternalComponent: + return ErrorDescription(QKeychain::OtherError, Job::tr("An internal component failed")); + case errSecDuplicateItem: + return ErrorDescription(QKeychain::OtherError, + Job::tr("The specified item already exists in the keychain")); + case errSecDecode: + return ErrorDescription(QKeychain::OtherError, + Job::tr("Unable to decode the provided data")); + } + + return ErrorDescription(QKeychain::OtherError, Job::tr("Unknown error")); + } +}; + +@interface AppleKeychainInterface : NSObject + +- (instancetype)initWithJob:(Job *)job andPrivateJob:(JobPrivate *)privateJob; +- (void)keychainTaskFinished; +- (void)keychainReadTaskFinished:(NSData *)retrievedData; +- (void)keychainTaskFinishedWithError:(OSStatus)status + descriptiveMessage:(NSString *)descriptiveMessage; + +@end + +@interface AppleKeychainInterface () { + QPointer _job; + QPointer _privateJob; +} +@end + +@implementation AppleKeychainInterface + +- (instancetype)initWithJob:(Job *)job andPrivateJob:(JobPrivate *)privateJob +{ + self = [super init]; + if (self) { + _job = job; + _privateJob = privateJob; + } + return self; +} + +- (void)dealloc +{ + [NSNotificationCenter.defaultCenter removeObserver:self]; + [super dealloc]; +} + +- (void)keychainTaskFinished +{ + if (_job) { + _job->emitFinished(); + } +} + +- (void)keychainReadTaskFinished:(NSData *)retrievedData +{ + if (_privateJob) { + _privateJob->data.clear(); + _privateJob->mode = JobPrivate::Binary; + if (retrievedData != nil) { + _privateJob->data = QByteArray::fromNSData(retrievedData); + } + } + + if (_job) { + _job->emitFinished(); + } +} + +- (void)keychainTaskFinishedWithError:(OSStatus)status + descriptiveMessage:(NSString *)descriptiveMessage +{ + const auto localisedDescriptiveMessage = Job::tr([descriptiveMessage UTF8String]); + + const ErrorDescription error = ErrorDescription::fromStatus(status); + const auto fullMessage = localisedDescriptiveMessage.isEmpty() + ? error.message + : QStringLiteral("%1: %2").arg(localisedDescriptiveMessage, error.message); + + if (_job) { + _job->emitFinishedWithError(error.code, fullMessage); + } +} + +@end + +static void StartReadPassword(const QString &service, const QString &key, + AppleKeychainInterface *const interface) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + NSDictionary *const query = @{ + (__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword, + (__bridge NSString *)kSecAttrService : service.toNSString(), + (__bridge NSString *)kSecAttrAccount : key.toNSString(), + (__bridge NSString *)kSecReturnData : @YES, + }; + + CFTypeRef dataRef = nil; + const OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataRef); + + if (status == errSecSuccess) { + const CFDataRef castedDataRef = (CFDataRef)dataRef; + NSData *const data = (__bridge NSData *)castedDataRef; + dispatch_async(dispatch_get_main_queue(), ^{ + [interface keychainReadTaskFinished:data]; + [interface release]; + }); + } else { + NSString *const descriptiveErrorString = + @"Could not retrieve private key from keystore"; + dispatch_async(dispatch_get_main_queue(), ^{ + [interface keychainTaskFinishedWithError:status + descriptiveMessage:descriptiveErrorString]; + [interface release]; + }); + } + + if (dataRef) { + CFRelease(dataRef); + } + }); +} + +static void StartWritePassword(const QString &service, const QString &key, const QByteArray &data, + AppleKeychainInterface *const interface) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + NSDictionary *const query = @{ + (__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword, + (__bridge NSString *)kSecAttrService : service.toNSString(), + (__bridge NSString *)kSecAttrAccount : key.toNSString(), + }; + + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, nil); + + if (status == errSecSuccess) { + NSDictionary *const update = @{ + (__bridge NSString *)kSecValueData : data.toNSData(), + }; + + status = SecItemUpdate((__bridge CFDictionaryRef)query, + (__bridge CFDictionaryRef)update); + } else { + NSDictionary *const insert = @{ + (__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword, + (__bridge NSString *)kSecAttrService : service.toNSString(), + (__bridge NSString *)kSecAttrAccount : key.toNSString(), + (__bridge NSString *)kSecValueData : data.toNSData(), + }; + + status = SecItemAdd((__bridge const CFDictionaryRef)insert, nil); + } + + if (status == errSecSuccess) { + dispatch_async(dispatch_get_main_queue(), ^{ + [interface keychainTaskFinished]; + [interface release]; + }); + } else { + NSString *const descriptiveErrorString = @"Could not store data in settings"; + + dispatch_async(dispatch_get_main_queue(), ^{ + [interface keychainTaskFinishedWithError:status + descriptiveMessage:descriptiveErrorString]; + [interface release]; + }); + } + }); +} + +static void StartDeletePassword(const QString &service, const QString &key, + AppleKeychainInterface *const interface) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + NSDictionary *const query = @{ + (__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword, + (__bridge NSString *)kSecAttrService : service.toNSString(), + (__bridge NSString *)kSecAttrAccount : key.toNSString(), + }; + + const OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + + if (status == errSecSuccess) { + dispatch_async(dispatch_get_main_queue(), ^{ + [interface keychainTaskFinished]; + [interface release]; + }); + } else { + NSString *const descriptiveErrorString = @"Could not remove private key from keystore"; + dispatch_async(dispatch_get_main_queue(), ^{ + [interface keychainTaskFinishedWithError:status + descriptiveMessage:descriptiveErrorString]; + [interface release]; + }); + } + }); +} + +void ReadPasswordJobPrivate::scheduledStart() +{ + AppleKeychainInterface *const interface = [[AppleKeychainInterface alloc] initWithJob:q + andPrivateJob:this]; + StartReadPassword(service, key, interface); +} + +void WritePasswordJobPrivate::scheduledStart() +{ + AppleKeychainInterface *const interface = [[AppleKeychainInterface alloc] initWithJob:q + andPrivateJob:this]; + StartWritePassword(service, key, data, interface); +} + +void DeletePasswordJobPrivate::scheduledStart() +{ + AppleKeychainInterface *const interface = [[AppleKeychainInterface alloc] initWithJob:q + andPrivateJob:this]; + StartDeletePassword(service, key, interface); +} + +bool QKeychain::isAvailable() +{ + return true; +} diff --git a/qtkeychain/keychain_haiku.cpp b/qtkeychain/keychain_haiku.cpp new file mode 100644 index 00000000..94374c8e --- /dev/null +++ b/qtkeychain/keychain_haiku.cpp @@ -0,0 +1,183 @@ +/****************************************************************************** + * Copyright (C) 2018 François Revol * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ +#include "keychain_p.h" + +#include + +#include +#include +#include + +#include +#include +#include + +using namespace QKeychain; + +class AutoApp +{ +public: + AutoApp(); + ~AutoApp(); + BApplication *app; +}; + +AutoApp::AutoApp() : app(nullptr) +{ + if (be_app) + return; + + // no BApplication object, probably using QCoreApplication + // but we need one around + + QString appSignature; + + char signature[B_MIME_TYPE_LENGTH]; + signature[0] = '\0'; + + QString appPath = QCoreApplication::applicationFilePath(); + + BFile appFile(appPath.toUtf8(), B_READ_ONLY); + if (appFile.InitCheck() == B_OK) { + BAppFileInfo info(&appFile); + if (info.InitCheck() == B_OK) { + if (info.GetSignature(signature) != B_OK) + signature[0] = '\0'; + } + } + + if (signature[0] != '\0') + appSignature = QLatin1String(signature); + else + appSignature = QLatin1String("application/x-vnd.qtkeychain-") + + QCoreApplication::applicationName().remove("_x86"); + + app = new BApplication(appSignature.toUtf8().constData()); +} + +AutoApp::~AutoApp() +{ + delete app; +} + +static QString strForStatus(status_t os) +{ + const char *const buf = strerror(os); + return QObject::tr("error 0x%1: %2").arg(os, 8, 16).arg(QString::fromUtf8(buf, strlen(buf))); +} + +void ReadPasswordJobPrivate::scheduledStart() +{ + AutoApp aa; + QString errorString; + Error error = NoError; + BKeyStore keyStore; + BPasswordKey password; + + status_t result = keyStore.GetKey(B_KEY_TYPE_PASSWORD, q->service().toUtf8().constData(), + q->key().toUtf8().constData(), false, password); + + data = QByteArray(reinterpret_cast(password.Data())); + + switch (result) { + case B_OK: + q->emitFinished(); + return; + case B_ENTRY_NOT_FOUND: + errorString = tr("Password not found"); + error = EntryNotFound; + break; + default: + errorString = strForStatus(result); + error = OtherError; + break; + } + + q->emitFinishedWithError(error, errorString); +} + +void WritePasswordJobPrivate::scheduledStart() +{ + AutoApp aa; + QString errorString; + Error error = NoError; + BKeyStore keyStore; + BPasswordKey password(data.constData(), B_KEY_PURPOSE_GENERIC, + q->service().toUtf8().constData(), q->key().toUtf8().constData()); + status_t result = B_OK; + + // re-add as binary if it's not text + if (mode == Binary) + result = password.SetData(reinterpret_cast(data.constData()), data.size()); + + if (result == B_OK) + result = keyStore.AddKey(password); + + if (result == B_NAME_IN_USE) { + BPasswordKey old_password; + result = keyStore.GetKey(B_KEY_TYPE_PASSWORD, q->service().toUtf8().constData(), + q->key().toUtf8().constData(), false, old_password); + if (result == B_OK) + result = keyStore.RemoveKey(old_password); + if (result == B_OK) + result = keyStore.AddKey(password); + } + + switch (result) { + case B_OK: + q->emitFinished(); + return; + case B_ENTRY_NOT_FOUND: + errorString = tr("Password not found"); + error = EntryNotFound; + break; + default: + errorString = strForStatus(result); + error = OtherError; + break; + } + + q->emitFinishedWithError(error, errorString); +} + +void DeletePasswordJobPrivate::scheduledStart() +{ + AutoApp aa; + QString errorString; + Error error = NoError; + BKeyStore keyStore; + BPasswordKey password; + + status_t result = keyStore.GetKey(B_KEY_TYPE_PASSWORD, q->service().toUtf8().constData(), + q->key().toUtf8().constData(), false, password); + + if (result == B_OK) + result = keyStore.RemoveKey(password); + + switch (result) { + case B_OK: + q->emitFinished(); + return; + case B_ENTRY_NOT_FOUND: + errorString = tr("Password not found"); + error = EntryNotFound; + break; + default: + errorString = strForStatus(result); + error = CouldNotDeleteEntry; + break; + } + + q->emitFinishedWithError(error, errorString); +} + +bool QKeychain::isAvailable() +{ + return true; +} diff --git a/qtkeychain/keychain_p.h b/qtkeychain/keychain_p.h new file mode 100644 index 00000000..5696e678 --- /dev/null +++ b/qtkeychain/keychain_p.h @@ -0,0 +1,168 @@ +/****************************************************************************** + * Copyright (C) 2011-2015 Frank Osterfeld * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ +#ifndef KEYCHAIN_P_H +#define KEYCHAIN_P_H + +#include +#include +#include +#include +#include + +#if defined(KEYCHAIN_DBUS) + +# include + +# include "kwallet_interface.h" +#else + +class QDBusPendingCallWatcher; + +#endif + +#include "keychain.h" + +namespace QKeychain { + +class JobExecutor; + +class JobPrivate : public QObject +{ + Q_OBJECT +public: + enum Mode { Text, Binary }; + + virtual void scheduledStart() = 0; + + static QString modeToString(Mode m); + static Mode stringToMode(const QString &s); + + Job *const q; + Mode mode; + QByteArray data; + +#if defined(KEYCHAIN_DBUS) + org::kde::KWallet *iface; + int walletHandle; + + static void gnomeKeyring_readCb(int result, const char *string, JobPrivate *data); + static void gnomeKeyring_writeCb(int result, JobPrivate *self); + + virtual void fallbackOnError(const QDBusError &err) = 0; + +public Q_SLOTS: + void kwalletWalletFound(QDBusPendingCallWatcher *watcher); + virtual void kwalletFinished(QDBusPendingCallWatcher *watcher); + virtual void kwalletOpenFinished(QDBusPendingCallWatcher *watcher); +#else + void kwalletWalletFound(QDBusPendingCallWatcher *) { } + virtual void kwalletFinished(QDBusPendingCallWatcher *) { } + virtual void kwalletOpenFinished(QDBusPendingCallWatcher *) { } +#endif + +protected: + JobPrivate(const QString &service_, Job *q); + +protected: + QKeychain::Error error; + QString errorString; + QString service; + bool autoDelete; + bool insecureFallback; + QPointer settings; + QString key; + + friend class Job; + friend class JobExecutor; + friend class ReadPasswordJob; + friend class WritePasswordJob; + friend class PlainTextStore; +}; + +class ReadPasswordJobPrivate : public JobPrivate +{ + Q_OBJECT +public: + explicit ReadPasswordJobPrivate(const QString &service_, ReadPasswordJob *qq); + void scheduledStart() override; + +#if defined(KEYCHAIN_DBUS) + void fallbackOnError(const QDBusError &err) override; + +private Q_SLOTS: + void kwalletOpenFinished(QDBusPendingCallWatcher *watcher) override; + void kwalletEntryTypeFinished(QDBusPendingCallWatcher *watcher); + void kwalletFinished(QDBusPendingCallWatcher *watcher) override; +#else // moc's too dumb to respect above macros, so just define empty slot implementations +private Q_SLOTS: + void kwalletOpenFinished(QDBusPendingCallWatcher *) override { } + void kwalletEntryTypeFinished(QDBusPendingCallWatcher *) { } + void kwalletFinished(QDBusPendingCallWatcher *) override { } +#endif + + friend class ReadPasswordJob; +}; + +class WritePasswordJobPrivate : public JobPrivate +{ + Q_OBJECT +public: + explicit WritePasswordJobPrivate(const QString &service_, WritePasswordJob *qq); + void scheduledStart() override; + +#if defined(KEYCHAIN_DBUS) + void fallbackOnError(const QDBusError &err) override; +#endif + + friend class WritePasswordJob; +}; + +class DeletePasswordJobPrivate : public JobPrivate +{ + Q_OBJECT +public: + explicit DeletePasswordJobPrivate(const QString &service_, DeletePasswordJob *qq); + + void scheduledStart() override; + +#if defined(KEYCHAIN_DBUS) + void fallbackOnError(const QDBusError &err) override; +#endif + +protected: + void doStart(); + + friend class DeletePasswordJob; +}; + +class JobExecutor : public QObject +{ + Q_OBJECT +public: + static JobExecutor *instance(); + + void enqueue(Job *job); + +private: + explicit JobExecutor(); + void startNextIfNoneRunning(); + +private Q_SLOTS: + void jobFinished(QKeychain::Job *); + void jobDestroyed(QObject *object); + +private: + static JobExecutor *s_instance; + QQueue> m_queue; + bool m_jobRunning; +}; + +} // namespace QKeychain + +#endif // KEYCHAIN_P_H diff --git a/qtkeychain/keychain_unix.cpp b/qtkeychain/keychain_unix.cpp new file mode 100644 index 00000000..97b188cb --- /dev/null +++ b/qtkeychain/keychain_unix.cpp @@ -0,0 +1,669 @@ +/****************************************************************************** + * Copyright (C) 2011-2015 Frank Osterfeld * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ +#include "keychain_p.h" +#include "gnomekeyring_p.h" +#include "libsecret_p.h" +#include "plaintextstore_p.h" + +#include + +using namespace QKeychain; + +enum KeyringBackend { + Backend_LibSecretKeyring, + Backend_GnomeKeyring, + Backend_Kwallet4, + Backend_Kwallet5, + Backend_Kwallet6, +}; + +enum DesktopEnvironment { + DesktopEnv_Gnome, + DesktopEnv_Kde4, + DesktopEnv_Plasma5, + DesktopEnv_Plasma6, + DesktopEnv_Unity, + DesktopEnv_Xfce, + DesktopEnv_Other +}; + +static constexpr const char KWALLET6_DBUS_IFACE[] = "org.kde.kwalletd6"; +static constexpr const char KWALLET6_DBUS_PATH[] = "/modules/kwalletd6"; +static constexpr const char KWALLET5_DBUS_IFACE[] = "org.kde.kwalletd5"; +static constexpr const char KWALLET5_DBUS_PATH[] = "/modules/kwalletd5"; +static constexpr const char KWALLET4_DBUS_IFACE[] = "org.kde.kwalletd"; +static constexpr const char KWALLET4_DBUS_PATH[] = "/modules/kwalletd"; + +// the following detection algorithm is derived from chromium, +// licensed under BSD, see base/nix/xdg_util.cc + +static DesktopEnvironment getKdeVersion() +{ + QByteArray value = qgetenv("KDE_SESSION_VERSION"); + if (value == "6") { + return DesktopEnv_Plasma6; + } else if (value == "5") { + return DesktopEnv_Plasma5; + } else if (value == "4") { + return DesktopEnv_Kde4; + } else { + // most likely KDE3 + return DesktopEnv_Other; + } +} + +static DesktopEnvironment detectDesktopEnvironment() +{ + QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP"); + if (xdgCurrentDesktop == "GNOME") { + return DesktopEnv_Gnome; + } else if (xdgCurrentDesktop == "Unity") { + return DesktopEnv_Unity; + } else if (xdgCurrentDesktop == "KDE") { + return getKdeVersion(); + } else if (xdgCurrentDesktop == "XFCE") { + return DesktopEnv_Xfce; + } + + QByteArray desktopSession = qgetenv("DESKTOP_SESSION"); + if (desktopSession == "gnome") { + return DesktopEnv_Gnome; + } else if (desktopSession == "kde") { + return getKdeVersion(); + } else if (desktopSession == "kde4") { + return DesktopEnv_Kde4; + } else if (desktopSession.contains("xfce") || desktopSession == "xubuntu") { + return DesktopEnv_Xfce; + } + + if (!qgetenv("GNOME_DESKTOP_SESSION_ID").isEmpty()) { + return DesktopEnv_Gnome; + } else if (!qgetenv("KDE_FULL_SESSION").isEmpty()) { + return getKdeVersion(); + } + + return DesktopEnv_Other; +} + +static bool isKwalletAvailable(const char *dbusIface, const char *dbusPath) +{ + if (!QDBusConnection::sessionBus().isConnected()) + return false; + + org::kde::KWallet iface(QLatin1String(dbusIface), QLatin1String(dbusPath), + QDBusConnection::sessionBus()); + + // At this point iface.isValid() can return false even though the + // interface is activatable by making a call. Hence we check whether + // a wallet can be opened. + + QDBusMessage reply = iface.call(QLatin1String("networkWallet")); + return reply.type() == QDBusMessage::ReplyMessage; +} + +static KeyringBackend detectKeyringBackend() +{ + /* The secret service dbus api, accessible through libsecret, is supposed + * to unify password services. + * + * Unfortunately at the time of Kubuntu 18.04 the secret service backend + * in KDE is gnome-keyring-daemon - using it has several complications: + * - the default collection isn't opened on session start, so users need + * to manually unlock it when the first application uses it + * - it's separate from the kwallet5 keyring, so switching to it means the + * existing keyring data can't be accessed anymore + * + * Thus we still prefer kwallet backends on KDE even if libsecret is + * available. + */ + + // Check if user wants to override detection logic + QByteArray backendOverride = qgetenv("QTKEYCHAIN_BACKEND"); + if (backendOverride == "libsecret") { + return Backend_LibSecretKeyring; + } else if (backendOverride == "gnome") { + return Backend_GnomeKeyring; + } else if (backendOverride == "kwallet4") { + return Backend_Kwallet4; + } else if (backendOverride == "kwallet5") { + return Backend_Kwallet5; + } else if (backendOverride == "kwallet6") { + return Backend_Kwallet6; + } + + switch (detectDesktopEnvironment()) { + case DesktopEnv_Kde4: + return Backend_Kwallet4; + + case DesktopEnv_Plasma5: + if (isKwalletAvailable(KWALLET5_DBUS_IFACE, KWALLET5_DBUS_PATH)) { + return Backend_Kwallet5; + } + if (LibSecretKeyring::isAvailable()) { + return Backend_LibSecretKeyring; + } + if (GnomeKeyring::isAvailable()) { + return Backend_GnomeKeyring; + } + // During startup the keychain backend might just not have started yet + return Backend_Kwallet5; + + case DesktopEnv_Plasma6: + if (isKwalletAvailable(KWALLET6_DBUS_IFACE, KWALLET6_DBUS_PATH)) { + return Backend_Kwallet6; + } + if (LibSecretKeyring::isAvailable()) { + return Backend_LibSecretKeyring; + } + if (GnomeKeyring::isAvailable()) { + return Backend_GnomeKeyring; + } + // During startup the keychain backend might just not have started yet + return Backend_Kwallet6; + + case DesktopEnv_Gnome: + case DesktopEnv_Unity: + case DesktopEnv_Xfce: + case DesktopEnv_Other: + default: + if (LibSecretKeyring::isAvailable()) { + return Backend_LibSecretKeyring; + } + if (GnomeKeyring::isAvailable()) { + return Backend_GnomeKeyring; + } + if (isKwalletAvailable(KWALLET6_DBUS_IFACE, KWALLET6_DBUS_PATH)) { + return Backend_Kwallet6; + } + if (isKwalletAvailable(KWALLET5_DBUS_IFACE, KWALLET5_DBUS_PATH)) { + return Backend_Kwallet5; + } + // During startup the keychain backend might just not have started yet + // + // This doesn't need to be libsecret because LibSecretKeyring::isAvailable() + // only fails if the libsecret shared library couldn't be loaded. In contrast + // to that GnomeKeyring::isAvailable() can return false if the shared library + // *was* loaded but its libgnome_keyring::is_available() returned false. + // + // In the future there should be a difference between "API available" and + // "keychain available". + return Backend_GnomeKeyring; + } +} + +static KeyringBackend getKeyringBackend() +{ + static KeyringBackend backend = detectKeyringBackend(); + return backend; +} + +static void kwalletReadPasswordScheduledStartImpl(const char *service, const char *path, + ReadPasswordJobPrivate *priv) +{ + if (QDBusConnection::sessionBus().isConnected()) { + priv->iface = new org::kde::KWallet(QLatin1String(service), QLatin1String(path), + QDBusConnection::sessionBus(), priv); + const QDBusPendingReply reply = priv->iface->networkWallet(); + auto watcher = new QDBusPendingCallWatcher(reply, priv); + priv->connect(watcher, &QDBusPendingCallWatcher::finished, priv, + &ReadPasswordJobPrivate::kwalletWalletFound); + } else { + // D-Bus is not reachable so none can tell us something about KWalletd + QDBusError err(QDBusError::NoServer, ReadPasswordJobPrivate::tr("D-Bus is not running")); + priv->fallbackOnError(err); + } +} + +void ReadPasswordJobPrivate::scheduledStart() +{ + switch (getKeyringBackend()) { + case Backend_LibSecretKeyring: { + if (!LibSecretKeyring::findPassword(key, q->service(), this)) { + q->emitFinishedWithError(OtherError, tr("Unknown error")); + } + } break; + case Backend_GnomeKeyring: + this->mode = JobPrivate::Text; + if (!GnomeKeyring::find_network_password( + key.toUtf8().constData(), q->service().toUtf8().constData(), "plaintext", + reinterpret_cast( + &JobPrivate::gnomeKeyring_readCb), + this, nullptr)) + q->emitFinishedWithError(OtherError, tr("Unknown error")); + break; + + case Backend_Kwallet4: + kwalletReadPasswordScheduledStartImpl(KWALLET4_DBUS_IFACE, KWALLET4_DBUS_PATH, this); + break; + case Backend_Kwallet5: + kwalletReadPasswordScheduledStartImpl(KWALLET5_DBUS_IFACE, KWALLET5_DBUS_PATH, this); + break; + case Backend_Kwallet6: + kwalletReadPasswordScheduledStartImpl(KWALLET6_DBUS_IFACE, KWALLET6_DBUS_PATH, this); + break; + } +} + +void JobPrivate::kwalletWalletFound(QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + const QDBusPendingReply reply = *watcher; + // Don't timeout after 25s, but 24 days + // This allows to wait for user to unlock wallet, e.g. at Plasma startup + iface->setTimeout(0x7FFFFFFF); + + const QDBusPendingReply pendingReply = iface->open(reply.value(), 0, q->service()); + auto pendingWatcher = new QDBusPendingCallWatcher(pendingReply, this); + connect(pendingWatcher, &QDBusPendingCallWatcher::finished, this, + &JobPrivate::kwalletOpenFinished); +} + +static QPair mapGnomeKeyringError(int result) +{ + Q_ASSERT(result != GnomeKeyring::RESULT_OK); + + switch (result) { + case GnomeKeyring::RESULT_DENIED: + return qMakePair(AccessDenied, QObject::tr("Access to keychain denied")); + case GnomeKeyring::RESULT_NO_KEYRING_DAEMON: + return qMakePair(NoBackendAvailable, QObject::tr("No keyring daemon")); + case GnomeKeyring::RESULT_ALREADY_UNLOCKED: + return qMakePair(OtherError, QObject::tr("Already unlocked")); + case GnomeKeyring::RESULT_NO_SUCH_KEYRING: + return qMakePair(OtherError, QObject::tr("No such keyring")); + case GnomeKeyring::RESULT_BAD_ARGUMENTS: + return qMakePair(OtherError, QObject::tr("Bad arguments")); + case GnomeKeyring::RESULT_IO_ERROR: + return qMakePair(OtherError, QObject::tr("I/O error")); + case GnomeKeyring::RESULT_CANCELLED: + return qMakePair(OtherError, QObject::tr("Cancelled")); + case GnomeKeyring::RESULT_KEYRING_ALREADY_EXISTS: + return qMakePair(OtherError, QObject::tr("Keyring already exists")); + case GnomeKeyring::RESULT_NO_MATCH: + return qMakePair(EntryNotFound, QObject::tr("No match")); + default: + break; + } + + return qMakePair(OtherError, QObject::tr("Unknown error")); +} + +void JobPrivate::gnomeKeyring_readCb(int result, const char *string, JobPrivate *self) +{ + if (result == GnomeKeyring::RESULT_OK) { + if (self->mode == JobPrivate::Text) + self->data = QByteArray(string); + else + self->data = QByteArray::fromBase64(string); + + self->q->emitFinished(); + } else if (self->mode == JobPrivate::Text) { + self->mode = JobPrivate::Binary; + if (!GnomeKeyring::find_network_password( + self->key.toUtf8().constData(), self->q->service().toUtf8().constData(), + "base64", + reinterpret_cast( + &JobPrivate::gnomeKeyring_readCb), + self, nullptr)) + self->q->emitFinishedWithError(OtherError, tr("Unknown error")); + } else { + const QPair errorResult = mapGnomeKeyringError(result); + self->q->emitFinishedWithError(errorResult.first, errorResult.second); + } +} + +void ReadPasswordJobPrivate::fallbackOnError(const QDBusError &err) +{ + PlainTextStore plainTextStore(q->service(), q->settings()); + + if (q->insecureFallback() && plainTextStore.contains(key)) { + mode = plainTextStore.readMode(key); + data = plainTextStore.readData(key); + + if (plainTextStore.error() != NoError) + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + else + q->emitFinished(); + } else { + if (err.type() == QDBusError::ServiceUnknown) // KWalletd not running + q->emitFinishedWithError(NoBackendAvailable, tr("No keychain service available")); + else + q->emitFinishedWithError( + OtherError, + tr("Could not open wallet: %1; %2") + .arg(QDBusError::errorString(err.type()), err.message())); + } +} + +void ReadPasswordJobPrivate::kwalletOpenFinished(QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + const QDBusPendingReply reply = *watcher; + + if (reply.isError()) { + fallbackOnError(reply.error()); + return; + } + + PlainTextStore plainTextStore(q->service(), q->settings()); + + if (plainTextStore.contains(key)) { + // We previously stored data in the insecure QSettings, but now have KWallet available. + // Do the migration + + data = plainTextStore.readData(key); + const WritePasswordJobPrivate::Mode mode = plainTextStore.readMode(key); + plainTextStore.remove(key); + + q->emitFinished(); + + auto j = new WritePasswordJob(q->service(), nullptr); + j->setSettings(q->settings()); + j->setKey(key); + j->setAutoDelete(true); + if (mode == WritePasswordJobPrivate::Binary) + j->setBinaryData(data); + else if (mode == WritePasswordJobPrivate::Text) + j->setTextData(QString::fromUtf8(data)); + else + Q_ASSERT(false); + + j->start(); + + return; + } + + walletHandle = reply.value(); + + if (walletHandle < 0) { + q->emitFinishedWithError(AccessDenied, tr("Access to keychain denied")); + return; + } + + const QDBusPendingReply nextReply = + iface->entryType(walletHandle, q->service(), key, q->service()); + auto nextWatcher = new QDBusPendingCallWatcher(nextReply, this); + connect(nextWatcher, &QDBusPendingCallWatcher::finished, this, + &ReadPasswordJobPrivate::kwalletEntryTypeFinished); +} + +// Must be in sync with KWallet::EntryType (kwallet.h) +enum KWalletEntryType { Unknown = 0, Password, Stream, Map }; + +void ReadPasswordJobPrivate::kwalletEntryTypeFinished(QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + if (watcher->isError()) { + const auto err = watcher->error(); + q->emitFinishedWithError(OtherError, + tr("Could not determine data type: %1; %2") + .arg(QDBusError::errorString(err.type()), err.message())); + return; + } + + const QDBusPendingReply reply = *watcher; + const int value = reply.value(); + + switch (value) { + case Unknown: + q->emitFinishedWithError(EntryNotFound, tr("Entry not found")); + return; + case Password: + mode = Text; + break; + case Stream: + mode = Binary; + break; + case Map: + q->emitFinishedWithError(EntryNotFound, tr("Unsupported entry type 'Map'")); + return; + default: + q->emitFinishedWithError(OtherError, tr("Unknown kwallet entry type '%1'").arg(value)); + return; + } + + const auto nextReply = (mode == Text) + ? QDBusPendingCall(iface->readPassword(walletHandle, q->service(), key, q->service())) + : QDBusPendingCall(iface->readEntry(walletHandle, q->service(), key, q->service())); + auto nextWatcher = new QDBusPendingCallWatcher(nextReply, this); + connect(nextWatcher, &QDBusPendingCallWatcher::finished, this, + &ReadPasswordJobPrivate::kwalletFinished); +} + +void ReadPasswordJobPrivate::kwalletFinished(QDBusPendingCallWatcher *watcher) +{ + if (!watcher->isError()) { + if (mode == Binary) { + QDBusPendingReply reply = *watcher; + if (reply.isValid()) { + data = reply.value(); + } + } else { + QDBusPendingReply reply = *watcher; + if (reply.isValid()) { + data = reply.value().toUtf8(); + } + } + } + + JobPrivate::kwalletFinished(watcher); +} + +static void kwalletWritePasswordScheduledStart(const char *service, const char *path, + JobPrivate *priv) +{ + if (QDBusConnection::sessionBus().isConnected()) { + priv->iface = new org::kde::KWallet(QLatin1String(service), QLatin1String(path), + QDBusConnection::sessionBus(), priv); + const QDBusPendingReply reply = priv->iface->networkWallet(); + auto watcher = new QDBusPendingCallWatcher(reply, priv); + priv->connect(watcher, &QDBusPendingCallWatcher::finished, priv, + &JobPrivate::kwalletWalletFound); + } else { + // D-Bus is not reachable so none can tell us something about KWalletd + QDBusError err(QDBusError::NoServer, WritePasswordJobPrivate::tr("D-Bus is not running")); + priv->fallbackOnError(err); + } +} + +void WritePasswordJobPrivate::scheduledStart() +{ + auto descr = service; + if (service.isEmpty()) { + descr = key; + } else if (!key.isEmpty()) { + descr = key + "@" + service; + } + + switch (getKeyringBackend()) { + case Backend_LibSecretKeyring: { + if (!LibSecretKeyring::writePassword(descr, key, service, mode, data, this)) { + q->emitFinishedWithError(OtherError, tr("Unknown error")); + } + } break; + case Backend_GnomeKeyring: { + QString type; + QByteArray password; + + switch (mode) { + case JobPrivate::Text: + type = QLatin1String("plaintext"); + password = data; + break; + default: + type = QLatin1String("base64"); + password = data.toBase64(); + break; + } + + QByteArray service = q->service().toUtf8(); + if (!GnomeKeyring::store_network_password( + GnomeKeyring::GNOME_KEYRING_DEFAULT, descr.toUtf8().constData(), + key.toUtf8().constData(), service.constData(), type.toUtf8().constData(), + password.constData(), + reinterpret_cast( + &JobPrivate::gnomeKeyring_writeCb), + this, nullptr)) + q->emitFinishedWithError(OtherError, tr("Unknown error")); + } break; + + case Backend_Kwallet4: + kwalletWritePasswordScheduledStart(KWALLET4_DBUS_IFACE, KWALLET4_DBUS_PATH, this); + break; + case Backend_Kwallet5: + kwalletWritePasswordScheduledStart(KWALLET5_DBUS_IFACE, KWALLET5_DBUS_PATH, this); + break; + case Backend_Kwallet6: + kwalletWritePasswordScheduledStart(KWALLET6_DBUS_IFACE, KWALLET6_DBUS_PATH, this); + break; + } +} + +void WritePasswordJobPrivate::fallbackOnError(const QDBusError &err) +{ + if (!q->insecureFallback()) { + q->emitFinishedWithError(OtherError, + tr("Could not open wallet: %1; %2") + .arg(QDBusError::errorString(err.type()), err.message())); + return; + } + + PlainTextStore plainTextStore(q->service(), q->settings()); + plainTextStore.write(key, data, mode); + + if (plainTextStore.error() != NoError) + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + else + q->emitFinished(); +} + +void JobPrivate::gnomeKeyring_writeCb(int result, JobPrivate *self) +{ + if (result == GnomeKeyring::RESULT_OK) { + self->q->emitFinished(); + } else { + const QPair errorResult = mapGnomeKeyringError(result); + self->q->emitFinishedWithError(errorResult.first, errorResult.second); + } +} + +void JobPrivate::kwalletOpenFinished(QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + + if (reply.isError()) { + fallbackOnError(reply.error()); + return; + } + + PlainTextStore plainTextStore(q->service(), q->settings()); + if (plainTextStore.contains(key)) { + // If we had previously written to QSettings, but we now have a kwallet available, migrate + // and delete old insecure data + plainTextStore.remove(key); + } + + const int handle = reply.value(); + + if (handle < 0) { + q->emitFinishedWithError(AccessDenied, tr("Access to keychain denied")); + return; + } + + QDBusPendingReply nextReply; + + if (!data.isNull()) { + if (mode == Text) { + nextReply = iface->writePassword(handle, q->service(), key, QString::fromUtf8(data), + q->service()); + } else { + Q_ASSERT(mode == Binary); + nextReply = iface->writeEntry(handle, q->service(), key, data, q->service()); + } + } else { + nextReply = iface->removeEntry(handle, q->service(), key, q->service()); + } + + auto nextWatcher = new QDBusPendingCallWatcher(nextReply, this); + connect(nextWatcher, &QDBusPendingCallWatcher::finished, this, &JobPrivate::kwalletFinished); +} + +void JobPrivate::kwalletFinished(QDBusPendingCallWatcher *watcher) +{ + if (!watcher->isError()) { + if (mode == Binary) { + QDBusPendingReply reply = *watcher; + if (reply.isValid()) { + data = reply.value(); + } + } else { + QDBusPendingReply reply = *watcher; + if (reply.isValid()) { + data = reply.value().toUtf8(); + } + } + } + + q->emitFinished(); +} + +void DeletePasswordJobPrivate::scheduledStart() +{ + switch (getKeyringBackend()) { + case Backend_LibSecretKeyring: { + if (!LibSecretKeyring::deletePassword(key, q->service(), this)) { + q->emitFinishedWithError(OtherError, tr("Unknown error")); + } + } break; + case Backend_GnomeKeyring: { + if (!GnomeKeyring::delete_network_password( + key.toUtf8().constData(), q->service().toUtf8().constData(), + reinterpret_cast( + &JobPrivate::gnomeKeyring_writeCb), + this, nullptr)) + q->emitFinishedWithError(OtherError, tr("Unknown error")); + } break; + + case Backend_Kwallet4: + kwalletWritePasswordScheduledStart(KWALLET4_DBUS_IFACE, KWALLET4_DBUS_PATH, this); + break; + case Backend_Kwallet5: + kwalletWritePasswordScheduledStart(KWALLET5_DBUS_IFACE, KWALLET5_DBUS_PATH, this); + break; + case Backend_Kwallet6: + kwalletWritePasswordScheduledStart(KWALLET6_DBUS_IFACE, KWALLET6_DBUS_PATH, this); + break; + } +} + +void DeletePasswordJobPrivate::fallbackOnError(const QDBusError &err) +{ + QScopedPointer local(!q->settings() ? new QSettings(q->service()) : nullptr); + QSettings *actual = q->settings() ? q->settings() : local.data(); + + if (!q->insecureFallback()) { + q->emitFinishedWithError(OtherError, + tr("Could not open wallet: %1; %2") + .arg(QDBusError::errorString(err.type()), err.message())); + return; + } + + actual->remove(key); + actual->sync(); + + q->emitFinished(); +} + +bool QKeychain::isAvailable() +{ + return LibSecretKeyring::isAvailable() || GnomeKeyring::isAvailable() + || isKwalletAvailable(KWALLET6_DBUS_IFACE, KWALLET6_DBUS_PATH) + || isKwalletAvailable(KWALLET5_DBUS_IFACE, KWALLET5_DBUS_PATH); +} diff --git a/qtkeychain/keychain_win.cpp b/qtkeychain/keychain_win.cpp new file mode 100644 index 00000000..c470b908 --- /dev/null +++ b/qtkeychain/keychain_win.cpp @@ -0,0 +1,288 @@ +/****************************************************************************** + * Copyright (C) 2011-2015 Frank Osterfeld * + * * + * 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. For licensing and distribution * + * details, check the accompanying file 'COPYING'. * + *****************************************************************************/ +#include "keychain_p.h" +#include "plaintextstore_p.h" + +#include +#include +#include +#include + +#include +#include + +using namespace QKeychain; + +namespace { +const std::wstring PRODUCT_NAME = L"QtKeychain"; +const std::wstring ENCRYPTED_DATA_KEY = L"QtKeychain-encrypted data"; +const std::wstring ATTRIBUTE_KEY = L"QtKeychain Attrib"; + +constexpr quint64 MAX_ATTRIBUTE_SIZE = 256; +constexpr quint64 MAX_ATTRIBUTE_COUNT = 64; +constexpr qsizetype MAX_BLOB_SIZE = + CRED_MAX_CREDENTIAL_BLOB_SIZE + MAX_ATTRIBUTE_SIZE * MAX_ATTRIBUTE_COUNT; + +QString formatWinError(ulong errorCode) +{ + return QStringLiteral("WindowsError: %1: %2") + .arg(QString::number(errorCode, 16), + QString::fromWCharArray(_com_error(errorCode).ErrorMessage())); +} + +// decrpyted data, error +std::pair unprotectData(const QByteArray &encrypted) +{ + DATA_BLOB blob_in, blob_out; + + blob_in.pbData = + const_cast(reinterpret_cast(encrypted.data())); + blob_in.cbData = encrypted.size(); + + if (!CryptUnprotectData(&blob_in, nullptr, nullptr, nullptr, nullptr, 0, &blob_out)) { + return { {}, formatWinError(GetLastError()) }; + } + + QByteArray decrypted(reinterpret_cast(blob_out.pbData), blob_out.cbData); + SecureZeroMemory(blob_out.pbData, blob_out.cbData); + LocalFree(blob_out.pbData); + return { decrypted, {} }; +} + +// encrypted data, error +std::pair protectData(const QByteArray &data) +{ + DATA_BLOB blob_in, blob_out; + blob_in.pbData = + const_cast(reinterpret_cast(data.data())); + blob_in.cbData = data.size(); + if (!CryptProtectData(&blob_in, ENCRYPTED_DATA_KEY.data(), nullptr, nullptr, nullptr, 0, + &blob_out)) { + + return { {}, formatWinError(GetLastError()) }; + } + + QByteArray encrypted(reinterpret_cast(blob_out.pbData), blob_out.cbData); + LocalFree(blob_out.pbData); + return { encrypted, {} }; +} + +} // namespace + +#if defined(USE_CREDENTIAL_STORE) + +/*** + * The credentials store has a limit of CRED_MAX_CREDENTIAL_BLOB_SIZE (5* 512) + * As this might not be enough in some scenarios, for bigger payloads we use CryptProtectData which + * offers similar protection as CredWrite in combination with CredWrite. We distribute the protected + * payload to the PCREDENTIALW->CredentialBlob as well as PCREDENTIALW->AttributeCount. This + * increases the max payload size to CRED_MAX_CREDENTIAL_BLOB_SIZE + 64 * 256 = 18944. As the + * protected data requires more space than the original payload, the effective max payload is + * smaller than that. As we continue to use PCREDENTIALW as storage medium, the credentials are + * still roaming. + */ +void ReadPasswordJobPrivate::scheduledStart() +{ + PCREDENTIALW cred = {}; + + if (!CredReadW(reinterpret_cast(key.utf16()), CRED_TYPE_GENERIC, 0, &cred)) { + Error err; + QString msg; + switch (GetLastError()) { + case ERROR_NOT_FOUND: + err = EntryNotFound; + msg = tr("Password entry not found"); + break; + default: + err = OtherError; + msg = tr("Could not decrypt data"); + break; + } + + q->emitFinishedWithError(err, msg); + return; + } + + if (cred->AttributeCount == 0) { + data = QByteArray(reinterpret_cast(cred->CredentialBlob), cred->CredentialBlobSize); + } else { + QByteArray encrypted; + encrypted.reserve(CRED_MAX_CREDENTIAL_BLOB_SIZE + + cred->AttributeCount * MAX_ATTRIBUTE_SIZE); + encrypted.append(reinterpret_cast(cred->CredentialBlob), cred->CredentialBlobSize); + for (ulong i = 0; i < cred->AttributeCount; ++i) { + encrypted.append(reinterpret_cast(cred->Attributes[i].Value), + cred->Attributes[i].ValueSize); + } + const auto result = unprotectData(encrypted); + if (!result.second.isEmpty()) { + q->emitFinishedWithError(OtherError, + tr("Could not decrypt data: %1").arg(result.second)); + return; + } + data = result.first; + } + + CredFree(cred); + + q->emitFinished(); +} + +void WritePasswordJobPrivate::scheduledStart() +{ + CREDENTIALW cred = {}; + cred.Comment = const_cast(PRODUCT_NAME.data()); + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = const_cast(reinterpret_cast(key.utf16())); + cred.Persist = CRED_PERSIST_ENTERPRISE; + + QByteArray buffer; + std::vector attributes; + + if (data.size() < CRED_MAX_CREDENTIAL_BLOB_SIZE) { + cred.CredentialBlob = reinterpret_cast(data.data()); + cred.CredentialBlobSize = data.size(); + } else { + // data is too big for CredentialBlob + // we encrpyt it instead with CryptProtectData which also encrpyt the data with the users + // credentials The data is also protected with the roaming profile + { + auto result = protectData(data); + if (!result.second.isEmpty()) { + q->emitFinishedWithError(OtherError, + tr("Encryption failed: %1").arg(result.second)); + return; + } + if (result.first.size() > MAX_BLOB_SIZE) { + q->emitFinishedWithError(OtherError, + tr("Credential size exceeds maximum size of %1: %2") + .arg(QString::number(MAX_BLOB_SIZE), + QString::number(result.first.size()))); + return; + } + // the data must be valid outside of the scope of result + buffer = std::move(result.first); + } + + quint64 pos = 0; + auto read = [&buffer, &pos](const quint64 size, auto &dest, auto &sizeOut) { + dest = reinterpret_cast::type>( + buffer.data()) + + pos; + sizeOut = std::min(size, buffer.size() - pos); + pos += sizeOut; + }; + read(CRED_MAX_CREDENTIAL_BLOB_SIZE, cred.CredentialBlob, cred.CredentialBlobSize); + + cred.AttributeCount = + std::ceil((buffer.size() - pos) / static_cast(MAX_ATTRIBUTE_SIZE)); + attributes.resize(cred.AttributeCount, {}); + cred.Attributes = attributes.data(); + for (ulong i = 0; i < cred.AttributeCount; ++i) { + attributes[i].Keyword = const_cast(ATTRIBUTE_KEY.data()); + read(MAX_ATTRIBUTE_SIZE, attributes[i].Value, attributes[i].ValueSize); + } + } + if (CredWriteW(&cred, 0)) { + q->emitFinished(); + return; + } + + const DWORD err = GetLastError(); + + // Detect size-exceeded errors and provide nicer messages. + // Unfortunately these error codes aren't documented. + // Found empirically on Win10 1803 build 17134.523. + if (err == RPC_S_INVALID_BOUND) { + const QString::size_type maxTargetName = CRED_MAX_GENERIC_TARGET_NAME_LENGTH; + if (key.size() > maxTargetName) { + q->emitFinishedWithError( + OtherError, tr("Credential key exceeds maximum size of %1").arg(maxTargetName)); + return; + } + } + + q->emitFinishedWithError(OtherError, + tr("Writing credentials failed: %1").arg(formatWinError(err))); +} + +void DeletePasswordJobPrivate::scheduledStart() +{ + if (!CredDeleteW(reinterpret_cast(key.utf16()), CRED_TYPE_GENERIC, 0)) { + Error err; + QString msg; + switch (GetLastError()) { + case ERROR_NOT_FOUND: + err = EntryNotFound; + msg = tr("Password entry not found"); + break; + default: + err = OtherError; + msg = tr("Could not decrypt data"); + break; + } + + q->emitFinishedWithError(err, msg); + } else { + q->emitFinished(); + } +} +#else +void ReadPasswordJobPrivate::scheduledStart() +{ + PlainTextStore plainTextStore(q->service(), q->settings()); + QByteArray encrypted = plainTextStore.readData(key); + if (plainTextStore.error() != NoError) { + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + return; + } + + const auto result = unprotectData(encrypted); + if (!result.second.isEmpty()) { + q->emitFinishedWithError(OtherError, tr("Could not decrypt data: %1").arg(result.second)); + return; + } + data = result.first; + q->emitFinished(); +} + +void WritePasswordJobPrivate::scheduledStart() +{ + const auto result = protectData(data); + if (!result.second.isEmpty()) { + q->emitFinishedWithError(OtherError, tr("Encryption failed: %1").arg(result.second)); + return; + } + + PlainTextStore plainTextStore(q->service(), q->settings()); + plainTextStore.write(key, result.first, Binary); + if (plainTextStore.error() != NoError) { + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + return; + } + + q->emitFinished(); +} + +void DeletePasswordJobPrivate::scheduledStart() +{ + PlainTextStore plainTextStore(q->service(), q->settings()); + plainTextStore.remove(key); + if (plainTextStore.error() != NoError) { + q->emitFinishedWithError(plainTextStore.error(), plainTextStore.errorString()); + } else { + q->emitFinished(); + } +} +#endif + +bool QKeychain::isAvailable() +{ + return true; +} diff --git a/qtkeychain/libsecret.cpp b/qtkeychain/libsecret.cpp new file mode 100644 index 00000000..e8006e75 --- /dev/null +++ b/qtkeychain/libsecret.cpp @@ -0,0 +1,309 @@ +#if defined(HAVE_LIBSECRET) +# include +#endif + +#include "libsecret_p.h" + +#include + +#if defined(HAVE_LIBSECRET) +const SecretSchema *qtkeychainSchema(void) +{ + static const SecretSchema schema = { "org.qt.keychain", + SECRET_SCHEMA_DONT_MATCH_NAME, + { { "user", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "server", SECRET_SCHEMA_ATTRIBUTE_STRING }, + { "type", SECRET_SCHEMA_ATTRIBUTE_STRING } } }; + + return &schema; +} + +typedef struct +{ + QKeychain::JobPrivate *self; + QString user; + QString server; +} callbackArg; + +typedef void (*secret_password_lookup_t)(const SecretSchema *schema, GCancellable *cancellable, + GAsyncReadyCallback callback, gpointer user_data, + ...) G_GNUC_NULL_TERMINATED; +typedef gchar *(*secret_password_lookup_finish_t)(GAsyncResult *result, GError **error); +typedef void (*secret_password_store_t)(const SecretSchema *schema, const gchar *collection, + const gchar *label, const gchar *password, + GCancellable *cancellable, GAsyncReadyCallback callback, + gpointer user_data, ...) G_GNUC_NULL_TERMINATED; +typedef gboolean (*secret_password_store_finish_t)(GAsyncResult *result, GError **error); +typedef void (*secret_password_clear_t)(const SecretSchema *schema, GCancellable *cancellable, + GAsyncReadyCallback callback, gpointer user_data, + ...) G_GNUC_NULL_TERMINATED; +typedef gboolean (*secret_password_clear_finish_t)(GAsyncResult *result, GError **error); +typedef void (*secret_password_free_t)(gchar *password); +typedef GQuark (*secret_error_get_quark_t)(void) G_GNUC_CONST; + +static secret_password_lookup_t secret_password_lookup_fn = nullptr; +static secret_password_lookup_finish_t secret_password_lookup_finish_fn = nullptr; +static secret_password_store_t secret_password_store_fn = nullptr; +static secret_password_store_finish_t secret_password_store_finish_fn = nullptr; +static secret_password_clear_t secret_password_clear_fn = nullptr; +static secret_password_clear_finish_t secret_password_clear_finish_fn = nullptr; +static secret_password_free_t secret_password_free_fn = nullptr; +static secret_error_get_quark_t secret_error_get_quark_fn = nullptr; + +static QKeychain::Error gerrorToCode(const GError *error) +{ + if (error->domain != secret_error_get_quark_fn()) { + return QKeychain::OtherError; + } + + switch (error->code) { + case SECRET_ERROR_NO_SUCH_OBJECT: + return QKeychain::EntryNotFound; + case SECRET_ERROR_IS_LOCKED: + return QKeychain::AccessDenied; + default: + return QKeychain::OtherError; + } +} + +static void on_password_lookup(GObject *source, GAsyncResult *result, gpointer inst) +{ + GError *error = nullptr; + callbackArg *arg = (callbackArg *)inst; + gchar *password = secret_password_lookup_finish_fn(result, &error); + + Q_UNUSED(source); + + if (arg) { + if (error) { + QKeychain::Error code = gerrorToCode(error); + + arg->self->q->emitFinishedWithError(code, QString::fromUtf8(error->message)); + } else { + if (password) { + QByteArray raw = QByteArray(password); + switch (arg->self->mode) { + case QKeychain::JobPrivate::Binary: + arg->self->data = QByteArray::fromBase64(raw); + break; + case QKeychain::JobPrivate::Text: + default: + arg->self->data = raw; + } + + arg->self->q->emitFinished(); + } else if (arg->self->mode == QKeychain::JobPrivate::Text) { + arg->self->mode = QKeychain::JobPrivate::Binary; + secret_password_lookup_fn(qtkeychainSchema(), nullptr, on_password_lookup, arg, + "user", arg->user.toUtf8().constData(), "server", + arg->server.toUtf8().constData(), "type", "base64", + nullptr); + return; + } else { + arg->self->q->emitFinishedWithError(QKeychain::EntryNotFound, + QObject::tr("Entry not found")); + } + } + } + if (error) { + g_error_free(error); + } + + if (password) { + secret_password_free_fn(password); + } + + if (arg) { + delete arg; + } +} + +static void on_password_stored(GObject *source, GAsyncResult *result, gpointer inst) +{ + GError *error = nullptr; + QKeychain::JobPrivate *self = (QKeychain::JobPrivate *)inst; + + Q_UNUSED(source); + + secret_password_store_finish_fn(result, &error); + + if (self) { + if (error) { + self->q->emitFinishedWithError(gerrorToCode(error), QString::fromUtf8(error->message)); + } else { + self->q->emitFinished(); + } + } + if (error) { + g_error_free(error); + } +} + +static void on_password_cleared(GObject *source, GAsyncResult *result, gpointer inst) +{ + GError *error = nullptr; + QKeychain::JobPrivate *self = (QKeychain::JobPrivate *)inst; + gboolean removed = secret_password_clear_finish_fn(result, &error); + + Q_UNUSED(source); + if (self) { + if (error) { + self->q->emitFinishedWithError(gerrorToCode(error), QString::fromUtf8(error->message)); + } else { + Q_UNUSED(removed); + self->q->emitFinished(); + } + } + if (error) { + g_error_free(error); + } +} + +static QString modeToString(QKeychain::JobPrivate::Mode mode) +{ + switch (mode) { + case QKeychain::JobPrivate::Binary: + return "base64"; + default: + return "plaintext"; + } +} +#endif + +bool LibSecretKeyring::isAvailable() +{ +#if defined(HAVE_LIBSECRET) + const LibSecretKeyring &keyring = instance(); + if (!keyring.isLoaded()) + return false; + if (secret_password_lookup_fn == nullptr) + return false; + if (secret_password_lookup_finish_fn == nullptr) + return false; + if (secret_password_store_fn == nullptr) + return false; + if (secret_password_store_finish_fn == nullptr) + return false; + if (secret_password_clear_fn == nullptr) + return false; + if (secret_password_clear_finish_fn == nullptr) + return false; + if (secret_password_free_fn == nullptr) + return false; + if (secret_error_get_quark_fn == nullptr) + return false; + return true; +#else + return false; +#endif +} + +bool LibSecretKeyring::findPassword(const QString &user, const QString &server, + QKeychain::JobPrivate *self) +{ +#if defined(HAVE_LIBSECRET) + if (!isAvailable()) { + return false; + } + + self->mode = QKeychain::JobPrivate::Text; + self->data = QByteArray(); + + callbackArg *arg = new callbackArg; + arg->self = self; + arg->user = user; + arg->server = server; + + secret_password_lookup_fn(qtkeychainSchema(), nullptr, on_password_lookup, arg, "user", + user.toUtf8().constData(), "server", server.toUtf8().constData(), + "type", "plaintext", nullptr); + return true; +#else + Q_UNUSED(user) + Q_UNUSED(server) + Q_UNUSED(self) + return false; +#endif +} + +bool LibSecretKeyring::writePassword(const QString &display_name, const QString &user, + const QString &server, const QKeychain::JobPrivate::Mode mode, + const QByteArray &password, QKeychain::JobPrivate *self) +{ +#if defined(HAVE_LIBSECRET) + if (!isAvailable()) { + return false; + } + + QString type = modeToString(mode); + QByteArray pwd; + switch (mode) { + case QKeychain::JobPrivate::Binary: + pwd = password.toBase64(); + break; + default: + pwd = password; + break; + } + + secret_password_store_fn( + qtkeychainSchema(), SECRET_COLLECTION_DEFAULT, display_name.toUtf8().constData(), + pwd.constData(), nullptr, on_password_stored, self, "user", user.toUtf8().constData(), + "server", server.toUtf8().constData(), "type", type.toUtf8().constData(), nullptr); + return true; +#else + Q_UNUSED(display_name) + Q_UNUSED(user) + Q_UNUSED(server) + Q_UNUSED(mode) + Q_UNUSED(password) + Q_UNUSED(self) + return false; +#endif +} + +bool LibSecretKeyring::deletePassword(const QString &key, const QString &service, + QKeychain::JobPrivate *self) +{ +#if defined(HAVE_LIBSECRET) + if (!isAvailable()) { + return false; + } + + secret_password_clear_fn(qtkeychainSchema(), nullptr, on_password_cleared, self, "user", + key.toUtf8().constData(), "server", service.toUtf8().constData(), + nullptr); + return true; +#else + Q_UNUSED(key) + Q_UNUSED(service) + Q_UNUSED(self) + return false; +#endif +} + +LibSecretKeyring::LibSecretKeyring() : QLibrary(QLatin1String("secret-1"), 0) +{ +#ifdef HAVE_LIBSECRET + if (load()) { + secret_password_lookup_fn = (secret_password_lookup_t)resolve("secret_password_lookup"); + secret_password_lookup_finish_fn = + (secret_password_lookup_finish_t)resolve("secret_password_lookup_finish"); + secret_password_store_fn = (secret_password_store_t)resolve("secret_password_store"); + secret_password_store_finish_fn = + (secret_password_store_finish_t)resolve("secret_password_store_finish"); + secret_password_clear_fn = (secret_password_clear_t)resolve("secret_password_clear"); + secret_password_clear_finish_fn = + (secret_password_clear_finish_t)resolve("secret_password_clear_finish"); + secret_password_free_fn = (secret_password_free_t)resolve("secret_password_free"); + secret_error_get_quark_fn = (secret_error_get_quark_t)resolve("secret_error_get_quark"); + } +#endif +} + +LibSecretKeyring &LibSecretKeyring::instance() +{ + static LibSecretKeyring instance; + + return instance; +} diff --git a/qtkeychain/libsecret_p.h b/qtkeychain/libsecret_p.h new file mode 100644 index 00000000..d5f26005 --- /dev/null +++ b/qtkeychain/libsecret_p.h @@ -0,0 +1,29 @@ +#ifndef QTKEYCHAIN_LIBSECRET_P_H +#define QTKEYCHAIN_LIBSECRET_P_H + +#include + +#include "keychain_p.h" + +class LibSecretKeyring : public QLibrary +{ +public: + static bool isAvailable(); + + static bool findPassword(const QString &user, const QString &server, + QKeychain::JobPrivate *self); + + static bool writePassword(const QString &display_name, const QString &user, + const QString &server, const QKeychain::JobPrivate::Mode type, + const QByteArray &password, QKeychain::JobPrivate *self); + + static bool deletePassword(const QString &key, const QString &service, + QKeychain::JobPrivate *self); + +private: + LibSecretKeyring(); + + static LibSecretKeyring &instance(); +}; + +#endif diff --git a/org.kde.KWallet.xml b/qtkeychain/org.kde.KWallet.xml similarity index 98% rename from org.kde.KWallet.xml rename to qtkeychain/org.kde.KWallet.xml index 548c2f82..ec4bd6e9 100644 --- a/org.kde.KWallet.xml +++ b/qtkeychain/org.kde.KWallet.xml @@ -156,7 +156,7 @@ - + @@ -164,7 +164,7 @@ - + @@ -172,7 +172,7 @@ - + diff --git a/plaintextstore.cpp b/qtkeychain/plaintextstore.cpp similarity index 88% rename from plaintextstore.cpp rename to qtkeychain/plaintextstore.cpp index b9d02726..14470e52 100644 --- a/plaintextstore.cpp +++ b/qtkeychain/plaintextstore.cpp @@ -14,18 +14,26 @@ using namespace QKeychain; namespace { #ifdef Q_OS_WIN -inline QString dataKey(const QString &key) { return key; } +inline QString dataKey(const QString &key) +{ + return key; +} #else // Q_OS_WIN -inline QString dataKey(const QString &key) { return key + QLatin1String("/data"); } -inline QString typeKey(const QString &key) { return key + QLatin1String("/type"); } -#endif // Q_OS_WIN +inline QString dataKey(const QString &key) +{ + return key + QLatin1String("/data"); } - +inline QString typeKey(const QString &key) +{ + return key + QLatin1String("/type"); +} +#endif // Q_OS_WIN +} // namespace PlainTextStore::PlainTextStore(const QString &service, QSettings *settings) - : m_localSettings(settings ? 0 : new QSettings(service)) - , m_actualSettings(settings ? settings : m_localSettings.data()) - , m_error(NoError) + : m_localSettings(settings ? nullptr : new QSettings(service)), + m_actualSettings(settings ? settings : m_localSettings.data()), + m_error(NoError) { } diff --git a/plaintextstore_p.h b/qtkeychain/plaintextstore_p.h similarity index 97% rename from plaintextstore_p.h rename to qtkeychain/plaintextstore_p.h index 7ec05aac..974318e0 100644 --- a/plaintextstore_p.h +++ b/qtkeychain/plaintextstore_p.h @@ -15,7 +15,8 @@ namespace QKeychain { -class PlainTextStore { +class PlainTextStore +{ Q_DECLARE_TR_FUNCTIONS(QKeychain::PlainTextStore) public: @@ -42,6 +43,6 @@ class PlainTextStore { Error m_error; }; -} +} // namespace QKeychain #endif // QTKEYCHAIN_PLAINTEXTSTORE_P_H diff --git a/testclient.cpp b/testclient.cpp index 94189cf1..9d4be2f8 100644 --- a/testclient.cpp +++ b/testclient.cpp @@ -6,110 +6,127 @@ * or FITNESS FOR A PARTICULAR PURPOSE. For licensing and distribution * * details, check the accompanying file 'COPYING'. * *****************************************************************************/ -#include + +#include +#ifdef Q_OS_DARWIN +# include +#else +# include +#endif + #include -#include "keychain.h" +#include "qtkeychain/keychain.h" #include using namespace QKeychain; -static int printUsage() { +static int printUsage() +{ std::cerr << "testclient store " << std::endl; std::cerr << "testclient restore " << std::endl; std::cerr << "testclient delete " << std::endl; return 1; } -int main( int argc, char** argv ) { - QCoreApplication app( argc, argv ); +int main(int argc, char **argv) +{ +#ifdef Q_OS_DARWIN + // Since we use NSNotificationCenter under the hood in keychain_apple, + // we use QGuiApplication to automatically configure the platform + // integration stuff done in this class and not in QCoreApplication + QGuiApplication app(argc, argv); +#else + QCoreApplication app(argc, argv); +#endif const QStringList args = app.arguments(); - if ( args.count() < 2 ) + if (args.count() < 2) return printUsage(); QStringList::ConstIterator it = args.constBegin(); ++it; - if ( *it == QLatin1String("store") ) { - if ( ++it == args.constEnd() ) + if (*it == QLatin1String("store")) { + if (++it == args.constEnd()) return printUsage(); const QString acc = *it; - if ( ++it == args.constEnd() ) + if (++it == args.constEnd()) return printUsage(); const QString pass = *it; - if ( ++it != args.constEnd() ) + if (++it != args.constEnd()) return printUsage(); - WritePasswordJob job( QLatin1String("qtkeychain-testclient") ); - job.setAutoDelete( false ); - job.setKey( acc ); - job.setTextData( pass ); + WritePasswordJob job(QLatin1String("qtkeychain-testclient")); + job.setAutoDelete(false); + job.setKey(acc); + job.setTextData(pass); QEventLoop loop; - job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); + job.connect(&job, &Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); - if ( job.error() ) { + if (job.error()) { std::cerr << "Storing password failed: " << qPrintable(job.errorString()) << std::endl; return 1; } std::cout << "Password stored successfully" << std::endl; - } else if ( *it == QLatin1String("bstore") ) { - if ( ++it == args.constEnd() ) + } else if (*it == QLatin1String("bstore")) { + if (++it == args.constEnd()) return printUsage(); const QString acc = *it; - if ( ++it == args.constEnd() ) + if (++it == args.constEnd()) return printUsage(); const QString pass = *it; - if ( ++it != args.constEnd() ) + if (++it != args.constEnd()) return printUsage(); - WritePasswordJob job( QLatin1String("qtkeychain-testclient") ); - job.setAutoDelete( false ); - job.setKey( acc ); - job.setBinaryData( pass.toUtf8() ); + WritePasswordJob job(QLatin1String("qtkeychain-testclient")); + job.setAutoDelete(false); + job.setKey(acc); + job.setBinaryData(pass.toUtf8()); QEventLoop loop; - job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); + job.connect(&job, &Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); - if ( job.error() ) { - std::cerr << "Storing binary password failed: " - << qPrintable(job.errorString()) << std::endl; + if (job.error()) { + std::cerr << "Storing binary password failed: " << qPrintable(job.errorString()) + << std::endl; return 1; } std::cout << "Password stored successfully" << std::endl; - } else if ( *it == QLatin1String("restore") ) { - if ( ++it == args.constEnd() ) + } else if (*it == QLatin1String("restore")) { + if (++it == args.constEnd()) return printUsage(); const QString acc = *it; - if ( ++it != args.constEnd() ) + if (++it != args.constEnd()) return printUsage(); - ReadPasswordJob job( QLatin1String("qtkeychain-testclient") ); - job.setAutoDelete( false ); - job.setKey( acc ); + ReadPasswordJob job(QLatin1String("qtkeychain-testclient")); + job.setAutoDelete(false); + job.setKey(acc); QEventLoop loop; - job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); + job.connect(&job, &Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); const QString pw = job.textData(); - if ( job.error() ) { - std::cerr << "Restoring password failed: " << qPrintable(job.errorString()) << std::endl; + if (job.error()) { + std::cerr << "Restoring password failed: " << qPrintable(job.errorString()) + << std::endl; return 1; } std::cout << qPrintable(pw) << std::endl; - } else if ( *it == QLatin1String("delete") ) { - if ( ++it == args.constEnd() ) + } else if (*it == QLatin1String("delete")) { + if (++it == args.constEnd()) return printUsage(); const QString acc = *it; - if ( ++it != args.constEnd() ) + if (++it != args.constEnd()) return printUsage(); - DeletePasswordJob job( QLatin1String("qtkeychain-testclient") ); - job.setAutoDelete( false ); - job.setKey( acc ); + DeletePasswordJob job(QLatin1String("qtkeychain-testclient")); + job.setAutoDelete(false); + job.setKey(acc); QEventLoop loop; - job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); + job.connect(&job, &Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); - if ( job.error() ) { + if (job.error()) { std::cerr << "Deleting password failed: " << qPrintable(job.errorString()) << std::endl; return 1; } @@ -118,4 +135,3 @@ int main( int argc, char** argv ) { return printUsage(); } } - diff --git a/translations/qtkeychain_fr.ts b/translations/qtkeychain_fr.ts new file mode 100644 index 00000000..35c60b9b --- /dev/null +++ b/translations/qtkeychain_fr.ts @@ -0,0 +1,267 @@ + + + + + QKeychain::DeletePasswordJobPrivate + + + Password entry not found + Mot de passe introuvable + + + + Could not decrypt data + Impossible de déchiffrer les données + + + + + Unknown error + Erreur inconnue + + + + Could not open wallet: %1; %2 + Impossible d'ouvrir le portefeuille : %1; %2 + + + + Password not found + Mot de passe introuvable + + + + QKeychain::JobPrivate + + + Unknown error + Erreur inconnue + + + + Access to keychain denied + Accès au trousseau refusé + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + Impossible de stocker les données dans les paramètres : Erreur d'accès + + + + Could not store data in settings: format error + Impossible de stocker les données dans les paramètres : Erreur de format + + + + Could not delete data from settings: access error + Impossible de supprimer les données depuis les paramètres : Erreur d'accès + + + + Could not delete data from settings: format error + Impossible de supprimer les données depuis les paramètres : Erreur de format + + + + Entry not found + Entrée introuvable + + + + QKeychain::ReadPasswordJobPrivate + + + + Unknown error + Erreur inconnue + + + + D-Bus is not running + D-Bus n'est pas en cours d'exécution + + + + No keychain service available + Aucun service de trousseau disponible + + + + Could not open wallet: %1; %2 + Impossible d'ouvrir le trousseau : %1; %2 + + + + Access to keychain denied + Accès au trousseau refusé + + + + Could not determine data type: %1; %2 + Impossible de déterminer le type de données : %1: %2 + + + + Unsupported entry type 'Map' + Type d'entrée non supporté 'Map' + + + + Unknown kwallet entry type '%1' + Type de trousseau inconnu '%1' + + + Could not read password: %1; %2 + Impossible de lire le mot de passe : %1; %2 + + + + + Password not found + Mot de passe introuvable + + + + Entry not found + Entrée introuvable + + + + Password entry not found + Entrée de mot de passe introuvable + + + + + Could not decrypt data + Impossible de déchiffrer les données + + + + QKeychain::WritePasswordJobPrivate + + + + Unknown error + Erreur inconnue + + + + D-Bus is not running + D-Bus n'est pas en cours d'exécution + + + + Could not open wallet: %1; %2 + Impossible d'ouvrir le trousseau : %1; %2 + + + Access to keychain denied + Accès au trousseau refusé + + + Could not delete encrypted data from settings: access error + Impossible de supprimer des données chiffrées dans les paramètres : Erreur d'accès + + + Could not delete encrypted data from settings: format error + Impossible de supprimer des données chiffrées dans les paramètres : Erreur de format + + + + + Encryption failed + Le chiffrement a échoué + + + Could not store encrypted data in settings: access error + Impossible de stocker des données chiffrées dans les paramètres : Erreur d'accès + + + Could not store encrypted data in settings: format error + Impossible de stocker des données chiffrées dans les paramètres : Erreur de format + + + + Password not found + Mot de passe introuvable + + + + QObject + + + Access to keychain denied + Accès au trousseau refusé + + + + No keyring daemon + Aucun démon de trousseau + + + + Already unlocked + Déjà déverrouillé + + + + No such keyring + Aucun trousseau + + + + Bad arguments + Mauvais arguments + + + + I/O error + Erreur d'E/S + + + + Cancelled + Annulé + + + + Keyring already exists + Trousseau déjà existant + + + + No match + Aucune correspondance + + + + Unknown error + Erreur inconnue + + + + OS X Keychain error (OSStatus %1) + OS X Keychain error (OSStatus %1) + + + + %1 (OSStatus %2) + %1 (OSStatus %2) + + + + Entry not found + Entrée introuvable + + + + error 0x%1: %2 + Erreur 0x%1 : %2 + + + diff --git a/translations/qtkeychain_ka.ts b/translations/qtkeychain_ka.ts new file mode 100644 index 00000000..8fee67be --- /dev/null +++ b/translations/qtkeychain_ka.ts @@ -0,0 +1,326 @@ + + + + + KeyChainClass + + + Read key failed: %1 + გასაღების წაკითხვა ჩავარდა: %1 + + + + Write key failed: %1 + გასაღების ჩაწერის შეცდომა: %1 + + + + Delete key failed: %1 + გასაღების წაშლის შეცდომა: %1 + + + + QKeychain::DeletePasswordJobPrivate + + + Could not open keystore + გასაღებების საცავის გახსნა შეუძლებელია + + + + Could not remove private key from keystore + გასაღებების საცავიდან პირადი გასაღების წაშლა შეუძლებელია + + + + Password not found + პაროლი აღმოჩენილი არაა + + + + + Unknown error + უცნობი შეცდომა + + + + Could not open wallet: %1; %2 + გახსნის შეცდომა საფულისთვის: %1; %2 + + + + Password entry not found + პაროლის ჩანაწერი აღმოჩენილი არაა + + + + Could not decrypt data + მონაცემების გაშიფვრის შეცდომა + + + + QKeychain::JobPrivate + + + Unknown error + უცნობი შეცდომა + + + + Access to keychain denied + ბრელოკთან წვდომა აკრძალულია + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + მონაცემების პარამეტრებში შენახვა შეუძლებელია: წვდომის შეცდომა + + + + Could not store data in settings: format error + მონაცემების პარამეტრებში შენახვა შეუძლებელია: ფორმატის შეცდომა + + + + Could not delete data from settings: access error + პარამეტრებიდან მონაცემების წაშლის შეცდომა: წვდომის შეცდომა + + + + Could not delete data from settings: format error + პარამეტრებიდან მონაცემების წაშლის შეცდომა: ფორმატის შეცდომა + + + + Entry not found + ჩანაწერი ვერ ვიპოვე + + + + QKeychain::ReadPasswordJobPrivate + + + + Entry not found + ჩანაწერი ვერ ვიპოვე + + + + Could not open keystore + გასაღებების საცავის გახსნა შეუძლებელია + + + + Could not retrieve private key from keystore + გასაღებების საცავიდან პირადი გასაღების მიღება შეუძლებელია + + + + Could not create decryption cipher + გაშიფვრის შიფრის შექმნა შეუძლებელია + + + + Password not found + პაროლი აღმოჩენილი არაა + + + + D-Bus is not running + D-Bus is გაშვებული არაა + + + + + Unknown error + უცნობი შეცდომა + + + + No keychain service available + ბრელოკი სერვისი ხელმისაწვდომი არაა + + + + Could not open wallet: %1; %2 + გახსნის შეცდომა საფულისთვის: %1; %2 + + + + Access to keychain denied + ბრელოკთან წვდომა აკრძალულია + + + + Could not determine data type: %1; %2 + მონაცემთა ტიპის დადგენა შეუძლებელია: %1; %2 + + + + Unsupported entry type 'Map' + მხარდაუჭერელი ჩანაწერის ტიპი 'Map' + + + + Unknown kwallet entry type '%1' + უცნობი kwallet-ის ჩანაწერის ტიპი '%1' + + + + Password entry not found + პაროლის ჩანაწერი აღმოჩენილი არაა + + + + Could not decrypt data + მონაცემების გაშიფვრის შეცდომა + + + + + Could not decrypt data: %1 + მონაცემების გაშიფვრის შეცდომა: %1 + + + + QKeychain::WritePasswordJobPrivate + + + Could not open keystore + გასაღებების საცავის გახსნა შეუძლებელია + + + + Could not create private key generator + პირადი გასაღების გენერატორის შექმნა შეუძლებელია + + + + Could not generate new private key + ახალი პირადი გასაღების გენერაცია შეუძლებელია + + + + Could not retrieve private key from keystore + გასაღებების საცავიდან პირადი გასაღების მიღება შეუძლებელია + + + + Could not create encryption cipher + დაშიფვრის შიფრის შექმნა შეუძლებელია + + + + Could not encrypt data + მონაცემების დაშიფვრა შეუძლებელია + + + + Password not found + პაროლი აღმოჩენილი არაა + + + + D-Bus is not running + D-Bus is გაშვებული არაა + + + + + Unknown error + უცნობი შეცდომა + + + + Could not open wallet: %1; %2 + გახსნის შეცდომა საფულისთვის: %1; %2 + + + + + Encryption failed: %1 + დაშიფვრის შეცდომა: %1 + + + + Credential size exceeds maximum size of %1: %2 + ავტორიზაციის დეტალების ზომა %1-ზე მეტია: %2 + + + + Credential key exceeds maximum size of %1 + ავტორიზაციის დეტალების ზომა %1-ზე მეტია + + + + Writing credentials failed: %1 + ავტორიზაციის დეტალების ჩაწერა ჩავარდა: %1 + + + + QObject + + + error 0x%1: %2 + შეცდომა 0x%1: %2 + + + + Access to keychain denied + ბრელოკთან წვდომა აკრძალულია + + + + No keyring daemon + ბრელოკის დემონის გარეშე + + + + Already unlocked + უკვე განბლოკილია + + + + No such keyring + ასეთი ბრელოკი არ არსებობს + + + + Bad arguments + არასწორი არგუმენტები + + + + I/O error + I/O შეცდომა + + + + Cancelled + გაუქმებულია + + + + Keyring already exists + ბრელოკი უნდა არსებობს + + + + No match + დამთხვევის გარეშე + + + + Unknown error + უცნობი შეცდომა + + + + Entry not found + ჩანაწერი ვერ ვიპოვე + + + diff --git a/translations/qtkeychain_nl.ts b/translations/qtkeychain_nl.ts new file mode 100644 index 00000000..7033a984 --- /dev/null +++ b/translations/qtkeychain_nl.ts @@ -0,0 +1,320 @@ + + + + + KeyChainClass + + + Read key failed: %1 + De sleutel kan niet worden ingelezen: %1 + + + + Write key failed: %1 + De sleutel kan niet worden weggeschreven: %1 + + + + Delete key failed: %1 + De sleutel kan niet worden verwijderd: %1 + + + + QKeychain::DeletePasswordJobPrivate + + + Could not open keystore + De sleutelbos kan niet worden geopend + + + + Could not remove private key from keystore + De privésleutel kan niet worden verwijderd uit de sleutelbos + + + + Password not found + Het wachtwoord is niet gevonden + + + + + Unknown error + Onbekende foutmelding + + + + Could not open wallet: %1; %2 + De sleutelbos kan niet worden geopend: %1; %2 + + + + Password entry not found + Het wachtwoord is niet gevonden + + + + Could not decrypt data + De gegevens kunnen niet worden ongrendeld + + + + QKeychain::JobPrivate + + + Unknown error + Onbekende foutmelding + + + + Access to keychain denied + U heeft geen toegang tot de sleutelbos + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + De gegevens kunnen niet worden opgeslagen in de instellingen: toegangsfout + + + + Could not store data in settings: format error + De gegevens kunnen niet worden opgeslagen in de instellingen: opmaakfout + + + + Could not delete data from settings: access error + De gegevens kunnen niet worden verwijderd uit de instellingen: toegangsfout + + + + Could not delete data from settings: format error + De gegevens kunnen niet worden verwijderd uit de instellingen: opmaakfout + + + + Entry not found + Het item is niet gevonden + + + + QKeychain::ReadPasswordJobPrivate + + + + Entry not found + Het item is niet gevonden + + + + Could not open keystore + De sleutelbos kan niet worden geopend + + + + Could not retrieve private key from keystore + De privésleutel kan niet worden verwijderd uit de sleutelbos + + + + Could not create decryption cipher + De ontgrendelcode kan niet worden aangemaakt + + + + Password not found + Het wachtwoord is niet gevonden + + + + D-Bus is not running + D-Bus is niet actief + + + + + Unknown error + Onbekende foutmelding + + + + No keychain service available + De sleutelbosdienst is niet beschikbaar + + + + Could not open wallet: %1; %2 + De sleutelbos kan niet worden geopend: %1; %2 + + + + Access to keychain denied + U heeft geen toegang tot de sleutelbos + + + + Could not determine data type: %1; %2 + De gegevensdrager kan niet worden bepaald: %1; %2 + + + + Unsupported entry type 'Map' + Niet-ondersteund itemtype ‘Map’ + + + + Unknown kwallet entry type '%1' + Onbekend KWallet-item van type ‘%1’ + + + + Password entry not found + Het wachtwoord is niet gevonden + + + + + Could not decrypt data + De gegevens kunnen niet worden ongrendeld + + + + QKeychain::WritePasswordJobPrivate + + + Could not open keystore + De sleutelbos kan niet worden geopend + + + + Could not create private key generator + De privésleutelgenerator kan niet worden gestart + + + + Could not generate new private key + Er kan geen nieuwe privésleutel worden gegenereerd + + + + Could not retrieve private key from keystore + De privésleutel kan niet worden opgehaald uit de sleutelbos + + + + Could not create encryption cipher + De vergrendelcode kan niet worden aangemaakt + + + + Could not encrypt data + De gegevens kunnen niet worden ontgrendeld + + + + Password not found + Het wachtwoord is niet gevonden + + + + D-Bus is not running + D-Bus is niet actief + + + + + Unknown error + Onbekende foutmelding + + + + Could not open wallet: %1; %2 + De sleutelbos kan niet worden geopend: %1; %2 + + + + Credential size exceeds maximum size of %1 + De omvang overschrijdt de maximumomvang van %1 + + + + Credential key exceeds maximum size of %1 + De sleutel overschrijdt de maximumomvang van %1 + + + + Writing credentials failed: Win32 error code %1 + De gegevens kunnen niet worden weggeschreven: Win32-foutcode %1 + + + + Encryption failed + Het ontgrendelen is mislukt + + + + QObject + + + error 0x%1: %2 + foutmelding 0x%1: %2 + + + + Access to keychain denied + U heeft geen toegang tot de sleutelbos + + + + No keyring daemon + De sleutelbosdienst is niet actief + + + + Already unlocked + De sleutelbos is reeds ontgrendeld + + + + No such keyring + De sleutelbos bestaat niet + + + + Bad arguments + Onjuiste opties + + + + I/O error + I/O-fout + + + + Cancelled + Geannuleerd + + + + Keyring already exists + De sleutelbos bestaat reeds + + + + No match + Er zijn geen overeenkomsten + + + + Unknown error + Onbekende foutmelding + + + + Entry not found + Het item is niet gevonden + + + diff --git a/translations/qtkeychain_ru.ts b/translations/qtkeychain_ru.ts new file mode 100644 index 00000000..1d611291 --- /dev/null +++ b/translations/qtkeychain_ru.ts @@ -0,0 +1,241 @@ + + + + + QKeychain::DeletePasswordJobPrivate + + + + Unknown error + Неизвестная ошибка + + + + Could not open wallet: %1; %2 + Не удалось открыть бумажник: %1; %2 + + + + Password entry not found + Пароль не найден + + + + Could not decrypt data + Не удалось расшифровать данные + + + + QKeychain::JobPrivate + + + Unknown error + Неизвестная ошибка + + + + Access to keychain denied + Доступ к связке ключей запрещён + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + Не удалось сохранить данные в настройках: ошибка доступа + + + + Could not store data in settings: format error + Не удалось сохранить данные в настройках: ошибка формата + + + + Could not delete data from settings: access error + Не удалось удалить данные из настроек: ошибка доступа + + + + Could not delete data from settings: format error + Не удалось удалить данные из настроек: ошибка формата + + + + Entry not found + Запись не найдена + + + + QKeychain::ReadPasswordJobPrivate + + + Password not found + Пароль не найден + + + + D-Bus is not running + D-Bus не запущен + + + + + Unknown error + Неизвестная ошибка + + + + No keychain service available + Служба связки ключей недоступна + + + + Could not open wallet: %1; %2 + Не удалось открыть кошелёк: %1; %2 + + + + Access to keychain denied + Доступ к связке ключей запрещён + + + + Could not determine data type: %1; %2 + Не удалось определить тип данных: %1; %2 + + + + Entry not found + Запись не найдена + + + + Unsupported entry type 'Map' + Неподдерживаемый тип записи 'Map' + + + + Unknown kwallet entry type '%1' + Неизвестный тип записи kwallet '%1' + + + + Password entry not found + Пароль не найден + + + + + Could not decrypt data + Не удалось расшифровать данные + + + + QKeychain::WritePasswordJobPrivate + + + D-Bus is not running + D-Bus не запущен + + + + + Unknown error + Неизвестная ошибка + + + + Could not open wallet: %1; %2 + Не удалось открыть кошелёк: %1; %2 + + + + Credential size exceeds maximum size of %1 + Учётные данные превышают максимальный размер %1 + + + + Credential key exceeds maximum size of %1 + Ключ учётных данных превышает максимальный размер %1 + + + + Writing credentials failed: Win32 error code %1 + Не удалось сохранить учётные данные: код ошибки win32 %1 + + + + Encryption failed + Шифрование не удалось + + + + QObject + + + OS X Keychain error (OSStatus %1) + Ошибка связки ключей OS X (OSStatus %1) + + + + %1 (OSStatus %2) + %1 (OSStatus %2) + + + + Access to keychain denied + Доступ к связке ключей запрещён + + + + No keyring daemon + Нет демона связки ключей + + + + Already unlocked + Уже разблокировано + + + + No such keyring + Связка ключей не найдена + + + + Bad arguments + Неверные аргументы + + + + I/O error + Ошибка ввода/вывода + + + + Cancelled + Отменено + + + + Keyring already exists + Связка ключей уже существует + + + + No match + Нет совпадений + + + + Unknown error + Неизвестная ошибка + + + + Entry not found + Запись не найдена + + + diff --git a/translations/qtkeychain_sv.ts b/translations/qtkeychain_sv.ts new file mode 100644 index 00000000..bfa5bc30 --- /dev/null +++ b/translations/qtkeychain_sv.ts @@ -0,0 +1,334 @@ + + + + sv_SE + + Daniel Nylander <github@danielnylander.se> + + + + Poedit 3.6 + Project-Id-Version,POT-Creation-Date,PO-Revision-Date,Last-Translator,Language-Team,Language,MIME-Version,Content-Type,Content-Transfer-Encoding,Plural-Forms,X-Language,X-Qt-Contexts,X-Generator + + KeyChainClass + + + Read key failed: %1 + Läsning av nyckel misslyckades: %1 + + + + Write key failed: %1 + Skrivning av nyckel misslyckades: %1 + + + + Delete key failed: %1 + Borttagning av nyckel misslyckades: %1 + + + + QKeychain::DeletePasswordJobPrivate + + + Could not open keystore + Det gick inte att öppna nyckellagret + + + + Could not remove private key from keystore + Det gick inte att ta bort den privata nyckeln från nyckelagret + + + + Password not found + Lösenordet hittades inte + + + + + Unknown error + Okänt fel + + + + Could not open wallet: %1; %2 + Det gick inte att öppna plånboken: %1; %2 + + + + Password entry not found + Lösenordet hittades inte + + + + Could not decrypt data + Kunde inte avkryptera data + + + + QKeychain::JobPrivate + + + Unknown error + Okänt fel + + + + Access to keychain denied + Åtkomst till nyckelring nekades + + + + QKeychain::PlainTextStore + + + Could not store data in settings: access error + Det gick inte att lagra data i inställningarna: åtkomstfel + + + + Could not store data in settings: format error + Det gick inte att lagra data i inställningarna: formatfel + + + + Could not delete data from settings: access error + Det gick inte att ta bort data från inställningarna: åtkomstfel + + + + Could not delete data from settings: format error + Det gick inte att ta bort data från inställningarna: formatfel + + + + Entry not found + Posten hittades inte + + + + QKeychain::ReadPasswordJobPrivate + + + + Entry not found + Posten hittades inte + + + + Could not open keystore + Det gick inte att öppna nyckellagret + + + + Could not retrieve private key from keystore + Det gick inte att hämta den privata nyckeln från nyckellagret + + + + Could not create decryption cipher + Kunde inte skapa avkrypteringsskiffer + + + + Password not found + Lösenordet hittades inte + + + + D-Bus is not running + D-Bus är inte igång + + + + + Unknown error + Okänt fel + + + + No keychain service available + Ingen nyckelringstjänst tillgänglig + + + + Could not open wallet: %1; %2 + Det gick inte att öppna plånboken: %1; %2 + + + + Access to keychain denied + Åtkomst till nyckelring nekades + + + + Could not determine data type: %1; %2 + Datatypen kunde inte fastställas: %1; %2 + + + + Unsupported entry type 'Map' + Posttypen 'Map' stöds inte + + + + Unknown kwallet entry type '%1' + Okänd typ av kwallet-post '%1' + + + + Password entry not found + Lösenordsposten hittades inte + + + + Could not decrypt data + Kunde inte avkryptera data + + + + + Could not decrypt data: %1 + Det gick inte att avkryptera data: %1 + + + + QKeychain::WritePasswordJobPrivate + + + Could not open keystore + Det gick inte att öppna nyckellagret + + + + Could not create private key generator + Det gick inte att skapa en generator för privata nycklar + + + + Could not generate new private key + Det gick inte att generera en ny privat nyckel + + + + Could not retrieve private key from keystore + Det gick inte att hämta den privata nyckeln från nyckellagret + + + + Could not create encryption cipher + Det gick inte att skapa krypteringschiffer + + + + Could not encrypt data + Kunde inte kryptera data + + + + Password not found + Lösenordet hittades inte + + + + D-Bus is not running + D-Bus är inte igång + + + + + Unknown error + Okänt fel + + + + Could not open wallet: %1; %2 + Det gick inte att öppna plånboken: %1; %2 + + + + + Encryption failed: %1 + Krypteringen misslyckades: %1 + + + + Credential size exceeds maximum size of %1: %2 + Autentiseringsstorleken överskrider maxstorleken för %1: %2 + + + + Credential key exceeds maximum size of %1 + Autentiseringsnyckeln överskrider den maximala storleken på %1 + + + + Writing credentials failed: %1 + Skrivning av autentiseringsuppgifter misslyckades: %1 + + + + QObject + + + error 0x%1: %2 + fel 0x%1: %2 + + + + Access to keychain denied + Åtkomst till nyckelring nekades + + + + No keyring daemon + Ingen keyring-daemon + + + + Already unlocked + Redan upplåst + + + + No such keyring + Ingen sådan nyckelring + + + + Bad arguments + Felaktiga argument + + + + I/O error + In/ut-fel + + + + Cancelled + Avbruten + + + + Keyring already exists + Nyckelringen finns redan + + + + No match + Ingen matchning + + + + Unknown error + Okänt fel + + + + Entry not found + Posten hittades inte + + + diff --git a/translations/translations.qrc.in b/translations/translations.qrc.in new file mode 100644 index 00000000..f49df661 --- /dev/null +++ b/translations/translations.qrc.in @@ -0,0 +1,5 @@ + + + @QM_FILE_LIST@ + +