aboutsummaryrefslogtreecommitdiffstats
path: root/src/m_email/EMailCredentialStore.cpp
blob: 6aefd515fc05a8b291b9defb2a5c3f88e128bc1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
 * Copyright (C) 2021-2024 Saturneric <eric@bktus.com>
 *
 * This file is part of GpgFrontend.
 *
 * GpgFrontend is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GpgFrontend is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
 *
 * The initial version of the source code is inherited from
 * the gpg4usb project, which is under GPL-3.0-or-later.
 *
 * All the source code of GpgFrontend was modified and released by
 * Saturneric <eric@bktus.com> starting on May 12, 2021.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#include "EMailCredentialStore.h"

#include "GFModule.h"

namespace {

/// Credentials are keyed by account id, not by host or username: re-hosting an
/// account or correcting its username must not orphan the stored secret.
auto CredentialKey(const QString& account_id) -> QString {
  return QString("mail/cred/%1").arg(account_id);
}

}  // namespace

namespace EMailCredentialStore {

auto CurrentProtection() -> ProtectionLevel {
  switch (GFAppKeyProtectionLevel(GFModuleSdkContext())) {
    case 0:
      return ProtectionLevel::kNONE;
    case 1:
      return ProtectionLevel::kKEYCHAIN;
    case 2:
      return ProtectionLevel::kPIN;
    default:
      return ProtectionLevel::kUNKNOWN;
  }
}

auto MayPersistSilently() -> bool {
  const auto protection = CurrentProtection();

  // kUNKNOWN is treated as kNONE rather than given the benefit of the doubt:
  // if we cannot establish that the key is protected, we have to assume it is
  // not. Guessing the other way stores a password on a promise nobody made.
  return protection == ProtectionLevel::kKEYCHAIN ||
         protection == ProtectionLevel::kPIN;
}

auto Save(const QString& account_id, const EMailSecret& password) -> bool {
  if (account_id.isEmpty()) return false;

  // Storing is not optional: there is no password prompt anywhere, so an
  // account without a stored password simply cannot be used. Refusing here
  // would not protect anyone, it would just break the feature.
  //
  // The protection level still matters though, so it is recorded rather than
  // shown. Everything here is encrypted under the application secure key, and
  // when that key is itself unprotected this is protection in name only --
  // worth having in a log when someone is working out how exposed a profile
  // is, even though it is not worth interrupting them over.
  if (!MayPersistSilently()) {
    LOG_WARN(
        "storing a mail credential while the application key is unprotected; "
        "it is only as protected as the profile itself");
  }

  //
  // Both arguments are handed over owned, from the allocators the SDK will
  // release them through: the key ordinary, the secret secure. Passing a
  // QByteArray's internal pointer here would have the SDK free memory Qt owns.
  // Deliberately the raw buffer API rather than the QString convenience: a
  // QString cannot be erased, and routing a password through one would add a
  // copy this module could never wipe. The buffer is wiped on release.
  //
  // The secure tier BORROWS now, like every other argument in this SDK. It
  // used to take ownership of both arguments and free them through two
  // different allocators, a rule nothing in the signature hinted at.
  auto* ctx = GFModuleSdkContext();
  auto* buffer = GFBufferNewFromBytes(ctx, password.Data(), password.Size());
  if (buffer == nullptr) {
    LOG_ERROR("could not allocate a secure buffer for a mail credential");
    return false;
  }

  const auto result = GFStorageCacheSet(
      ctx, GF_STORE_SECURE_DURABLE,
      CredentialKey(account_id).toUtf8().constData(), buffer, 0);
  GFBufferRelease(ctx, buffer);

  if (result != 0) LOG_ERROR("failed to store mail credential");
  return result == 0;
}

auto Load(const QString& account_id) -> EMailSecretPtr {
  if (account_id.isEmpty()) return std::make_shared<EMailSecret>();

  // Raw buffer again, for the same reason: the secret must not pass through
  // a QString on its way into storage that can erase it.
  auto* ctx = GFModuleSdkContext();
  GFBufferRef raw = nullptr;
  if (GFStorageCacheGet(ctx, GF_STORE_SECURE_DURABLE,
                        CredentialKey(account_id).toUtf8().constData(),
                        &raw) != 0) {
    return std::make_shared<EMailSecret>();
  }

  // AdoptBuffer copies the bytes out and releases the buffer, which wipes it.
  return EMailSecret::AdoptBuffer(ctx, raw);
}

auto Has(const QString& account_id) -> bool {
  return !Load(account_id)->IsEmpty();
}

void Remove(const QString& account_id) {
  if (account_id.isEmpty()) return;
  gf::sdk::RemoveCache(GFModuleSdkContext(), GF_STORE_SECURE_DURABLE,
                       (CredentialKey(account_id)).toUtf8().constData());
}

}  // namespace EMailCredentialStore