Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Apr1 Md5 Laravel Package

whitehat101/apr1-md5

A small PHP implementation of the Apache APR1-MD5 password hash for generating and verifying htpasswd-compatible credentials. Offers simple hashing and checking APIs without external extensions, making it easy to integrate into Laravel or legacy PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

whitehat101/apr1-md5 is one class with two static methods. It implements Apache's $apr1$ hash — the algorithm behind htpasswd -m and the $apr1$... entries in .htpasswd Basic Auth files. No service provider, no config, no facade. Install it and call it directly:

composer require whitehat101/apr1-md5
use WhiteHat101\Crypt\APR1_MD5;

APR1_MD5::hash('secret');                     // "$apr1$<salt>$<digest>" (random salt)
APR1_MD5::hash('secret', 'abc123');           // caller-supplied salt
APR1_MD5::check('secret', $storedHash);       // true / false

Read the source first. Before building anything on top of it, open vendor/whitehat101/apr1-md5/src/APR1_MD5.php. It is short enough to read end-to-end in a couple of minutes, and you will want to confirm two things yourself: how it handles malformed input (does check() throw or return false?), and whether it uses a constant-time string comparison for the digest (see Gotchas — do not take this on faith).

First real use case: authenticating a user against a stored .htpasswd-style hash without shelling out to htpasswd or requiring the Apache binary in the container. A one-off script or artisan command to verify existing hashes looks like:

$ok = APR1_MD5::check($request->input('password'), $user->password);

That is the entire day-to-day surface. Everything else in this assessment is about wrapping it so you can delete it later.

Implementation Patterns

Wrap it behind a contract, not inline calls. Because the API is two static methods, it is tempting to call APR1_MD5::check() directly in a controller or middleware. Do not. Put it behind a single interface so you can swap the implementation for Laravel's Hash::check() (or delete it) when the migration finishes:

interface LegacyHasher
{
    public function verify(string $plain, string $stored): bool;
    public function make(string $plain): string; // transitional only
}

final class Apr1Hasher implements LegacyHasher
{
    public function verify(string $plain, string $stored): bool
    {
        return str_starts_with($stored, '$apr1$')
            && APR1_MD5::check($plain, $stored);
    }

    public function make(string $plain): string
    {
        return APR1_MD5::hash($plain);
    }
}

Binding it in AppServiceProvider keeps the vendor class namespaced to a single class in your codebase.

Register it as a Laravel hashing driver. This is usually the cleanest integration point because every caller (auth controllers, seeders, tests) can keep using Hash::check() without knowing which driver is active:

// AppServiceProvider::boot()
Hash::extend('apr1', fn () => new class implements \Illuminate\Contracts\Hashing\Hasher {
    public function make($value, array $options = [])
    {
        return \WhiteHat101\Crypt\APR1_MD5::hash($value);
    }
    public function check($value, $hashedValue, array $options = [])
    {
        if (! is_string($hashedValue) || ! str_starts_with($hashedValue, '$apr1$')) {
            return false;
        }
        return \WhiteHat101\Crypt\APR1_MD5::check($value, $hashedValue);
    }
    public function needsRehash($hashedValue, array $options = [])
    {
        return str_starts_with($hashedValue, '$apr1$');
    }
});

Then resolve it explicitly only where you need it: Hash::driver('apr1')->check($password, $user->password). Resist setting 'driver' => 'apr1' globally in config/hashing.php — that makes the legacy path the default and is precisely the mistake you eventually have to undo.

Rehash-on-login. This is the pattern most teams actually ship: verify against the legacy hash, then immediately upgrade the stored hash to something modern, so the legacy dependency shrinks to zero as users log in.

if (Hash::driver('apr1')->check($password, $user->password)) {
    $user->forceFill([
        'password' => Hash::make($password),
    ])->save();
    return $user;
}

Store the algorithm (or just sniff the $apr1$ prefix) so the login path can decide which driver to use without a schema change. A password_algo column is worth the migration if you also have {SHA} or bcrypt records in the same table (see Gotchas).

Bulk import from .htpasswd. If you are loading an existing Apache credential file into a user table, copy the hash verbatim. Do not call APR1_MD5::hash() on the plaintext — you do not have the plaintext, and even if you did, calling hash() would produce a new salt and a different string. Just split on : and copy:

foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
    [$user, $hash] = explode(':', $line, 2);
    User::where('username', $user)->update(['password' => $hash]);
}

Fixture tests. Round-trip fidelity with real Apache output is where the bugs hide. Capture a handful of hashes from htpasswd -m on a dev machine, commit them as fixtures, and assert that APR1_MD5::check('correct-horse', $fixture) is true and APR1_MD5::check('wrong', $fixture) is false. This is especially important because upstream has 0 dependents and low activity — your test suite is the contract.

Gotchas and Tips

  • Never use APR1_MD5::hash() for new passwords. APR1 is MD5 with ~1000 iterations and a short salt — it was obsolete before Laravel existed. This package is for reading legacy hashes only. If make() shows up in a registration or password-reset flow, that is a bug worth catching in code review. New credentials go through Hash::make() (bcrypt/Argon2id) from day one.

  • .htpasswd is not exclusively APR1. Apache will happily write plaintext, {SHA} (base64 SHA-1), $2y$ bcrypt, and $5$/$6$ SHA-crypt into the same file depending on which htpasswd flag was used. Prefix-check before dispatching: $apr1$ → this package, $2y$/$2a$ → Hash::check(), {SHA} → base64 sha1() compare, bare string → plaintext (and treat that as a red flag to rehash immediately).

  • Validate the prefix inside your wrapper. A malformed or non-APR1 string passed to APR1_MD5::check() may throw or return something surprising depending on the vendor version. Guarding with str_starts_with($stored, '$apr1$') before calling into the class makes your login path cheap to reason about and impossible to blow up on a stray bcrypt hash in the same column.

  • Verify constant-time comparison yourself. Some APR1 implementations compare the digest with ==, which is timing-sensitive on the hash suffix. Whether this package does is not something to assume — read the source (it is one file). If it does not, use hash_equals() in your wrapper, or accept the risk explicitly with a note in the migration ticket.

  • Treat the package as unmaintained-until-proven. 34 stars, 0 dependents, single maintainer, no recent activity. MIT permits vendoring: if the algorithm only needs to survive a 3–6 month migration window, copy src/APR1_MD5.php into your own namespace, pin it with a comment linking upstream, and record a removal date. That removes supply-chain and abandonment risk for the cost of ~100 lines you would have read anyway.

  • Salt is up to 8 characters, and it's user-controllable. APR1_MD5::hash($plain, $salt) accepts a caller-supplied salt. Do not allow user input to reach that second argument in any path that matters — a caller-chosen salt is an unnecessary footgun even for transitional generation. Call APR1_MD5::hash($plain) and let it generate.

  • No service provider, no facade, no config file. If you want it discoverable via the container, bind it yourself. Laravel's auto-discovery does nothing for this package, and there is no Laravel\... glue to look for — the "integration" is entirely the wrapper you write.

  • Pin PHP version compatibility in composer.json and run CI on your target runtime. Given the low activity level, an upgrade from PHP 8.2 to 8.3 or 8.4 is a good moment to re-run the fixture tests. If the package fails to autoload on a new PHP version, vendoring (see above) makes the fix a one-line str_* change instead of a fork.

  • Sunset criterion on the ticket, not on the roadmap. The dependency does not become safe by staying in the codebase — it becomes safe by leaving. Write the exit condition (migrated-user count, or a date) into the migration ticket at the same time you add the composer requirement, and delete the wrapper and the dependency together when it triggers.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk