Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ SESSION_LIFETIME=120
LDAP_HOST=
LDAP_USERNAME=
LDAP_PASSWORD=
# Obfuscation scheme used for LDAP_PASSWORD: none (default) or sss (SSSD's sss_obfuscate(8) format)
LDAP_PASSWORD_OBFUSCATION=none
LDAP_CACHE=false
LDAP_ALERT_ROOTDN=true
80 changes: 80 additions & 0 deletions app/Classes/LDAP/SSSDPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace App\Classes\LDAP;

use InvalidArgumentException;

/**
* Encodes/decodes passwords using SSSD's "obfuscated_password" format, ie compatible with
* sss_obfuscate(8) and ldap_default_authtok_type=obfuscated_password in sssd.conf.
*
* This is *not* encryption - the AES key travels in the same buffer as the ciphertext, so
* anyone with the obfuscated string can trivially recover the password. It only avoids the
* password appearing as cleartext in configuration.
*
* Buffer layout, base64 encoded (see sssd/src/util/crypto/libcrypto/crypto_obfuscate.c):
* uint16 method (0 = AES-256-CBC)
* uint16 ciphertext length
* 32 bytes key
* 16 bytes IV
* ciphertext bytes (PKCS7 padded encryption of the password plus a trailing NUL)
* 4 byte sentinel: \x00\x01\x02\x03
*/
final class SSSDPassword
{
private const METHOD_AES_256 = 0;
private const CIPHER = 'aes-256-cbc';
private const KEY_LEN = 32;
private const IV_LEN = 16;
private const SENTINEL = "\x00\x01\x02\x03";

public static function obfuscate(string $password): string
{
$key = random_bytes(self::KEY_LEN);
$iv = random_bytes(self::IV_LEN);

$ciphertext = openssl_encrypt($password."\x00",self::CIPHER,$key,OPENSSL_RAW_DATA,$iv);

return base64_encode(
pack('vv',self::METHOD_AES_256,strlen($ciphertext))
.$key
.$iv
.$ciphertext
.self::SENTINEL
);
}

public static function deobfuscate(string $encoded): string
{
$buffer = base64_decode($encoded,true);

if ($buffer === false || strlen($buffer) < 4)
throw new InvalidArgumentException('Invalid obfuscated password: not a valid base64 buffer.');

['method'=>$method,'ctsize'=>$ctsize] = unpack('vmethod/vctsize',substr($buffer,0,4));

if ($method !== self::METHOD_AES_256)
throw new InvalidArgumentException(sprintf('Invalid obfuscated password: unsupported method [%d].',$method));

if (strlen($buffer) !== 4+self::KEY_LEN+self::IV_LEN+$ctsize+strlen(self::SENTINEL))
throw new InvalidArgumentException('Invalid obfuscated password: unexpected buffer length.');

$p = 4;
$key = substr($buffer,$p,self::KEY_LEN); $p += self::KEY_LEN;
$iv = substr($buffer,$p,self::IV_LEN); $p += self::IV_LEN;
$ciphertext = substr($buffer,$p,$ctsize); $p += $ctsize;

if (substr($buffer,$p,strlen(self::SENTINEL)) !== self::SENTINEL)
throw new InvalidArgumentException('Invalid obfuscated password: sentinel mismatch, buffer may be corrupt.');

$plaintext = openssl_decrypt($ciphertext,self::CIPHER,$key,OPENSSL_RAW_DATA,$iv);

if ($plaintext === false)
throw new InvalidArgumentException('Invalid obfuscated password: unable to decrypt buffer.');

// The plaintext was encrypted with a trailing NUL terminator (C string).
$nul = strpos($plaintext,"\x00");

return $nul === false ? $plaintext : substr($plaintext,0,$nul);
}
}
29 changes: 26 additions & 3 deletions config/ldap.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
<?php

use App\Classes\LDAP\SSSDPassword;

/*
|--------------------------------------------------------------------------
| LDAP Bind DN Password
|--------------------------------------------------------------------------
|
| LDAP_PASSWORD_OBFUSCATION indicates the scheme (if any) used to obfuscate
| LDAP_PASSWORD, so that it doesnt need to be stored as clear text:
| - none (default): LDAP_PASSWORD is a clear text password.
| - sss: LDAP_PASSWORD is obfuscated using SSSD's sss_obfuscate(8) format
| (ldap_default_authtok_type=obfuscated_password).
|
*/

$ldap_password = env('LDAP_PASSWORD','');

$ldap_password = match (strtolower(env('LDAP_PASSWORD_OBFUSCATION','none'))) {
'none' => $ldap_password,
'sss' => $ldap_password !== '' ? SSSDPassword::deobfuscate($ldap_password) : $ldap_password,
default => throw new \InvalidArgumentException(sprintf('Unknown LDAP_PASSWORD_OBFUSCATION scheme [%s].',env('LDAP_PASSWORD_OBFUSCATION'))),
};

return [

/*
Expand Down Expand Up @@ -32,7 +55,7 @@
'name' => env('LDAP_NAME','LDAP Server'),
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', ''),
'password' => env('LDAP_PASSWORD', ''),
'password' => $ldap_password,
'port' => env('LDAP_PORT', 389),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
Expand All @@ -47,7 +70,7 @@
'name' => env('LDAP_NAME','LDAPS Server'),
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', ''),
'password' => env('LDAP_PASSWORD', ''),
'password' => $ldap_password,
'port' => env('LDAP_PORT', 636),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', true),
Expand All @@ -62,7 +85,7 @@
'name' => env('LDAP_NAME','LDAP-TLS Server'),
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', ''),
'password' => env('LDAP_PASSWORD', ''),
'password' => $ldap_password,
'port' => env('LDAP_PORT', 389),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
Expand Down
51 changes: 51 additions & 0 deletions tests/Unit/SSSDPasswordTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

use App\Classes\LDAP\SSSDPassword;

class SSSDPasswordTest extends TestCase
{
/**
* Fixture generated by the real sss_obfuscate(8) tool, confirming our format is byte
* compatible with SSSD's own implementation, not just a self consistent round trip.
*/
public function test_deobfuscate_matches_real_sss_obfuscate_output(): void
{
$token = 'AAAQABagVAjf9KgUyIxTw3A+HUfbig7N1+L0qtY4xAULt2GYHFc1B3CBWGAE9ArooklBkpxQtROiyCGDQH+VzLHYmiIAAQID';

$this->assertEquals('Passw0rd',SSSDPassword::deobfuscate($token));
}

public function test_obfuscate_then_deobfuscate_round_trips(): void
{
$password = 'S0m3 \'Weird\' P@ssw0rd!';

$this->assertEquals($password,SSSDPassword::deobfuscate(SSSDPassword::obfuscate($password)));
}

public function test_obfuscate_output_is_not_deterministic(): void
{
$password = 'Passw0rd';

$this->assertNotEquals(SSSDPassword::obfuscate($password),SSSDPassword::obfuscate($password));
}

public function test_deobfuscate_rejects_invalid_base64(): void
{
$this->expectException(\InvalidArgumentException::class);

SSSDPassword::deobfuscate('not-valid-base64!!!');
}

public function test_deobfuscate_rejects_corrupt_buffer(): void
{
$buffer = base64_decode(SSSDPassword::obfuscate('Passw0rd'));

$this->expectException(\InvalidArgumentException::class);

SSSDPassword::deobfuscate(base64_encode($buffer.'x'));
}
}