Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement k anonymity (#45) #45

Merged
merged 11 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions backend/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,21 @@ int main(int argc, char *argv[])
const size_t offset = 1;
spdlog::info("Password offset: {}", offset);

// limit the number of leaked bytes to 4
if (offset > 4)
{
throw std::invalid_argument("Offset cannot be greater than 4.");
}

// build the database
if (build)
{
// create password table
db.execute("CREATE TABLE passwords (password TEXT);");
// create all tables from 0 to (2^8)^offset-1
const int num_tables = std::pow(std::pow(2, 8), offset);
for (int i = 0; i < num_tables; i++)
{
db.execute("CREATE TABLE `" + std::to_string(i) + "` (password TEXT);");
}

// generate and insert the passwords into the database
std::unordered_set<std::string> passwords = password::generatePasswords(100, 20);
Expand All @@ -67,31 +78,37 @@ int main(int argc, char *argv[])
// 3. insert into database
for (const auto &password : encrypted_passwords)
{
// determine which table to insert into based on leaked byte
unsigned int leaked_byte = ((unsigned char)password.substr(0, offset)[0]) & (num_tables-1);
std::string raw_password = password.substr(offset, password.size() - offset);
ni-jessica marked this conversation as resolved.
Show resolved Hide resolved
// encode password before inserting into database
db.execute("INSERT INTO passwords (password) VALUES ('" + crow::utility::base64encode(password, password.size()) + "');");
db.execute("INSERT INTO `" + std::to_string(leaked_byte) + "` (password) VALUES ('" + crow::utility::base64encode(raw_password, raw_password.size()) + "');");
}

// create key table
db.execute("CREATE TABLE secret (key TEXT);");

// encode key b and insert into database
db.execute("INSERT INTO secret (key) VALUES ('" + crow::utility::base64encode(std::string(reinterpret_cast<const char *>(b), crypto_core_ristretto255_SCALARBYTES), crypto_core_ristretto255_SCALARBYTES) + "');");

// create offset table and insert offset
db.execute("CREATE TABLE offset (key INTEGER);");
db.execute("INSERT INTO offset (key) VALUES (" + std::to_string(offset) + ");");
}
else
{
// error check if !build but passwords table does not exist in the file passed in
// error check if !build but secret table does not exist in the file passed in
std::function<bool(sqlite3_stmt *)> callback = [](sqlite3_stmt *stmt)
{
int count = atoi(reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0)));
return count;
};

// check if passwords and key table exists
std::vector<bool> password_result = db.execute("SELECT COUNT(*) FROM sqlite_schema WHERE name = 'passwords';", callback);
std::vector<bool> secret_result = db.execute("SELECT COUNT(*) FROM sqlite_schema WHERE name = 'secret';", callback);
if (password_result.front() == 0 || secret_result.front() == 0) // no passwords or no key table exists
// check if key and offset table exists
std::vector<bool> secret_key = db.execute("SELECT COUNT(*) FROM sqlite_schema WHERE name = 'secret';", callback);
std::vector<bool> offset_key = db.execute("SELECT COUNT(*) FROM sqlite_schema WHERE name = 'offset';", callback);
if (secret_key.front() == 0 || offset_key.front() == 0) // no secret key or no offset key table exists
{
throw std::invalid_argument("Passwords and/or secret key table does not exist. Use --build to create a new database");
throw std::invalid_argument("Secret key table or offset key table does not exist. Use --build to create a new database");
}
}
// Enable CORS
Expand Down
11 changes: 9 additions & 2 deletions backend/src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ namespace server
std::function <std::string(sqlite3_stmt *)> callback = [](sqlite3_stmt *stmt) {
return std::string(reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0)));
};
std::vector<std::string> breached_passwords = db.execute("SELECT * FROM passwords;", callback);

// get the table num corresponding to the user password leaked byte
std::string decoded_password = crow::utility::base64decode(user_password, user_password.size());
ni-jessica marked this conversation as resolved.
Show resolved Hide resolved
unsigned char leaked_byte = decoded_password.substr(0, offset)[0];
unsigned int table_num = leaked_byte & ((int)std::pow(std::pow(2, 8), offset) - 1);

// get all passwords from the table corresponding to the user password leaked byte
std::vector<std::string> breached_passwords = db.execute("SELECT * FROM `" + std::to_string(table_num) + "`;", callback);

// get b secret key from database
std::string encoded_b = db.execute("SELECT * FROM secret;", callback)[0];
Expand All @@ -42,7 +49,7 @@ namespace server
unsigned char *b = (unsigned char *)decoded_b.data();

// encrypt user password
std::string encrypted_password = cryptography::encryptPassword(crow::utility::base64decode(user_password, user_password.size()), b, offset);
std::string encrypted_password = cryptography::encryptPassword(decoded_password, b, offset);
response["status"] = "success";
response["userPassword"] = crow::utility::base64encode(encrypted_password, encrypted_password.size());
response["breachedPasswords"] = breached_passwords;
Expand Down
62 changes: 33 additions & 29 deletions backend/tests/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ TEST_CASE("Test endpoints using handler")

// create the database, with tables passwords and secret
database::Database db = database::Database(path, true);
REQUIRE_NOTHROW(db.execute("CREATE TABLE passwords (password TEXT);"));
REQUIRE_NOTHROW(db.execute("CREATE TABLE secret (key TEXT);"));

// offset constant
const size_t offset = 1;
const int num_tables = std::pow(std::pow(2, 8), offset);

// create all tables from 0 to (2^8)^offset-1
for (int i = 0; i < num_tables; i++)
{
db.execute("CREATE TABLE `" + std::to_string(i) + "` (password TEXT);");
}

// create a mock password set
std::unordered_set<std::string> passwords = {"TestPass1&", "ChocolateCake1!", "LoveMyDogs3$"};
Expand All @@ -38,19 +46,26 @@ TEST_CASE("Test endpoints using handler")
crypto_core_ristretto255_scalar_random(b);

// 2. encrypt each password with b (and hash to point)
const size_t offset = 1;
std::vector<std::string> encrypted_passwords = cryptography::encrypt(passwords, b, offset);

// 3. insert into database
for (const auto &password : encrypted_passwords)
{
// determine which table to insert into based on leaked byte
unsigned int leaked_byte = ((unsigned char)password.substr(0, offset)[0]) & (num_tables-1);
std::string raw_password = password.substr(offset, password.size() - offset);

// encode password before inserting into database
db.execute("INSERT INTO passwords (password) VALUES ('" + crow::utility::base64encode(password, password.size()) + "');");
db.execute("INSERT INTO `" + std::to_string(leaked_byte) + "` (password) VALUES ('" + crow::utility::base64encode(raw_password, raw_password.size()) + "');");
}

// create key table
db.execute("CREATE TABLE secret (key TEXT);");

// encode key b and insert into database
db.execute("INSERT INTO secret (key) VALUES ('" + crow::utility::base64encode(std::string(reinterpret_cast<const char *>(b), crypto_core_ristretto255_SCALARBYTES), crypto_core_ristretto255_SCALARBYTES) + "');");

// initialize endpoints
server::breachedPasswords(app, db, offset);

// check that all the route handlers were created
Expand All @@ -73,38 +88,27 @@ TEST_CASE("Test endpoints using handler")
req.method = "POST"_method;
req.add_header("Access-Control-Allow-Headers", "*");
req.add_header("Content-Type", "application/json");
req.body = "TestPass1&";

// encrypt user password with b (and hash to point)
std::string user_request = "TestPass1&";
std::string encrypted_password = cryptography::hashAndEncryptPassword(user_request, b, offset);
req.body = crow::utility::base64encode(encrypted_password, encrypted_password.size());

app.handle(req, res);

auto body = nlohmann::json::parse(res.body);
CHECK(body["status"] == "success");

// check correct bucket is returned
REQUIRE(body["breachedPasswords"].is_array());
std::vector<std::string> breached_passwords = body["breachedPasswords"].get<std::vector<std::string>>();
CHECK(breached_passwords.size() == 3);
for (const auto &breached_password : breached_passwords)
{
// offset determines the amount of padding appended to the end of the password
switch (offset % 3)
{
case 0:
// password is padded with single '='
CHECK(breached_password[breached_password.size() - 1] == '=');
CHECK(breached_password[breached_password.size() - 2] != '=');
break;
case 1:
// password is not padded
CHECK(breached_password[breached_password.size() - 1] != '=');
CHECK(breached_password[breached_password.size() - 2] != '=');
break;
case 2:
// password is padded with two '='
CHECK(breached_password[breached_password.size() - 1] == '=');
CHECK(breached_password[breached_password.size() - 2] == '=');
break;
}
}
std::vector<std::string> breached_bucket = body["breachedPasswords"].get<std::vector<std::string>>();
CHECK(breached_bucket.size() == 1);

// check breached password encoding
std::string breached_password = breached_bucket[0];
CHECK(breached_password[breached_password.size() - 1] == '=');
csirianni marked this conversation as resolved.
Show resolved Hide resolved

// check user password encoded
std::string user_password = body["userPassword"];
CHECK(!user_password.empty());
CHECK(user_password.back() == '=');
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/psi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function computeIntersection(
offset = 0
): boolean {
const userPassword = base64.parse(data.userPassword);
const breachedPasswords = new Set((data.breachedPasswords).map(function (element) { return base64.parse(element).subarray(offset).join(""); }));
const breachedPasswords = new Set((data.breachedPasswords).map(function (element) { return base64.parse(element).join(""); }));

// Client phase 2 - applies inverse seed A to (user password)^ab
// so now ((user password)^ab)^-a = (user password)^b
Expand Down
Loading