1 | /* |
2 | SPDX-FileCopyrightText: 2017 KDE Developers |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | |
7 | #include "katesecuretextbuffer_p.h" |
8 | |
9 | #include "config.h" |
10 | |
11 | #include <KAuth/HelperSupport> |
12 | |
13 | #ifndef Q_OS_WIN |
14 | #include <cerrno> |
15 | #include <unistd.h> |
16 | #endif |
17 | |
18 | #include <QDir> |
19 | #include <QFile> |
20 | #include <QFileInfo> |
21 | #include <QString> |
22 | #include <QTemporaryFile> |
23 | |
24 | KAUTH_HELPER_MAIN("org.kde.ktexteditor6.katetextbuffer" , SecureTextBuffer) |
25 | |
26 | ActionReply SecureTextBuffer::savefile(const QVariantMap &args) |
27 | { |
28 | const QString sourceFile = args[QStringLiteral("sourceFile" )].toString(); |
29 | const QString targetFile = args[QStringLiteral("targetFile" )].toString(); |
30 | const QByteArray checksum = args[QStringLiteral("checksum" )].toByteArray(); |
31 | const uint ownerId = (uint)args[QStringLiteral("ownerId" )].toInt(); |
32 | const uint groupId = (uint)args[QStringLiteral("groupId" )].toInt(); |
33 | |
34 | if (saveFileInternal(sourceFile, targetFile, checksum, ownerId, groupId)) { |
35 | return ActionReply::SuccessReply(); |
36 | } |
37 | |
38 | return ActionReply::HelperErrorReply(); |
39 | } |
40 | |
41 | bool SecureTextBuffer::saveFileInternal(const QString &sourceFile, |
42 | const QString &targetFile, |
43 | const QByteArray &checksum, |
44 | const uint ownerId, |
45 | const uint groupId) |
46 | { |
47 | // open source file for reading |
48 | // if not possible, signal error |
49 | QFile readFile(sourceFile); |
50 | if (!readFile.open(flags: QIODevice::ReadOnly)) { |
51 | return false; |
52 | } |
53 | |
54 | // construct file info for target file |
55 | // we need to know things like path/exists/permissions |
56 | const QFileInfo targetFileInfo(targetFile); |
57 | |
58 | // create temporary file in current directory to be able to later do an atomic rename |
59 | // we need to pass full path, else QTemporaryFile uses the temporary directory |
60 | // if not possible, signal error, this catches e.g. a non-existing target directory, too |
61 | QTemporaryFile tempFile(targetFileInfo.absolutePath() + QLatin1String("/secureXXXXXX" )); |
62 | if (!tempFile.open()) { |
63 | return false; |
64 | } |
65 | |
66 | // copy contents + do checksumming |
67 | // if not possible, signal error |
68 | QCryptographicHash cryptographicHash(checksumAlgorithm); |
69 | const qint64 bufferLength = 4096; |
70 | char buffer[bufferLength]; |
71 | qint64 read = -1; |
72 | while ((read = readFile.read(data: buffer, maxlen: bufferLength)) > 0) { |
73 | cryptographicHash.addData(data: QByteArrayView(buffer, read)); |
74 | if (tempFile.write(data: buffer, len: read) == -1) { |
75 | return false; |
76 | } |
77 | } |
78 | |
79 | // check that copying was successful and checksum matched |
80 | // we need to flush the file, as QTemporaryFile keeps the handle open |
81 | // and we later do things like renaming of the file! |
82 | // if not possible, signal error |
83 | if ((read == -1) || (cryptographicHash.result() != checksum) || !tempFile.flush()) { |
84 | return false; |
85 | } |
86 | |
87 | // try to preserve the permissions |
88 | if (!targetFileInfo.exists()) { |
89 | // ensure new file is readable by anyone |
90 | tempFile.setPermissions(tempFile.permissions() | QFile::Permission::ReadGroup | QFile::Permission::ReadOther); |
91 | } else { |
92 | // ensure the same file permissions |
93 | tempFile.setPermissions(targetFileInfo.permissions()); |
94 | |
95 | // ensure file has the same owner and group as before |
96 | setOwner(filedes: tempFile.handle(), ownerId, groupId); |
97 | } |
98 | |
99 | // try to (atomic) rename temporary file to the target file |
100 | if (moveFile(sourceFile: tempFile.fileName(), targetFile: targetFileInfo.filePath())) { |
101 | // temporary file was renamed, there is nothing to remove anymore |
102 | tempFile.setAutoRemove(false); |
103 | return true; |
104 | } |
105 | |
106 | // we failed |
107 | // QTemporaryFile will handle cleanup |
108 | return false; |
109 | } |
110 | |
111 | void SecureTextBuffer::setOwner(const int filedes, const uint ownerId, const uint groupId) |
112 | { |
113 | #ifndef Q_OS_WIN |
114 | if (ownerId != (uint)-2 && groupId != (uint)-2) { |
115 | int result = fchown(fd: filedes, owner: ownerId, group: groupId); |
116 | // set at least correct group if owner cannot be changed |
117 | if (result != 0 && errno == EPERM) { |
118 | result = fchown(fd: filedes, owner: getuid(), group: groupId); |
119 | (void)result; // ignore if we can't do a thing |
120 | } |
121 | } |
122 | #else |
123 | // no-op for windows |
124 | #endif |
125 | } |
126 | |
127 | bool SecureTextBuffer::moveFile(const QString &sourceFile, const QString &targetFile) |
128 | { |
129 | #if !defined(Q_OS_WIN) && !defined(Q_OS_ANDROID) |
130 | const int result = std::rename(old: QFile::encodeName(fileName: sourceFile).constData(), new: QFile::encodeName(fileName: targetFile).constData()); |
131 | return (result == 0); |
132 | #else |
133 | // use racy fallback for windows |
134 | QFile::remove(targetFile); |
135 | return QFile::rename(sourceFile, targetFile); |
136 | #endif |
137 | } |
138 | |