diff --git a/migrations/Version20250605112000.php b/migrations/Version20250605112000.php new file mode 100644 index 0000000..906ba98 --- /dev/null +++ b/migrations/Version20250605112000.php @@ -0,0 +1,26 @@ +addSql("ALTER TABLE s3_presigned_urls ADD url VARCHAR(2048) NOT NULL"); + } + + public function down(Schema $schema): void + { + $this->addSql("ALTER TABLE s3_presigned_urls DROP COLUMN url"); + } +} diff --git a/src/Controller/ConsoleApiController.php b/src/Controller/ConsoleApiController.php index c4ae4ee..8890554 100644 --- a/src/Controller/ConsoleApiController.php +++ b/src/Controller/ConsoleApiController.php @@ -389,7 +389,8 @@ class ConsoleApiController extends AbstractController 'method' => $url->getMethod(), 'access_key' => $url->getAccessKey(), 'expires_at' => $url->getExpiresAt()->format('Y-m-d H:i:s'), - 'created_at' => $url->getCreatedAt()->format('Y-m-d H:i:s') + 'created_at' => $url->getCreatedAt()->format('Y-m-d H:i:s'), + 'url' => $url->getUrl() ]; }, $urls) ]); @@ -413,7 +414,7 @@ class ConsoleApiController extends AbstractController return new JsonResponse(['error' => 'Invalid access key'], 404); } - $url = $this->s3Service->generatePresignedGetUrl($bucketName, $objectKey, $credential, $expiresIn); + $url = $this->s3Service->generatePresignedUrl($bucketName, $objectKey, $credential, $method, $expiresIn); return new JsonResponse(['url' => $url], 201); } diff --git a/src/Entity/S3PresignedUrl.php b/src/Entity/S3PresignedUrl.php index 62dbe6c..07167ff 100644 --- a/src/Entity/S3PresignedUrl.php +++ b/src/Entity/S3PresignedUrl.php @@ -40,6 +40,9 @@ class S3PresignedUrl #[ORM\Column(type: 'datetime')] private \DateTime $createdAt; + #[ORM\Column(type: 'string', length: 2048)] + private string $url; + public function __construct() { $this->createdAt = new \DateTime(); @@ -59,6 +62,8 @@ class S3PresignedUrl public function getAccessKey(): string { return $this->accessKey; } public function setAccessKey(string $accessKey): self { $this->accessKey = $accessKey; return $this; } public function getCreatedAt(): \DateTime { return $this->createdAt; } + public function getUrl(): string { return $this->url; } + public function setUrl(string $url): self { $this->url = $url; return $this; } public function isExpired(): bool { diff --git a/src/Service/S3Service.php b/src/Service/S3Service.php index 5946ab0..2c55dc8 100644 --- a/src/Service/S3Service.php +++ b/src/Service/S3Service.php @@ -403,31 +403,54 @@ class S3Service } // AWS Signature V4 helpers - public function generatePresignedGetUrl(string $bucketName, string $objectKey, S3Credential $credential, int $expiresIn = 3600): string + public function generatePresignedUrl(string $bucketName, string $objectKey, S3Credential $credential, string $method = 'GET', int $expiresIn = 3600): string { $expiresAt = new \DateTime('+' . $expiresIn . ' seconds'); - - // Store in database for tracking - $presignedUrl = $this->createPresignedUrl($bucketName, $objectKey, 'GET', $expiresAt, $credential->getAccessKey()); - - // Generate the actual presigned URL (simplified - you'll need full AWS Signature V4 implementation) + + // Store entry in DB + $presignedUrl = $this->createPresignedUrl($bucketName, $objectKey, $method, $expiresAt, $credential->getAccessKey()); + + $amzDate = $presignedUrl->getCreatedAt()->format('Ymd\THis\Z'); + $shortDate = $presignedUrl->getCreatedAt()->format('Ymd'); + $scope = $shortDate . '/us-east-1/s3/aws4_request'; + $params = [ 'X-Amz-Algorithm' => 'AWS4-HMAC-SHA256', - 'X-Amz-Credential' => $credential->getAccessKey() . '/' . date('Ymd') . '/us-east-1/s3/aws4_request', - 'X-Amz-Date' => gmdate('Ymd\THis\Z'), + 'X-Amz-Credential' => $credential->getAccessKey() . '/' . $scope, + 'X-Amz-Date' => $amzDate, 'X-Amz-Expires' => $expiresIn, - 'X-Amz-SignedHeaders' => 'host', - 'X-Amz-Signature' => $this->calculateSignature($bucketName, $objectKey, $params, $credential->getSecretKey()), - 'hash' => $presignedUrl->getUrlHash() + 'X-Amz-SignedHeaders' => 'host' ]; - return '/' . $bucketName . '/' . $objectKey . '?' . http_build_query($params); + $canonicalQuery = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + $canonicalRequest = sprintf("%s\n/%s/%s\n%s\nhost:%s\n\nhost\nUNSIGNED-PAYLOAD", $method, $bucketName, $objectKey, $canonicalQuery, 'localhost'); + + $stringToSign = "AWS4-HMAC-SHA256\n" . $amzDate . "\n" . $scope . "\n" . hash('sha256', $canonicalRequest); + $signingKey = $this->deriveSigningKey($credential->getSecretKey(), $shortDate, 'us-east-1', 's3'); + $signature = hash_hmac('sha256', $stringToSign, $signingKey); + + $params['X-Amz-Signature'] = $signature; + $params['hash'] = $presignedUrl->getUrlHash(); + + $url = '/' . $bucketName . '/' . $objectKey . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + $presignedUrl->setUrl($url); + $this->entityManager->flush(); + + return $url; } - private function calculateSignature(string $bucketName, string $objectKey, array $params, string $secretKey): string + // Deprecated - kept for backward compatibility + public function generatePresignedGetUrl(string $bucketName, string $objectKey, S3Credential $credential, int $expiresIn = 3600): string { - // Simplified signature calculation - implement full AWS Signature V4 here - return hash_hmac('sha256', $bucketName . $objectKey . serialize($params), $secretKey); + return $this->generatePresignedUrl($bucketName, $objectKey, $credential, 'GET', $expiresIn); + } + + private function deriveSigningKey(string $secretKey, string $date, string $region, string $service) + { + $kDate = hash_hmac('sha256', $date, 'AWS4' . $secretKey, true); + $kRegion = hash_hmac('sha256', $region, $kDate, true); + $kService = hash_hmac('sha256', $service, $kRegion, true); + return hash_hmac('sha256', 'aws4_request', $kService, true); } /** diff --git a/templates/console/index.html.twig b/templates/console/index.html.twig index b3760d2..f510f49 100644 --- a/templates/console/index.html.twig +++ b/templates/console/index.html.twig @@ -577,6 +577,59 @@ + +
Create a presigned URL to share temporary access.
+| Bucket | +Object | +Method | +Expires | +URL | +
|---|---|---|---|---|
| + | + | + | + | + + | +