Skip to content

Commit

Permalink
Let dovecot api behave like current checkpasswd
Browse files Browse the repository at this point in the history
  • Loading branch information
y3n4 committed Oct 11, 2024
1 parent 31f9541 commit 49eeda4
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 16 deletions.
49 changes: 39 additions & 10 deletions src/Controller/DovecotController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Enum\Roles;
use App\Handler\MailCryptKeyHandler;
use App\Handler\UserAuthenticationHandler;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
Expand All @@ -18,14 +19,16 @@ class DovecotController extends AbstractController
const MESSAGE_SUCCESS = 'success';
const MESSAGE_AUTHENTICATION_FAILED = 'authentication failed';
const MESSAGE_USER_NOT_FOUND = 'user not found';
const MESSAGE_INTERNAL_ERROR = 'error retrieving public key';
const MESSAGE_KEY_GENERATION_ERROR = 'unable to create mailbox key';
const MESSAGE_KEY_DECRYPTION_ERROR = 'unable to decrypt mailbox key';


public function __construct(
private readonly MailCryptKeyHandler $mailCryptKeyHandler,
private readonly UserAuthenticationHandler $authHandler,
) {}

#[Route('/api/dovecot/userdb/{email}', name: 'api_dovecot_userdb', methods: ['GET'], stateless: true)]
#[Route('/api/dovecot/userdb/{email}', name: 'api_dovecot_userdb', methods: ['GET'], stateless: true, requirements: ['mailCrypt' => '[1-4]'])]
public function userdb(
#[MapEntity(mapping: ['email' => 'email'])] User $user,
): JsonResponse {
Expand All @@ -38,30 +41,56 @@ public function userdb(
'body' => [
'email' => $user->getEmail(),
'domain' => $user->getDomain()->getName(),
'quota' => $user->getQuota(),
'pubkey' => $user->getMailCryptPublicKey() ?? '',
'quota' => $user->getQuota() ?? '',
'publicKey' => $user->getMailCryptPublicKey() ?? '',
]
], 200);
}

#[Route('/api/dovecot/passdb/{email}', name: 'api_dovecot_passdb', methods: ['POST'], stateless: true)]
#[Route('/api/dovecot/{mailCrypt}/passdb/{email}', name: 'api_dovecot_passdb', methods: ['POST'], stateless: true, requirements: ['mailCrypt' => '[0-3]'])]
public function passdb(
int $mailCrypt,
#[MapEntity(mapping: ['email' => 'email'])] User $user,
#[MapRequestPayload] DovecotPassdbDto $request
#[MapRequestPayload] DovecotPassdbDto $request,
): JsonResponse {
if (null === $user || $user->isDeleted() || $user->hasRole(Roles::SPAM)) {
return $this->json(['message' => self::MESSAGE_USER_NOT_FOUND], 404);
}

if (null === $this->authHandler->authenticate($user, $request->getPassword())) {
if (null === $this->authHandler->authenticate($user, $request->getPassword())) {
return $this->json(['message' => self::MESSAGE_AUTHENTICATION_FAILED], 403);
}

// If authentication is successfull, optionally create mail_crypt key pair and recovery token
if (false === $user->hasMailCrypt() && null === $user->getMailCryptPublicKey()) {
$this->mailCryptKeyHandler->create($user, $request->getPassword());
if (
$mailCrypt >= 3 &&
false === $user->hasMailCrypt() &&
null === $user->getMailCryptPublicKey()
) {
try {
$this->mailCryptKeyHandler->create($user, $request->getPassword());
} catch (Exception $exception) {
return $this->json(['message' => self::MESSAGE_KEY_GENERATION_ERROR, 'error' => $exception->getMessage()], 500);
}
}

// If mailcrypt is enabled, derive private key
if ($mailCrypt >= 1 && $user->hasMailCrypt()) {
try {
$privateKey = $this->mailCryptKeyHandler->decrypt($user, $request->getPassword());
} catch (Exception $exception) {
return $this->json(['message' => self::MESSAGE_KEY_DECRYPTION_ERROR, 'error' => $exception->getMessage()], 500);
}
}
$request->erasePassword();

return $this->json([
'message' => self::MESSAGE_SUCCESS,
'body' => ['privateKey' => $privateKey ?? '']
], 200);

return $this->json(['message' => self::MESSAGE_SUCCESS], 200);
if ($privateKey) {
sodium_memzero($privateKey);
}
}
}
5 changes: 5 additions & 0 deletions src/Dto/DovecotPassdbDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ public function setPassword(string $password): void
{
$this->password = $password;
}

public function erasePassword(): void
{
sodium_memzero($this->password);
}
}
24 changes: 18 additions & 6 deletions tests/Controller/DovecotControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function testPassdbUser(): void
$client = static::createClient([], [
'HTTP_Authorization' => 'Bearer insecure',
]);
$client->request('POST', '/api/dovecot/passdb/[email protected]', ['password' => 'password']);
$client->request('POST', '/api/dovecot/1/passdb/[email protected]', ['password' => 'password']);

self::assertResponseStatusCodeSame(200);
}
Expand All @@ -31,7 +31,7 @@ public function testPassdbUserWrongPassword(): void
$client = static::createClient([], [
'HTTP_Authorization' => 'Bearer insecure',
]);
$client->request('POST', '/api/dovecot/passdb/[email protected]', ['password' => 'wrong']);
$client->request('POST', '/api/dovecot/1/passdb/[email protected]', ['password' => 'wrong']);

self::assertResponseStatusCodeSame(403);
}
Expand All @@ -41,7 +41,7 @@ public function testPassdbNonexistentUser(): void
$client = static::createClient([], [
'HTTP_Authorization' => 'Bearer insecure',
]);
$client->request('POST', '/api/dovecot/passdb/[email protected]');
$client->request('POST', '/api/dovecot/1/passdb/[email protected]', ['password' => 'password']);

self::assertResponseStatusCodeSame(404);
}
Expand All @@ -51,11 +51,23 @@ public function testPassdbSpamUser(): void
$client = static::createClient([], [
'HTTP_Authorization' => 'Bearer insecure',
]);
$client->request('POST', '/api/dovecot/[email protected]');
$client->request('POST', '/api/dovecot/1/passdb/[email protected]', ['password' => 'password']);

self::assertResponseStatusCodeSame(404);
}

public function testPassdbMailCrypt(): void
{
$client = static::createClient([], [
'HTTP_Authorization' => 'Bearer insecure',
]);
$client->request('POST', '/api/dovecot/3/passdb/[email protected]', ['password' => 'password']);

self::assertResponseStatusCodeSame(200);
$data = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
self::assertNotEquals($data['body']['privateKey'], '');
}

public function testUserdbUser(): void
{
$client = static::createClient([], [
Expand All @@ -69,7 +81,7 @@ public function testUserdbUser(): void
self::assertEquals($data['message'], 'success');
self::assertEquals($data['body']['email'], '[email protected]');
self::assertEquals($data['body']['domain'], 'example.org');
self::assertEquals($data['body']['pubkey'], '');
self::assertEquals($data['body']['publicKey'], '');
}

public function testUserdbMailcrypt(): void
Expand All @@ -83,7 +95,7 @@ public function testUserdbMailcrypt(): void

$data = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
self::assertEquals($data['message'], 'success');
self::assertNotEquals($data['body']['pubkey'], '');
self::assertNotEquals($data['body']['publicKey'], '');
}

public function testUserdbNonexistentUser(): void
Expand Down

0 comments on commit 49eeda4

Please sign in to comment.