Initial commit

This commit is contained in:
2024-09-24 13:27:33 -05:00
parent 03badb316d
commit 98f9582dad
10 changed files with 2124 additions and 8 deletions

2
.env.dist Normal file
View File

@@ -0,0 +1,2 @@
JWT_SECRET_KEY=a1b2c3d4e5f6g7h8i9j0
ENCRYPTION_KEY=a1b2c3d4e5f6g7h8i9j0

13
.gitignore vendored
View File

@@ -1,8 +1,5 @@
# ---> Composer
composer.phar
/vendor/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
.aider*
vendor/
database/
*.sqlite
.env

42
bin/manage.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Hpz937\Phpvault\Database;
use Hpz937\Phpvault\Handler\AuthHandler;
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
$database = new Database();
$authHandler = new AuthHandler($_ENV['JWT_SECRET_KEY'], $database);
$app = new Ahc\Cli\Application('phpvault', '0.0.1');
$app->command('create-tables', 'Create database tables', 'ct')
->action(function() use ($database) {
$database->createTables();
echo "Tables created successfully!\n";
});
$app->command('add-user', 'Add a new user', 'au')
->argument('<username>', 'Username')
->action(function($username) use ($authHandler) {
$interactor = new Ahc\Cli\IO\Interactor;
$passValidator = function ($pass) {
if (\strlen($pass) < 6) {
throw new \InvalidArgumentException('Password too short');
}
return $pass;
};
$password = $interactor->promptHidden('Password', $passValidator, 2);
if ($authHandler->addUser($username, $password)) {
echo "User added successfully!\n";
} else {
echo "Failed to add user\n";
}
});
$app->handle($argv);

30
composer.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "hpz937/phpvault",
"type": "project",
"autoload": {
"psr-4": {
"Hpz937\\Phpvault\\": "src/"
}
},
"authors": [
{
"name": "Hpz937",
"email": "hpz937@gmail.com"
}
],
"repositories": [{
"type": "composer",
"url": "https://git.hpz.pw/api/packages/hpz937/composer"
}
],
"require": {
"hpz937/encryption": "^1.0",
"vlucas/phpdotenv": "^5.6",
"slim/slim": "^4.14",
"php-di/php-di": "^7.0",
"firebase/php-jwt": "^6.10",
"slim/psr7": "^1.7",
"paragonie/sodium_compat": "^2.1",
"adhocore/cli": "^1.7"
}
}

1596
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

192
public/index.php Normal file
View File

@@ -0,0 +1,192 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Slim\Factory\AppFactory;
use DI\Container;
use Hpz937\Encryption\DataEncryptor;
use Hpz937\Phpvault\Handler\AuthHandler;
use Hpz937\Phpvault\Database;
use Hpz937\Phpvault\Middleware\AuthMiddleware;
use Hpz937\Phpvault\Vault;
use Psr\Container\ContainerInterface;
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
$app = AppFactory::create();
$container = new Container();
$container->set(AuthMiddleware::class, function (ContainerInterface $container) {
$authHandler = $container->get(AuthHandler::class);
return new AuthMiddleware($authHandler);
});
$container->set(Database::class, function () {
return new Database();
});
$container->set(DataEncryptor::class, function () {
return new DataEncryptor($_ENV['ENCRYPTION_KEY']);
});
// Set up the AuthHandler in the container
$container->set(AuthHandler::class, function () {
$secretKey = $_ENV['JWT_SECRET_KEY'];
$database = new Database(); // Assuming you have a Database class
return new AuthHandler($secretKey, $database);
});
$authMiddleware = $container->get(AuthMiddleware::class);
AppFactory::setContainer($container);
$app->post('/login', function ($request, $response) use ($container) {
$data = $request->getParsedBody();
$username = $data['username'];
$password = $data['password'];
$authHandler = $container->get(AuthHandler::class);
$token = $authHandler->generateToken($username, $password);
if ($token) {
$response->getBody()->write(json_encode(['token' => $token]));
return $response->withStatus(200);
} else {
$response->getBody()->write(json_encode(['error' => 'Invalid credentials']));
return $response->withStatus(401);
}
});
$app->post('/addUser', function ($request, $response) use ($container) {
$data = $request->getParsedBody();
$username = $data['username'];
$password = $data['password'];
$authHandler = $container->get(AuthHandler::class);
$token = $authHandler->addUser($username, $password);
return $response->withStatus(201);
});
$app->post('/manage/{vaultName}', function ($request, $response, array $args) use ($container) {
try {
// the sent body will be a json object
$secret = $request->getBody()->getContents();
// if secret is empty or secret is not valid json data return 400
if (empty($secret) || json_decode($secret) === null) {
$response->getBody()->write(json_encode(['error' => 'Invalid secret']));
return $response->withStatus(400);
}
if (!isset(json_decode($secret, true)['key'])) {
$response->getBody()->write(json_encode(['error' => 'Key is required']));
return $response->withStatus(400);
}
$key = json_decode($secret, true)['key'];
$username = $request->getAttribute('username');
if (!isset($args['vaultName'])) {
$response->getBody()->write(json_encode(['error' => 'Vault name is required']));
return $response->withStatus(400);
}
$vaultName = $args['vaultName'];
$vault = $container->get(Vault::class);
$vault->storeSecret($username, $vaultName, $key, $secret);
$response->getBody()->write(json_encode(['message' => 'Secret stored']));
return $response->withStatus(201);
} catch (Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(500);
}
})->add($authMiddleware);
$app->put('/manage/{vaultName}', function ($request, $response, array $args) use ($container) {
try {
// the sent body will be a json object
$secret = $request->getBody()->getContents();
// if secret is empty or secret is not valid json data return 400
if (empty($secret) || json_decode($secret) === null) {
$response->getBody()->write(json_encode(['error' => 'Invalid secret']));
return $response->withStatus(400);
}
if (!isset(json_decode($secret, true)['key'])) {
$response->getBody()->write(json_encode(['error' => 'Key is required']));
return $response->withStatus(400);
}
$key = json_decode($secret, true)['key'];
$username = $request->getAttribute('username');
if (!isset($args['vaultName'])) {
$response->getBody()->write(json_encode(['error' => 'Vault name is required']));
return $response->withStatus(400);
}
$vaultName = $args['vaultName'];
$vault = $container->get(Vault::class);
$vault->updateSecret($username, $vaultName, $key, $secret);
$response->getBody()->write(json_encode(['message' => 'Secret updated']));
return $response->withStatus(201);
} catch (Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(500);
}
})->add($authMiddleware);
$app->delete('/manage/{vaultName}', function ($request, $response, array $args) use ($container) {
try {
$username = $request->getAttribute('username');
$vaultName = $args['vaultName'];
$vault = $container->get(Vault::class);
$vault->deleteSecret($username, $vaultName);
$response->getBody()->write(json_encode(['message' => 'Secret deleted']));
return $response->withStatus(200);
} catch (Exception $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(500);
}
})->add($authMiddleware);
$app->post('/vault/{vaultName}', function ($request, $response, array $args) use ($container) {
// the sent body will be a json object
$secret = $request->getBody()->getContents();
// if secret is empty or secret is not valid json data return 400
if (empty($secret) || json_decode($secret) === null || json_decode($secret)->key === null) {
$response->getBody()->write(json_encode(['error' => 'Invalid Key']));
return $response->withStatus(400);
}
$key = json_decode($secret)->key;
$username = $request->getAttribute('username');
if (!isset($args['vaultName'])) {
$response->getBody()->write(json_encode(['error' => 'Vault name is required']));
return $response->withStatus(400);
}
$vaultName = $args['vaultName'];
$vault = $container->get(Vault::class);
$secret = $vault->getSecret($username, $key, $vaultName);
if ($secret) {
$response->getBody()->write($secret);
return $response->withStatus(200);
} else {
$response->getBody()->write(json_encode(['error' => 'Secret not found']));
return $response->withStatus(404);
}
})->add($authMiddleware);
$app->run();

54
src/Database.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
namespace Hpz937\Phpvault;
use SQLite3;
class Database {
private $db;
public function __construct() {
$this->db = new SQLite3(__DIR__ . '/../database/db.sqlite');
if (!$this->db) {
echo 'Unable to connect to database';
exit;
}
}
public function query($query) {
return $this->db->query($query);
}
public function createTables() {
$queries = [
'CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)',
'CREATE TABLE IF NOT EXISTS vault (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
vaultname TEXT NOT NULL,
encrypted_data TEXT NOT NULL
)',
'CREATE INDEX IF NOT EXISTS username_idx ON users (username)',
'CREATE UNIQUE INDEX IF NOT EXISTS vault_unique_idx ON vault (username, vaultname);',
];
foreach ($queries as $query) {
if (!$this->db->exec($query)) {
echo 'Error creating tables: ' . $this->db->lastErrorMsg();
exit;
}
}
}
public function getDb() {
return $this->db;
}
public function close() {
$this->db->close();
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Hpz937\Phpvault\Handler;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Hpz937\Phpvault\Database;
use stdClass;
class AuthHandler
{
private $secretKey;
private $database;
public function __construct($secretKey, Database $database)
{
$this->secretKey = $secretKey;
$this->database = $database->getDb();
}
public function generateToken($username, $password)
{
$query = "SELECT * FROM users WHERE username='$username'";
$result = $this->database->query($query);
if (!$result) {
return null;
}
$user = $result->fetchArray();
if (!$user) {
return null;
}
if (password_verify($password, $user['password'])) {
$payload = [
'username' => $username,
];
$issuedAt = time();
$expirationTime = $issuedAt + 3600; // jwt valid for 1 hour
$payload['iat'] = $issuedAt;
$payload['exp'] = $expirationTime;
return JWT::encode($payload, $this->secretKey, 'HS256');
} else {
return null;
}
}
public function addUser($username, $password)
{
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$username = $this->database->escapeString($username);
$hashedPassword = $this->database->escapeString($hashedPassword);
$query = "INSERT INTO users (username, password) VALUES ('$username', '$hashedPassword')";
return $this->database->exec($query);
}
public function verifyToken($token)
{
try {
if (! preg_match('/Bearer\s(\S+)/', $token, $matches)) {
throw new Exception('Invalid token');
}
$decoded = JWT::decode($matches[1], new Key($this->secretKey, 'HS256'));
return $decoded->username;
} catch (Exception $e) {
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Hpz937\Phpvault\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response as SlimResponse;
use Hpz937\Phpvault\Handler\AuthHandler;
class AuthMiddleware
{
private $authHandler;
public function __construct(AuthHandler $authHandler)
{
$this->authHandler = $authHandler;
}
public function __invoke(Request $request, RequestHandler $handler): Response
{
$authHeader = $request->getHeaderLine('Authorization');
// Verify the token
$username = $this->authHandler->verifyToken($authHeader);
if ($username === null) {
// Token is invalid, return 401 Unauthorized
$response = new SlimResponse();
$response->getBody()->write(json_encode(['error' => 'Unauthorized']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
$request = $request->withAttribute('username', $username);
// Token is valid, proceed to the next middleware/route
return $handler->handle($request);
}
}

95
src/Vault.php Normal file
View File

@@ -0,0 +1,95 @@
<?php
namespace Hpz937\Phpvault;
use Hpz937\Encryption\DataEncryptor;
class Vault
{
private $database;
public function __construct(Database $database)
{
$this->database = $database->getDb();
}
public function storeSecret(string $userName, string $vaultName, string $key, string $secret)
{
$jsonSecret = json_decode($secret, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return false;
}
$key = $jsonSecret['key'];
if (!$key) {
return false;
}
unset($jsonSecret['key']);
$dataEncryptor = new DataEncryptor($key);
$encryptedSecret = $dataEncryptor->encrypt(json_encode($jsonSecret));
// make sure vaultname does not exist first
$query = "SELECT * FROM vault WHERE username = '$userName' AND vaultname = '$vaultName'";
$result = $this->database->query($query);
$row = $result->fetchArray();
if ($row) {
throw new \Exception('Vault already exists, try PUT to update');
}
$query = "INSERT INTO vault (username, vaultname, encrypted_data) VALUES ('$userName', '$vaultName', '$encryptedSecret')";
if ($result = $this->database->exec($query)) {
return true;
}
}
public function getSecret(string $userName, string $key, string $vaultName)
{
$query = "SELECT encrypted_data FROM vault WHERE username = '$userName' AND vaultname = '$vaultName'";
$result = $this->database->query($query);
$row = $result->fetchArray();
if ($row) {
$dataEncryptor = new DataEncryptor($key);
return $dataEncryptor->decrypt($row['encrypted_data']);
}
return null;
}
public function updateSecret(string $userName, string $vaultName, string $key, string $secret)
{
$jsonSecret = json_decode($secret, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return false;
}
$key = $jsonSecret['key'];
if (!$key) {
return false;
}
unset($jsonSecret['key']);
$dataEncryptor = new DataEncryptor($key);
$encryptedSecret = $dataEncryptor->encrypt(json_encode($jsonSecret));
$query = "SELECT * FROM vault WHERE username = '$userName' AND vaultname = '$vaultName'";
$result = $this->database->query($query);
$row = $result->fetchArray();
if ($row) {
$query = "UPDATE vault SET encrypted_data = '$encryptedSecret' WHERE username = '$userName' AND vaultname = '$vaultName'";
if ($this->database->exec($query)) {
return true;
}
} else {
throw new \Exception('Vault does not exist, try POST to create');
}
}
public function deleteSecret(string $userName, string $vaultName)
{
$query = "SELECT * FROM vault WHERE username = '$userName' AND vaultname = '$vaultName'";
$result = $this->database->query($query);
$row = $result->fetchArray();
if ($row) {
$query = "DELETE FROM vault WHERE username = '$userName' AND vaultname = '$vaultName'";
if ($this->database->exec($query)) {
return true;
}
} else {
throw new \Exception('Vault does not exist');
}
}
}