65.9K
CodeProject 正在变化。 阅读更多。
Home

securelocker: 一个 httplite“非常安全”的文件存储锁

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2022年1月7日

MPL

4分钟阅读

viewsIcon

11789

downloadIcon

86

一个将 httplite 与安全组件结合使用的令人兴奋的概念验证

引言

本项目展示了如何将httplite REST API与现成的安全组件结合,构建一个“相当安全”的文件存储应用程序。该应用程序在其所在的计算机和网络世界中可能没有什么实际用途,但编写和运行它很有趣,并且产生了一些有趣的代码,所以请享用。

我们所站立的肩膀

securelocker - 相当安全的存储锁

securelocker的理念是,我们希望模仿现实世界中租赁储物柜供人们存储和交换物品的商业模式来构建一个软件系统。你支付费用,获得储物柜号码和钥匙。你把东西放进去,锁好,然后也许把号码和钥匙给别人作为交换,或者以后再回来取回你的东西。

重要提示:储物柜公司会保留你得到的钥匙的副本。所以它是相当安全的:它和你信任储物柜公司一样安全。

为了在C++中模拟这一点,我决定编写两个应用程序。系统操作员运行securelockerd来经营储物柜业务,终端用户操作securelocker来处理他们的储物柜。很多事情都发生在计算机命令和网络操作的“带外”(off-the-band)进行。

securelockerd

securelockerd是用于运行储物柜业务的服务器端命令行应用程序。该应用程序接受注册、签入和签出客户端的命令。它还运行一个httplite Web服务器来处理运行securelocker(下面描述的客户端程序)的客户端。

register命令是在你去locker业务之前打的电话,预订一个locker。此命令接受预订人的姓名,并将其存储在下面描述的leger中。客户端会通过应用程序外部的“带外”方式传达其姓名。我不确定这一步的价值,可能是不必要的。请对此发表您的看法。

checkin命令会找到一个空闲的locker,生成locker的钥匙,并将locker的房间号和钥匙存储在leger中,然后将号码和钥匙交给操作员,由操作员“带外”提供给客户。你可能会猜到,这个系统采用了共享密钥的对称加密,即Blowfish。

checkout命令会清空locker并从leger中删除记录。

有一个leger对象,用于管理储物柜信息。这个leger在命令处理代码和Web服务器之间共享。leger是线程安全的。leger数据存储在磁盘上的加密文件中。当操作员启动securelockerd时,它会提示用户输入leger文件的密码,并将leger加载到内存中。对leger的所有更改都会将leger文件写回。这显然不是为成千上万的locker设计的,但对于几百个locker来说应该足够了,与真实的locker业务差不多。

class locker leger
{
public: // interface
	lockerleger(const std::wstring& password, const std::wstring& legerFilePath);
	void load();

	void registerClient(const std::wstring& name);

	void checkin(const std::wstring& name, uint32_t& room, std::string& key);

	void checkout(const std::wstring& name, uint32_t& room);

	struct legerentry
	{
		legerentry();
		legerentry(const std::wstring& _name, uint32_t _room, const std::string& _key);
		legerentry(const std::wstring& str);

		std::wstring toString() const;

		std::wstring name;
		uint32_t room;
		std::string key;
	};

private: // implementation
	void save();

	bool isNameRegistered(const std::wstring& name) const;
	
	std::shared_ptr<legerentry> getNameEntry(const std::wstring& name) const;
	
	uint32_t getAvailableRoom() const;

private: // state
	std::vector<std::shared_ptr<legerentry>> m_entries;

	mutable std::mutex m_mutex;

	const std::string m_password;
	const std::wstring m_legerFilePath;
};

寻找可用房间的算法很有趣,是一个不错的面试问题。

uint32_t lockerleger::getAvailableRoom() const
{
	// Get the sorted list of all rooms
	std::vector<uint32_t> rooms;
	for (const auto& legerentry : m_entries)
	{
		if (legerentry->room > 0)
			rooms.push_back(legerentry->room);
	}
	std::sort(rooms.begin(), rooms.end());

	// Walk the list looking for a gap in the sequence
	uint32_t last = 0;
	for (uint32_t cur : rooms)
	{
		uint32_t should = last + 1;
		if (cur > should) // past empty spot
			return should;
		last = cur;
	}

	// Failing that, go one after the end
	return last + 1;
}

Locker的内容存储在一个目录中,每个locker一个子目录。每个locker包含一个扁平的文件列表,没有子目录。这使得实现更简单。如果你想要子目录,可以使用ZIP文件。如果你想要更好的安全性,可以使用密码保护的ZIP文件。即使是密码保护的ZIP文件,如果你要交换文件,你也需要以某种方式与对方共享密码。一切都取决于你信任谁。

securelockerd需要在程序启动时从用户那里获取leger密码。由于leger的内容高度敏感,密码输入时不应在屏幕上显示用户的密码。为此,你只需要#include <conio.h>并使用一段如下的代码。

printf("Leger Access Password: ");
std::wstring password;
while (true)
{
	char c = _getch();
	if (c == '\r' || c == '\n')
		break;
	password += c;
	printf("*");
}
printf("\n");

在接收用户命令之前,程序会加载leger并启动Web服务器。

std::wstring lockerRootDir = argv[2];
std::wstring legerFilePath = fs::path(lockerRootDir).append("leger.dat");
lockerleger leger(password, legerFilePath);
leger.load();

lockerserver server
(
	static_cast<uint16_t>(port), 
	leger, 
	lockerRootDir
);
server.start();

防止恶意文件访问的代码非常巧妙。

bool securelib::IsFilenameValid(const std::wstring& filename)
{
	if (filename.empty())
		return false;

	if
	(
		filename == L"."
		||
		filename.front() == ' '
		||
		filename.back() == ' '
		||
		filename.back() == '.'
		||
		filename.find(L"..") != std::wstring::npos
	)
	{
		return false;
	}

	for (auto c : filename)
	{
		if ((c >= 0x0 && c <= 0x1F) || c == 0x7F)
			return false;

		switch (static_cast<char>(c))
		{
		case '\"':
		case '\\':
		case ':':
		case '/':
		case '<':
		case '>':
		case '|':
		case '?':
		case '*':
			return false;
		}
	}
	return true;
}

securelocker

securelocker是客户端命令行程序,你可以用它来

  1. 将文件放入储物柜。
  2. dir
  3. get
  4. del

文件。

我实现了一个基本的httplite包装类来实现这些功能,这样应用程序就可以只做命令行相关的事情了。

void lockerclient::put(const std::wstring& filename, const std::vector<uint8_t>& bytes)
{
	Request request;
	request.Verb = "PUT";
	request.Path.push_back(filename);
	request.Payload.emplace(Encrypt(bytes, m_key));
	doHttp(request);
}
	
std::vector<std::wstring> lockerclient::dir()
{
	Request request;
	request.Verb = "DIR"; // nonstandard, works well for us
	request.Path.push_back(L"/");
	Response response = doHttp(request);
	std::wstring dirResult =
		response.Payload.has_value() ? response.Payload->ToString() : L"";
	return Split(dirResult, L'\n');
}

std::vector<uint8_t> lockerclient::get(const std::wstring& filename)
{
	Request request;
	request.Verb = "GET";
	request.Path.push_back(filename);
	Response response = doHttp(request);
	return Decrypt(response.Payload->Bytes, m_key);
}

void lockerclient::del(const std::wstring& filename)
{
	Request request;
	request.Verb = "DELETE";
	request.Path.push_back(filename);
	doHttp(request);
}

Response lockerclient::doHttp(Request& request)
{
	Response response =
		issueClientHttpCommand
		(
			m_client,
			m_room,
			m_key,
			request
		);
	if (response.GetStatusCode() / 100 != 2)
		throw std::runtime_error(("HTTP operation failed: " + response.Status).c_str());
	return response;
}

……但是issueClientHttpCommand到底是什么?它实现了客户端的挑战-响应身份验证机制。

static Response issueClientHttpCommand
(
	httplite::HttpClient& client,
	uint32_t room,
	const std::string& key,
	httplite::Request& request
)
{
	trace(L"Client HTTP Command: " + std::to_wstring(room) + L" - " + 
          toWideStr(request.Verb) + L" - " + request.Path[0]);
	request.Headers["X-Room-Number"] = std::to_string(room); // we send every time, 
                                       // but the server only believes us once

	bool gotChallenge = false;
	bool submittedChallenge = false;

	std::string challengePhrase;
	std::string challengeNonce;

	while (true) // process authentication challenges then the actual request
	{
		if (gotChallenge)
		{
			auto encryptedResponse =
				Encrypt
				(
					StrToVec
					(
						std::to_string(room) +
						challengePhrase +
						challengeNonce
					),
					key
				);
			std::string challengeResponse =
				Hash(encryptedResponse.data(), encryptedResponse.size());
			request.Headers["X-Challenge-Response"] = challengeResponse;
			submittedChallenge = true;
			trace("Got challenge, response: " + challengeResponse);
		}

		trace("Issuing request...");
		Response response = client.ProcessRequest(request);
		trace("Response: " + response.Status);
		uint16_t statusCode = response.GetStatusCode();
		if (statusCode / 100 == 2)
		{
			// Authentication is fine, so remove the auth headers 
			// to keep subsequent requests clean
			request.Headers.erase("X-Room-Number");
			request.Headers.erase("X-Challenge-Response");
			return response;
		}
		else if (statusCode / 100 == 4)
		{
			// no double-dipping, you get one shot
			// client expects a successful response, 
			// so we throw instead for return response
			if (submittedChallenge)
				throw std::runtime_error("Access denied.");

			gotChallenge = true;
			challengePhrase = response.Headers["X-Challenge-Phrase"];
			challengeNonce = response.Headers["X-Challenge-Nonce"];
		}
		else
			throw std::runtime_error(("Unregonized Server Response: " + 
                                       response.Status).c_str());
	}
}

身份验证的服务器端有点镜像。我向httplite添加了ConnectionVariables,以便可以从每个套接字线程的本地变量传递到Request对象。它有点像会话变量,没有Cookie,只是在服务器的每个套接字线程中持久化的状态。不可扩展,但非常简单。

static std::shared_ptr<Response> authServerHttpRequest 
(
	const Request& request,
	std::function<int()> nonceGen,
	std::function<std::string(uint32_t room)> keyGet
)
{
	// Make connection variables easier to work with
	auto& connVars = *request.ConnectionVariables;

	// Bail if the client is already authenticated
	if (connVars.find(L"Authenticated") != connVars.end())
	{
		trace("Auth: Client authenticated");
		return nullptr;
	}

	// Unpack the challenge connection vars
	auto roomIt = connVars.find(L"RoomNumber");
	auto challengeIt = connVars.find(L"ChallengePhrase");
	auto nonceIt = connVars.find(L"ChallengeNonce");
	if
	(
		roomIt == connVars.end()
		||
		challengeIt == connVars.end()
		||
		nonceIt == connVars.end()
	)
	{
		trace("Auth: Client not challenged yet");

		// Get the room number from the request and validate it
		// NOTE: This is the only time when the room number is read from the client
		//		 We can't allow clients to change which room they're talking about 
		//		 later and gain access to another room's contents, password or not
		auto roomRequestIt = request.Headers.find("X-Room-Number");
		if (roomRequestIt == request.Headers.end())
		{
			trace("Auth: No room number");
			std::shared_ptr<Response> response = std::make_shared<Response>();
			response->Status = "403 Invalid Request";
			return response;
		}
		int roomInt = atoi(roomRequestIt->second.c_str());
		if (roomInt <= 0)
		{
			trace("Auth: Invalid room number");
			std::shared_ptr<Response> response = std::make_shared<Response>();
			response->Status = "403 Invalid Request";
			return response;
		}

		// Create the challenge
		std::string challenge = UniqueStr();
		std::string nonce = std::to_string(nonceGen());
		trace("Auth: Challenge: " + challenge + " - " + nonce);

		// Stash the challenge in connections vars
		connVars[L"RoomNumber"] = std::to_wstring(roomInt);
		connVars[L"ChallengePhrase"] = toWideStr(challenge);
		connVars[L"ChallengeNonce"] = toWideStr(nonce);

		// Return the challenge response
		std::shared_ptr<Response> response = std::make_shared<Response>();
		response->Status = "401 Access Denied";
		response->Headers["X-Challenge-Phrase"] = challenge;
		response->Headers["X-Challenge-Nonce"] = nonce;
		return response;
	}
	else // we have a complete set of challenge connections vars
	{
		trace("Auth: Client challenged");

		// Unpack the server challenge from the connection vars
		uint32_t room = static_cast<uint32_t>(_wtoi(roomIt->second.c_str()));
		std::string challengePhrase = toNarrowStr(challengeIt->second);
		std::string challengeNonce = toNarrowStr(nonceIt->second);

		// Erase the challenge connection vars to prevent clients from hammering
		// on the same challenge until they find something that works
		connVars.erase(roomIt);
		connVars.erase(challengeIt);
		connVars.erase(nonceIt);

		// Get the client's response to the server challenge
		std::string challengeClientResponse;
		{
			auto requestChallengeResponseIt = request.Headers.find("X-Challenge-Response");
			if (requestChallengeResponseIt == request.Headers.end())
			{
				trace("Auth: Client did not respond to challenge");
				std::shared_ptr<Response> response = std::make_shared<Response>();
				response->Status = "401 Access Denied";
				return response;
			}
			challengeClientResponse = requestChallengeResponseIt->second;
		}

		// Compute the server version of the challenge response
		auto encryptedLocalResponse =
			Encrypt
			(
				StrToVec
				(
					std::to_string(room) +
					challengePhrase +
					challengeNonce
				),
				keyGet(room)
			);
		std::string challengeLocalResponse =
			Hash(encryptedLocalResponse.data(), encryptedLocalResponse.size());
		if (challengeClientResponse == challengeLocalResponse) // client is granted access
		{
			trace("Auth: Client challenge response matches, client authenticated");
			connVars[L"Authenticated"] = L"true";
			connVars[L"RoomNumber"] = std::to_wstring(room);
			return nullptr;
		}
		else
		{
			trace("Auth: Client challenge response does not match");
			std::shared_ptr<Response> response = std::make_shared<Response>();
			response->Status = "401 Access Denied";
			return response;
		}
	}

	// Unreachable
	//return nullptr;
}

securelib - 关于我们所站立的肩膀

securelib全局函数API为应用程序依赖的安全基本操作提供了一个清晰的接口。

std::string securelib::Hash(const uint8_t* data, size_t len)
{
	// https://github.com/System-Glitch/SHA256
	SHA256 hasher;
	hasher.update(data, len);

	uint8_t* digestBytes = hasher.digest();
	std::string digest = hasher.toString(digestBytes);
	delete[] digestBytes; // don't forget to delete the digest!
	
	return digest;
}

std::string securelib::Hash(const std::string& str)
{
	return Hash(reinterpret_cast<const uint8_t*>(str.c_str()), str.size());
}

//
// The trick to Blowfish encryption is that 
// you have to pad the plaintext to an 8-byte boundary,
// hence you have to tuck the original length of the plaintext into ciphertext output
// and retrieve it when decrypting.
//

inline void PadVectorTo8ths(std::vector<uint8_t>& vec)
{
	while (vec.size() % 8)
		vec.push_back(0);
}

union lenbytes
{
	uint32_t len;
	uint8_t bytes[4];
};

inline void AddLengthToVector(std::vector<uint8_t>& vec, uint32_t len)
{
	lenbytes holder;
	holder.len = htonl(len);
	vec.push_back(holder.bytes[0]);
	vec.push_back(holder.bytes[1]);
	vec.push_back(holder.bytes[2]);
	vec.push_back(holder.bytes[3]);
}

inline uint32_t RemoveLengthFromVector(std::vector<uint8_t>& vec)
{
	lenbytes holder;
	memcpy(holder.bytes, vec.data() + vec.size() - 4, 4);
	vec.resize(vec.size() - 4);
	return ntohl(holder.len);
}

std::vector<uint8_t> securelib::Encrypt(std::vector<uint8_t> data, const std::string& key)
{
	uint32_t originalInputSize = static_cast<uint32_t>(data.size());
	if (data.size() > 0)
	{
		PadVectorTo8ths(data);

		auto keyVec = StrToVec(key);
		CBlowFish enc(keyVec.data(), keyVec.size());

		enc.Encrypt(data.data(), data.size());
	}
	AddLengthToVector(data, originalInputSize);
	return data;
}

std::vector<uint8_t> securelib::Decrypt(std::vector<uint8_t> data, const std::string& key)
{
	if (data.size() < sizeof(uint32_t))
		return std::vector<uint8_t>();

	uint32_t originalInputSize = RemoveLengthFromVector(data);
	if (originalInputSize == 0)
		return data;

	auto keyVec = StrToVec(key);
	CBlowFish dec(keyVec.data(), keyVec.size());

	dec.Decrypt(data.data(), data.size());

	data.resize(originalInputSize);
	return data;
}

std::string securelib::Encrypt(const std::string& str, const std::string& key)
{
	return VecToStr(Encrypt(StrToVec(str), key));
}

std::string securelib::Decrypt(const std::string& str, const std::string& key)
{
	return VecToStr(Decrypt(StrToVec(str), key));
}

std::vector<unsigned char> securelib::StrToVec(const std::string& str)
{
	std::vector<unsigned char> vec;
	vec.resize(str.size());
	memcpy(vec.data(), str.c_str(), vec.size());
	return vec;
}

std::string securelib::VecToStr(const std::vector<unsigned char>& data)
{
	std::string retVal;
	retVal.resize(data.size());
	memcpy(const_cast<char*>(retVal.c_str()), data.data(), data.size());
	return retVal;
}

// Generate a unique string for HTTP challenging and locker key generation
std::string securelib::UniqueStr() 
{
	static std::mutex mutex;
    std::lock_guard<std::mutex> lock(mutex); // protect rand()
	std::stringstream stream;
	// https://github.com/graeme-hill/crossguid
	stream << xg::newGuid() << rand() << time(nullptr);
	return Hash(stream.str());
}

结论

我希望您已经看到了使用httplite进行C++ REST API编程是多么容易,以及如何轻松使用现成的安全组件来构建像securelocker这样的相当安全的应用程序。

历史

  • 2022年1月6日:初始版本
© . All rights reserved.