<?php
/**
* PassGram v5.0 - Setup Script
*
* Initialises PassGram by:
* 1. Detecting server capabilities (PHP extensions, GnuPG support)
* 2. Generating the Master Application Key (MAK)
* 3. Optionally enabling GnuPG multi-recipient group sharing
* 4. Creating the first admin user
* 5. Creating the first group
* 6. Initialising the database
* 7. Generating an invite code for additional users
*
* Delete this file after installation is complete.
*/
require_once __DIR__ . '/autoload.php';
use PassGram\Core\Config;
use PassGram\Core\Database;
use PassGram\Security\Encryption;
use PassGram\Security\GnuPGEncryption;
use PassGram\Models\User;
use PassGram\Models\Group;
use PassGram\Models\Invite;
use PassGram\Helpers\Validator;
// ---------------------------------------------------------------------------
// Detect server capabilities
// ---------------------------------------------------------------------------
$gnupgAvailable = GnuPGEncryption::isAvailable();
$gnupgStatusStr = $gnupgAvailable ? GnuPGEncryption::statusString() : 'php-gnupg extension not found';
// ---------------------------------------------------------------------------
// Generate missing config files before Config is loaded.
// This lets the installer run on a completely fresh deployment where only
// autoload.php and setup.php exist in the project root.
// ---------------------------------------------------------------------------
$configDir = __DIR__ . '/config';
if (!file_exists($configDir . '/config.php')) {
file_put_contents($configDir . '/config.php', <<<'CFGPHP'
<?php
/**
* PassGram v5.0 - Application Configuration
*
* Generated during installation.
* Edit base_url and set cookie_secure to true before deploying to production.
*/
return [
'name' => 'PassGram',
'version' => '5.0',
'base_url' => '', // e.g. https://your-domain.com (no trailing slash)
'debug' => false,
// Session settings
'session' => [
'timeout' => 3600, // seconds (60 minutes)
'name' => 'passgram_session',
'cookie_secure' => false, // set true when serving over HTTPS
'cookie_httponly' => true,
'cookie_samesite' => 'Strict',
],
// Input validation / security rules
'security' => [
'password_min_length' => 12,
'username_min_length' => 3,
'username_max_length' => 32,
'max_login_attempts' => 5,
'lockout_duration' => 900, // seconds (15 minutes)
'invite_expiry_days' => 7,
],
// Filesystem paths used at runtime
'paths' => [
'pgp' => dirname(__DIR__) . '/data/pgp',
'gnupg' => dirname(__DIR__) . '/data/gnupg', // GnuPG keyring directory
],
// GnuPG multi-recipient group sharing (set by setup.php)
'gnupg_enabled' => false,
];
CFGPHP);
}
if (!file_exists($configDir . '/security.php')) {
file_put_contents($configDir . '/security.php', <<<'SECPHP'
<?php
/**
* PassGram v5.0 - Security Configuration
*
* Generated during installation. Keep this file backed up in a secure location.
* If the master_application_key is ever lost, all encrypted data is unrecoverable.
*/
return [
// Master Application Key (MAK) — set by setup.php during first run
'master_application_key' => 'REPLACE_WITH_GENERATED_KEY_DURING_INSTALLATION',
// AES-256-GCM settings used by the Encryption class
'encryption' => [
'algorithm' => 'aes-256-gcm',
'key_length' => 32, // bytes
'iv_length' => 12, // bytes (96-bit nonce, recommended for GCM)
'tag_length' => 16, // bytes (128-bit authentication tag)
],
// Argon2id parameters for key derivation (PGP private-key passphrase)
'argon2' => [
'memory_cost' => 65536, // KB (64 MB)
'time_cost' => 4, // iterations
'threads' => 2,
],
// bcrypt cost factor for password hashing
'bcrypt_cost' => 12,
];
SECPHP);
}
// Check if already installed
$config = Config::getInstance();
if ($config->isInstalled()) {
die("PassGram is already installed. Delete config/security.php to reinstall.");
}
// ---------------------------------------------------------------------------
// Handle POST submission
// ---------------------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$adminUsername = trim($_POST['admin_username'] ?? '');
$adminEmail = trim($_POST['admin_email'] ?? '');
$adminPassword = $_POST['admin_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$enableGnuPG = isset($_POST['enable_gnupg']) && $gnupgAvailable;
// Validate inputs
$validator = new Validator($config);
if (!$validator->username($adminUsername)) {
throw new \Exception($validator->getFirstError());
}
if (!$validator->email($adminEmail)) {
throw new \Exception($validator->getFirstError());
}
if (!$validator->password($adminPassword)) {
throw new \Exception($validator->getFirstError());
}
if ($adminPassword !== $confirmPassword) {
throw new \Exception('Passwords do not match');
}
// Generate Master Application Key
$masterKey = Encryption::generateKey();
$masterKeyHex = bin2hex($masterKey);
// Write security.php with the generated MAK
$generatedDate = date('Y-m-d H:i:s');
file_put_contents($configDir . '/security.php', <<<SECPHP
<?php
/**
* PassGram v5.0 - Security Configuration
*
* Generated: {$generatedDate}
* Keep this file backed up in a secure location.
* If the master_application_key is ever lost, all encrypted data is unrecoverable.
*/
return [
// Master Application Key (MAK) — generated by setup.php
'master_application_key' => '{$masterKeyHex}',
// AES-256-GCM settings used by the Encryption class
'encryption' => [
'algorithm' => 'aes-256-gcm',
'key_length' => 32, // bytes
'iv_length' => 12, // bytes (96-bit nonce, recommended for GCM)
'tag_length' => 16, // bytes (128-bit authentication tag)
],
// Argon2id parameters for key derivation (PGP private-key passphrase)
'argon2' => [
'memory_cost' => 65536, // KB (64 MB)
'time_cost' => 4, // iterations
'threads' => 2,
],
// bcrypt cost factor for password hashing
'bcrypt_cost' => 12,
];
SECPHP);
// Write config.php with GnuPG setting
$gnupgEnabledStr = $enableGnuPG ? 'true' : 'false';
file_put_contents($configDir . '/config.php', <<<CFGPHP
<?php
/**
* PassGram v5.0 - Application Configuration
*
* Generated: {$generatedDate}
* Edit base_url and set cookie_secure to true before deploying to production.
*/
return [
'name' => 'PassGram',
'version' => '5.0',
'base_url' => '', // e.g. https://your-domain.com (no trailing slash)
'debug' => false,
// Session settings
'session' => [
'timeout' => 3600,
'name' => 'passgram_session',
'cookie_secure' => false,
'cookie_httponly' => true,
'cookie_samesite' => 'Strict',
],
// Input validation / security rules
'security' => [
'password_min_length' => 12,
'username_min_length' => 3,
'username_max_length' => 32,
'max_login_attempts' => 5,
'lockout_duration' => 900,
'invite_expiry_days' => 7,
],
// Filesystem paths used at runtime
'paths' => [
'pgp' => dirname(__DIR__) . '/data/pgp',
'gnupg' => dirname(__DIR__) . '/data/gnupg',
],
// GnuPG multi-recipient group sharing
// When true, every new user receives a GnuPG key pair at registration
// and group credential shares are encrypted once for all members.
'gnupg_enabled' => {$gnupgEnabledStr},
];
CFGPHP);
// Initialize database
$encryption = new Encryption($masterKey, $config->get('security.encryption'));
$db = new Database($encryption, $config->database());
$db->initialize();
// Create models
$userModel = new User($db, $config, $validator);
$groupModel = new Group($db, $validator);
$inviteModel = new Invite($db, $config, $validator);
// Create admin user
$admin = $userModel->create([
'username' => $adminUsername,
'email' => $adminEmail,
'password' => $adminPassword,
]);
// If GnuPG is enabled, generate a key pair for the admin user
$adminGnuPGKey = null;
if ($enableGnuPG) {
// Admin's GnuPG passphrase = their login password for initial setup.
// They can rotate it later via the key-management UI.
$gnupgHome = __DIR__ . '/data/gnupg';
$gnupg = new GnuPGEncryption($gnupgHome);
$adminGnuPGKey = $gnupg->generateKeyPair(
$adminUsername,
$adminEmail,
$adminPassword // passphrase = login password at setup time
);
$userModel->setGnuPGFingerprint($admin['id'], $adminGnuPGKey['fingerprint']);
}
// Create default group
$group = $groupModel->create([
'name' => 'Default Group',
'description' => 'Initial group for PassGram users',
'created_by' => $admin['id'],
]);
// Add admin to group
$userModel->addToGroup($admin['id'], $group['id']);
// Generate invite code for additional users
$invite = $inviteModel->generate($group['id'], $admin['id']);
$inviteCode = $invite['code'];
$success = true;
} catch (\Exception $e) {
$error = $e->getMessage();
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PassGram v5.0 - Setup</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', Courier, monospace;
background: #008080;
color: #000;
padding: 20px;
}
.container {
max-width: 640px;
margin: 40px auto;
background: #C0C0C0;
border: 3px outset #fff;
box-shadow: 3px 3px 0 #000;
}
.title-bar {
background: #5D009D;
color: #fff;
padding: 4px 8px;
font-weight: bold;
border-bottom: 2px solid #000;
}
.content { padding: 20px; }
h1 { font-size: 20px; margin-bottom: 20px; }
h2 { font-size: 16px; margin-bottom: 10px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 4px;
border: 2px inset #808080;
background: #fff;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
}
input[type="checkbox"] { margin-right: 6px; }
.button {
background: #C0C0C0;
border: 2px outset #fff;
padding: 6px 20px;
cursor: pointer;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
font-weight: bold;
}
.button:active { border-style: inset; }
.error {
background: #ff0000;
color: #fff;
padding: 10px;
margin-bottom: 15px;
border: 2px solid #800000;
}
.success {
background: #00ff00;
color: #000;
padding: 20px;
border: 2px solid #008000;
}
.info {
background: #ffff00;
padding: 10px;
margin-bottom: 15px;
border: 2px solid #808000;
}
.code {
background: #000;
color: #0f0;
padding: 10px;
font-family: 'Courier New', Courier, monospace;
word-break: break-all;
margin: 10px 0;
}
.requirements {
background: #fff;
border: 2px inset #808080;
padding: 15px;
margin-bottom: 20px;
}
.requirements ul { margin-left: 20px; }
.requirements li { margin-bottom: 5px; }
.ok { color: #007700; font-weight: bold; }
.bad { color: #cc0000; font-weight: bold; }
.opt { color: #555555; }
.gnupg-section {
background: #e8e8ff;
border: 2px inset #808080;
padding: 15px;
margin-bottom: 20px;
}
.gnupg-section h2 { margin-bottom: 8px; }
small { display: block; margin-top: 3px; color: #444; font-size: 12px; }
hr { margin: 20px 0; border: 1px solid #808080; }
</style>
</head>
<body>
<div class="container">
<div class="title-bar">PassGram v5.0 - Setup</div>
<div class="content">
<?php if (isset($success) && $success): ?>
<div class="success">
<h1>Setup Complete!</h1>
<p><strong>PassGram v5.0 has been installed successfully.</strong></p>
<p style="margin-top:15px;">Admin username: <strong><?php echo htmlspecialchars($adminUsername); ?></strong></p>
<p>Admin email: <strong><?php echo htmlspecialchars($adminEmail); ?></strong></p>
<?php if ($adminGnuPGKey): ?>
<p style="margin-top:15px;"><strong>GnuPG Mode:</strong> Enabled</p>
<p>Admin GnuPG fingerprint:</p>
<div class="code"><?php echo htmlspecialchars($adminGnuPGKey['fingerprint']); ?></div>
<p style="margin-top:8px;">Your GnuPG passphrase is the same as your login password
(set during this setup). You can rotate it from the key-management page after
logging in.</p>
<?php endif; ?>
<p style="margin-top:20px;"><strong>Invite Code for Additional Users:</strong></p>
<div class="code"><?php echo htmlspecialchars($inviteCode); ?></div>
<div class="info" style="margin-top:20px;">
<strong>IMPORTANT SECURITY NOTES:</strong>
<ul style="margin-left:20px; margin-top:10px;">
<li>Delete this <strong>setup.php</strong> file immediately</li>
<li>Backup <strong>config/security.php</strong> in a secure location</li>
<li>Save the invite code — you'll need it to register more users</li>
<li>Configure HTTPS on your server</li>
<li>Update <strong>config/config.php</strong> with your domain</li>
<?php if ($adminGnuPGKey): ?>
<li>The GnuPG keyring lives in <strong>data/gnupg/</strong> —
keep that directory backed up</li>
<?php endif; ?>
</ul>
</div>
<p style="margin-top:20px;">
<a href="/login.php" class="button">Go to Login Page</a>
</p>
</div>
<?php else: ?>
<h1>PassGram Setup</h1>
<!-- ----------------------------------------------------------------
Server Requirements
---------------------------------------------------------------- -->
<div class="requirements">
<h2>Server Requirements:</h2>
<ul>
<li>
PHP <?php echo phpversion(); ?>
<?php if (version_compare(phpversion(), '7.4.0', '>=')):?>
<span class="ok">✓</span>
<?php else: ?>
<span class="bad">✗ (7.4+ required)</span>
<?php endif; ?>
</li>
<li>
OpenSSL Extension
<?php if (extension_loaded('openssl')): ?>
<span class="ok">✓</span>
<?php else: ?>
<span class="bad">✗ (required)</span>
<?php endif; ?>
</li>
<li>
JSON Extension
<?php if (extension_loaded('json')): ?>
<span class="ok">✓</span>
<?php else: ?>
<span class="bad">✗ (required)</span>
<?php endif; ?>
</li>
<li>
Writable data/ directory
<?php if (is_writable(__DIR__ . '/data')): ?>
<span class="ok">✓</span>
<?php else: ?>
<span class="bad">✗ (required)</span>
<?php endif; ?>
</li>
<li>
Writable config/ directory
<?php if (is_writable(__DIR__ . '/config')): ?>
<span class="ok">✓</span>
<?php else: ?>
<span class="bad">✗ (required)</span>
<?php endif; ?>
</li>
<li>
GnuPG Extension (php-gnupg) — optional, for group sharing
<?php if ($gnupgAvailable): ?>
<span class="ok">✓ <?php echo htmlspecialchars($gnupgStatusStr); ?></span>
<?php else: ?>
<span class="opt">◯ Not available — <?php echo htmlspecialchars($gnupgStatusStr); ?></span>
<?php endif; ?>
</li>
</ul>
</div>
<?php if (isset($error)): ?>
<div class="error">
<strong>Error:</strong> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<div class="info">
<strong>Note:</strong> This will create the first admin user and generate a
secure Master Application Key. Keep this key secure — if lost, all data
will be unrecoverable!
</div>
<!-- ----------------------------------------------------------------
GnuPG Group Sharing Option
---------------------------------------------------------------- -->
<div class="gnupg-section">
<h2>GnuPG Group Sharing</h2>
<?php if ($gnupgAvailable): ?>
<p style="margin-bottom:10px;">
The <strong>php-gnupg</strong> extension is available on this server.
Enabling GnuPG group sharing allows a credential to be encrypted
<em>once</em> for an entire group, so any member can decrypt it with
their own private key passphrase — no per-user re-encryption needed.
</p>
<p style="margin-bottom:10px;">
When enabled, every new user is issued a GnuPG key pair at registration.
The keyring is stored in <code>data/gnupg/</code> on the server.
</p>
<div class="form-group">
<label>
<input type="checkbox" name="enable_gnupg" form="setup-form"
id="enable_gnupg" value="1"
<?php echo isset($_POST['enable_gnupg']) ? 'checked' : ''; ?>>
Enable GnuPG multi-recipient group sharing
</label>
</div>
<?php else: ?>
<p style="color:#666;">
The <strong>php-gnupg</strong> extension is <strong>not installed</strong>
on this server. To enable GnuPG group sharing, install the extension
(<code>apt install php-gnupg</code> or equivalent) and re-run setup.
</p>
<p style="margin-top:8px; color:#444;">
PassGram will use the standard OpenSSL-based per-user encryption for
credential sharing.
</p>
<?php endif; ?>
</div>
<!-- ----------------------------------------------------------------
Admin Account Form
---------------------------------------------------------------- -->
<form method="POST" id="setup-form">
<div class="form-group">
<label for="admin_username">Admin Username:</label>
<input type="text" id="admin_username" name="admin_username" required
value="<?php echo htmlspecialchars($_POST['admin_username'] ?? ''); ?>">
<small>3–32 characters, letters, numbers, underscore only</small>
</div>
<div class="form-group">
<label for="admin_email">Admin Email:</label>
<input type="email" id="admin_email" name="admin_email" required
value="<?php echo htmlspecialchars($_POST['admin_email'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="admin_password">Admin Password:</label>
<input type="password" id="admin_password" name="admin_password" required>
<small>Minimum 12 characters, must include uppercase, lowercase,
numbers, and special characters</small>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password:</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<?php if ($gnupgAvailable): ?>
<div class="info" id="gnupg-note" style="display:none;">
<strong>GnuPG Note:</strong> When GnuPG mode is enabled, the admin's
GnuPG key passphrase is set to the login password entered above.
You can rotate the passphrase from the key-management page after setup.
</div>
<?php endif; ?>
<div class="form-group">
<button type="submit" class="button">Install PassGram</button>
</div>
</form>
<?php endif; ?>
</div><!-- .content -->
</div><!-- .container -->
<?php if ($gnupgAvailable): ?>
<script>
(function () {
var cb = document.getElementById('enable_gnupg');
var note = document.getElementById('gnupg-note');
if (!cb || !note) return;
function sync() { note.style.display = cb.checked ? 'block' : 'none'; }
cb.addEventListener('change', sync);
sync();
})();
</script>
<?php endif; ?>
</body>
</html>