Initial commit
This commit is contained in:
2
.env.dist
Normal file
2
.env.dist
Normal file
@@ -0,0 +1,2 @@
|
||||
JWT_SECRET_KEY=a1b2c3d4e5f6g7h8i9j0
|
||||
ENCRYPTION_KEY=a1b2c3d4e5f6g7h8i9j0
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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
42
bin/manage.php
Normal 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
30
composer.json
Normal 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
1596
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
192
public/index.php
Normal file
192
public/index.php
Normal 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
54
src/Database.php
Normal 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();
|
||||
}
|
||||
}
|
||||
70
src/Handler/AuthHandler.php
Normal file
70
src/Handler/AuthHandler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Middleware/AuthMiddleware.php
Normal file
38
src/Middleware/AuthMiddleware.php
Normal 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
95
src/Vault.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user