master
cpp 255 lines 7.08 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 FileCredStorage.cpp
8
9 Abstract:
10
11 DPAPI-encrypted JSON file credential storage implementation.
12
13 --*/
14
15 #include "precomp.h"
16 #include "FileCredStorage.h"
17
18 using wsl::shared::Localization;
19
20 using namespace wsl::shared;
21 using namespace wsl::windows::common::wslutil;
22 using namespace wsl::windows::wslc::services;
23
24 namespace {
25
26 std::filesystem::path GetFilePath()
27 {
28 return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json";
29 }
30
31 wil::unique_file RetryOpenFileOnSharingViolation(const std::function<wil::unique_file()>& openFunc)
32 {
33 try
34 {
35 return wsl::shared::retry::RetryWithTimeout<wil::unique_file>(openFunc, std::chrono::milliseconds(100), std::chrono::seconds(1), []() {
36 return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION);
37 });
38 }
39 catch (...)
40 {
41 auto result = wil::ResultFromCaughtException();
42 auto errorString = wsl::windows::common::wslutil::GetSystemErrorString(result);
43 THROW_HR_WITH_USER_ERROR(result, Localization::MessageWslcFailedToOpenFile(GetFilePath(), errorString));
44 }
45 }
46
47 wil::unique_file OpenFileExclusive()
48 {
49 wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"r+b", _SH_DENYRW));
50 if (!f)
51 {
52 auto dosError = _doserrno;
53 if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND)
54 {
55 return nullptr;
56 }
57
58 THROW_WIN32_IF(dosError, dosError != 0);
59 THROW_HR(E_FAIL);
60 }
61
62 return f;
63 }
64
65 wil::unique_file CreateFileExclusive()
66 {
67 auto filePath = GetFilePath();
68 std::filesystem::create_directories(filePath.parent_path());
69
70 using UniqueFd = wil::unique_any<int, decltype(_close), _close, wil::details::pointer_access_all, int, int, -1>;
71
72 UniqueFd fd;
73 auto err = _wsopen_s(fd.addressof(), filePath.c_str(), _O_RDWR | _O_CREAT | _O_BINARY, _SH_DENYRW, _S_IREAD | _S_IWRITE);
74 if (err != 0)
75 {
76 auto dosError = _doserrno;
77 THROW_WIN32_IF(dosError, dosError != 0);
78 THROW_HR(E_FAIL);
79 }
80
81 wil::unique_file f(_fdopen(fd.get(), "r+b"));
82 if (!f)
83 {
84 auto dosError = _doserrno;
85 THROW_WIN32_IF(dosError, dosError != 0);
86 THROW_HR(E_FAIL);
87 }
88
89 fd.release();
90 return f;
91 }
92
93 wil::unique_file OpenFileShared()
94 {
95 wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"rb", _SH_DENYWR));
96 if (!f)
97 {
98 auto dosError = _doserrno;
99 if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND)
100 {
101 return nullptr;
102 }
103
104 THROW_WIN32_IF(dosError, dosError != 0);
105 THROW_HR(E_FAIL);
106 }
107
108 return f;
109 }
110
111 CredentialFile ReadCredentialFile(FILE* f)
112 {
113 WI_ASSERT(f != nullptr);
114
115 auto seekResult = fseek(f, 0, SEEK_SET);
116 THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), seekResult != 0);
117
118 // Handle newly created empty files (from CreateFileExclusive).
119 if (_filelengthi64(_fileno(f)) <= 0)
120 {
121 return {};
122 }
123
124 try
125 {
126 return nlohmann::json::parse(f).get<CredentialFile>();
127 }
128 catch (const nlohmann::json::exception&)
129 {
130 THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON, Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath()));
131 }
132 }
133
134 void WriteCredentialFile(FILE* f, const CredentialFile& data)
135 {
136 auto error = fseek(f, 0, SEEK_SET);
137 THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), error != 0);
138
139 error = _chsize_s(_fileno(f), 0);
140 THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(error)), error != 0);
141
142 auto content = nlohmann::json(data).dump(2);
143 auto written = fwrite(content.data(), 1, content.size(), f);
144 THROW_HR_WITH_USER_ERROR_IF(
145 E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), written != content.size());
146 }
147
148 void ModifyFileStore(FILE* f, const std::function<bool(CredentialFile&)>& modifier)
149 {
150 auto data = ReadCredentialFile(f);
151
152 if (modifier(data))
153 {
154 WriteCredentialFile(f, data);
155 }
156 }
157
158 std::string Protect(const std::string& plaintext)
159 {
160 DATA_BLOB input{};
161 input.cbData = static_cast<DWORD>(plaintext.size());
162 input.pbData = reinterpret_cast<BYTE*>(const_cast<char*>(plaintext.data()));
163
164 DATA_BLOB output{};
165 THROW_IF_WIN32_BOOL_FALSE(CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output));
166 auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); });
167
168 return Base64Encode(std::string(reinterpret_cast<const char*>(output.pbData), output.cbData));
169 }
170
171 std::string Unprotect(const std::string& cipherBase64)
172 {
173 auto decoded = Base64Decode(cipherBase64);
174
175 DATA_BLOB input{};
176 input.cbData = static_cast<DWORD>(decoded.size());
177 input.pbData = reinterpret_cast<BYTE*>(decoded.data());
178
179 DATA_BLOB output{};
180 THROW_IF_WIN32_BOOL_FALSE(CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output));
181 auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); });
182
183 return std::string(reinterpret_cast<const char*>(output.pbData), output.cbData);
184 }
185
186 } // namespace
187
188 namespace wsl::windows::wslc::services {
189
190 void FileCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret)
191 {
192 auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive);
193
194 ModifyFileStore(file.get(), [&](CredentialFile& data) {
195 data.Credentials[serverAddress] = CredentialEntry{username, Protect(secret)};
196 return true;
197 });
198 }
199
200 std::pair<std::string, std::string> FileCredStorage::Get(const std::string& serverAddress)
201 {
202 auto file = RetryOpenFileOnSharingViolation(OpenFileShared);
203 if (!file)
204 {
205 return {};
206 }
207
208 auto data = ReadCredentialFile(file.get());
209 const auto entry = data.Credentials.find(serverAddress);
210
211 if (entry == data.Credentials.end())
212 {
213 return {};
214 }
215
216 return {entry->second.UserName, Unprotect(entry->second.Secret)};
217 }
218
219 void FileCredStorage::Erase(const std::string& serverAddress)
220 {
221 auto file = RetryOpenFileOnSharingViolation(OpenFileExclusive);
222 bool erased = false;
223
224 if (file)
225 {
226 ModifyFileStore(file.get(), [&](CredentialFile& data) {
227 erased = data.Credentials.erase(serverAddress) > 0;
228 return erased;
229 });
230 }
231
232 THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased);
233 }
234
235 std::vector<std::wstring> FileCredStorage::List()
236 {
237 auto file = RetryOpenFileOnSharingViolation(OpenFileShared);
238 if (!file)
239 {
240 return {};
241 }
242
243 auto data = ReadCredentialFile(file.get());
244
245 std::vector<std::wstring> result;
246
247 for (const auto& [key, value] : data.Credentials)
248 {
249 result.push_back(wsl::shared::string::MultiByteToWide(key));
250 }
251
252 return result;
253 }
254
255 } // namespace wsl::windows::wslc::services