feat: ajout signature HMAC SHA-256 sur Devis, Advert et Facture

src/Entity/Devis.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "devis|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals (timing-safe)

src/Entity/Advert.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "advert|numOrder|createdAt" signe avec APP_SECRET
- verifyHmac(): verification par hash_equals

src/Entity/Facture.php:
- Nouveau champ hmac (string 128), genere dans le constructeur
- Constructeur prend maintenant le hmacSecret en 2eme parametre
- generateHmac(): payload = "facture|numOrder|splitIndex|createdAt"
  signe avec APP_SECRET (inclut splitIndex pour differencier les splits)
- verifyHmac(): verification par hash_equals

src/Service/DevisService.php, AdvertService.php, FactureService.php:
- Injection de APP_SECRET via #[Autowire('%env(APP_SECRET)%')]
- Passage du hmacSecret aux constructeurs des entites

migrations/Version20260402203207.php:
- Ajout colonne hmac VARCHAR(128) NOT NULL sur devis, advert, facture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-02 22:32:18 +02:00
parent 5b0e4707f7
commit cdd5c656a9
7 changed files with 124 additions and 23 deletions

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260402203207 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE advert ADD hmac VARCHAR(128) NOT NULL');
$this->addSql('ALTER TABLE devis ADD hmac VARCHAR(128) NOT NULL');
$this->addSql('ALTER TABLE facture ADD hmac VARCHAR(128) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE advert DROP hmac');
$this->addSql('ALTER TABLE devis DROP hmac');
$this->addSql('ALTER TABLE facture DROP hmac');
}
}

View File

@@ -23,6 +23,9 @@ class Advert
#[ORM\JoinColumn(nullable: true)]
private ?Devis $devis = null;
#[ORM\Column(length: 128)]
private string $hmac;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
@@ -30,11 +33,12 @@ class Advert
#[ORM\OneToMany(targetEntity: Facture::class, mappedBy: 'advert')]
private Collection $factures;
public function __construct(OrderNumber $orderNumber)
public function __construct(OrderNumber $orderNumber, string $hmacSecret)
{
$this->orderNumber = $orderNumber;
$this->createdAt = new \DateTimeImmutable();
$this->factures = new ArrayCollection();
$this->hmac = $this->generateHmac($hmacSecret);
}
public function getId(): ?int
@@ -57,6 +61,11 @@ class Advert
$this->devis = $devis;
}
public function getHmac(): string
{
return $this->hmac;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
@@ -67,4 +76,20 @@ class Advert
{
return $this->factures;
}
public function verifyHmac(string $hmacSecret): bool
{
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
}
private function generateHmac(string $secret): string
{
$payload = implode('|', [
'advert',
$this->orderNumber->getNumOrder(),
$this->createdAt->format('Y-m-d\TH:i:s'),
]);
return hash_hmac('sha256', $payload, $secret);
}
}

View File

@@ -19,6 +19,9 @@ class Devis
#[ORM\JoinColumn(nullable: false)]
private OrderNumber $orderNumber;
#[ORM\Column(length: 128)]
private string $hmac;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
@@ -26,11 +29,12 @@ class Devis
#[ORM\OneToMany(targetEntity: Advert::class, mappedBy: 'devis')]
private Collection $adverts;
public function __construct(OrderNumber $orderNumber)
public function __construct(OrderNumber $orderNumber, string $hmacSecret)
{
$this->orderNumber = $orderNumber;
$this->createdAt = new \DateTimeImmutable();
$this->adverts = new ArrayCollection();
$this->hmac = $this->generateHmac($hmacSecret);
}
public function getId(): ?int
@@ -43,6 +47,11 @@ class Devis
return $this->orderNumber;
}
public function getHmac(): string
{
return $this->hmac;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
@@ -53,4 +62,20 @@ class Devis
{
return $this->adverts;
}
public function verifyHmac(string $hmacSecret): bool
{
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
}
private function generateHmac(string $secret): string
{
$payload = implode('|', [
'devis',
$this->orderNumber->getNumOrder(),
$this->createdAt->format('Y-m-d\TH:i:s'),
]);
return hash_hmac('sha256', $payload, $secret);
}
}

View File

@@ -27,13 +27,17 @@ class Facture
#[ORM\Column(type: 'smallint', options: ['default' => 0])]
private int $splitIndex = 0;
#[ORM\Column(length: 128)]
private string $hmac;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
public function __construct(OrderNumber $orderNumber)
public function __construct(OrderNumber $orderNumber, string $hmacSecret)
{
$this->orderNumber = $orderNumber;
$this->createdAt = new \DateTimeImmutable();
$this->hmac = $this->generateHmac($hmacSecret);
}
public function getId(): ?int
@@ -77,8 +81,30 @@ class Facture
return $this->splitIndex > 0 ? $num.'-'.$this->splitIndex : $num;
}
public function getHmac(): string
{
return $this->hmac;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function verifyHmac(string $hmacSecret): bool
{
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
}
private function generateHmac(string $secret): string
{
$payload = implode('|', [
'facture',
$this->orderNumber->getNumOrder(),
(string) $this->splitIndex,
$this->createdAt->format('Y-m-d\TH:i:s'),
]);
return hash_hmac('sha256', $payload, $secret);
}
}

View File

@@ -5,25 +5,24 @@ namespace App\Service;
use App\Entity\Advert;
use App\Entity\Devis;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class AdvertService
{
public function __construct(
private OrderNumberService $orderNumberService,
private EntityManagerInterface $em,
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
) {
}
/**
* Cree un advert avec un nouveau numero de commande.
*/
public function create(?Devis $devis = null): Advert
{
$orderNumber = null !== $devis
? $devis->getOrderNumber()
: $this->orderNumberService->generateAndUse();
$advert = new Advert($orderNumber);
$advert = new Advert($orderNumber, $this->hmacSecret);
if (null !== $devis) {
$advert->setDevis($devis);
@@ -35,9 +34,6 @@ class AdvertService
return $advert;
}
/**
* Cree un advert a partir d'un devis existant (meme numero).
*/
public function createFromDevis(Devis $devis): Advert
{
return $this->create($devis);

View File

@@ -4,22 +4,21 @@ namespace App\Service;
use App\Entity\Devis;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class DevisService
{
public function __construct(
private OrderNumberService $orderNumberService,
private EntityManagerInterface $em,
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
) {
}
/**
* Cree un devis avec un nouveau numero de commande.
*/
public function create(): Devis
{
$orderNumber = $this->orderNumberService->generateAndUse();
$devis = new Devis($orderNumber);
$devis = new Devis($orderNumber, $this->hmacSecret);
$this->em->persist($devis);
$this->em->flush();

View File

@@ -5,18 +5,17 @@ namespace App\Service;
use App\Entity\Advert;
use App\Entity\Facture;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class FactureService
{
public function __construct(
private OrderNumberService $orderNumberService,
private EntityManagerInterface $em,
#[Autowire('%env(APP_SECRET)%')] private string $hmacSecret,
) {
}
/**
* Cree une facture avec un nouveau numero de commande (facture seule).
*/
public function create(?Advert $advert = null): Facture
{
if (null !== $advert) {
@@ -24,7 +23,7 @@ class FactureService
}
$orderNumber = $this->orderNumberService->generateAndUse();
$facture = new Facture($orderNumber);
$facture = new Facture($orderNumber, $this->hmacSecret);
$this->em->persist($facture);
$this->em->flush();
@@ -32,20 +31,16 @@ class FactureService
return $facture;
}
/**
* Cree une facture a partir d'un advert (meme numero + splitIndex si plusieurs).
*/
public function createFromAdvert(Advert $advert): Facture
{
$existingCount = $advert->getFactures()->count();
$facture = new Facture($advert->getOrderNumber());
$facture = new Facture($advert->getOrderNumber(), $this->hmacSecret);
$facture->setAdvert($advert);
if ($existingCount > 0) {
$facture->setSplitIndex($existingCount + 1);
// La premiere facture existante doit aussi avoir un splitIndex si elle n'en a pas
$firstFacture = $advert->getFactures()->first();
if (false !== $firstFacture && 0 === $firstFacture->getSplitIndex()) {
$firstFacture->setSplitIndex(1);