GitGram — setup.php — GitGram
PassGram / main / v6.00 / setup.php23,163 B↓ Raw
<?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 &mdash; 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> &mdash;
                        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">&#10003;</span>
                    <?php else: ?>
                        <span class="bad">&#10007; (7.4+ required)</span>
                    <?php endif; ?>
                </li>
                <li>
                    OpenSSL Extension
                    <?php if (extension_loaded('openssl')): ?>
                        <span class="ok">&#10003;</span>
                    <?php else: ?>
                        <span class="bad">&#10007; (required)</span>
                    <?php endif; ?>
                </li>
                <li>
                    JSON Extension
                    <?php if (extension_loaded('json')): ?>
                        <span class="ok">&#10003;</span>
                    <?php else: ?>
                        <span class="bad">&#10007; (required)</span>
                    <?php endif; ?>
                </li>
                <li>
                    Writable data/ directory
                    <?php if (is_writable(__DIR__ . '/data')): ?>
                        <span class="ok">&#10003;</span>
                    <?php else: ?>
                        <span class="bad">&#10007; (required)</span>
                    <?php endif; ?>
                </li>
                <li>
                    Writable config/ directory
                    <?php if (is_writable(__DIR__ . '/config')): ?>
                        <span class="ok">&#10003;</span>
                    <?php else: ?>
                        <span class="bad">&#10007; (required)</span>
                    <?php endif; ?>
                </li>
                <li>
                    GnuPG Extension (php-gnupg) &mdash; optional, for group sharing
                    <?php if ($gnupgAvailable): ?>
                        <span class="ok">&#10003; <?php echo htmlspecialchars($gnupgStatusStr); ?></span>
                    <?php else: ?>
                        <span class="opt">&#9711; Not available &mdash; <?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 &mdash; 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 &mdash; 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&ndash;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>
Ready
GitGram