Issues (6)

src/HttpDigest.php (3 issues)

Labels
Severity
1
<?php declare(strict_types=1);
2
3
namespace Jasny\HttpDigest;
4
5
use Improved as i;
6
use const Improved\FUNCTION_ARGUMENT_PLACEHOLDER as __;
0 ignored issues
show
The constant Improved\FUNCTION_ARGUMENT_PLACEHOLDER was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
7
8
use Improved\IteratorPipeline\Pipeline;
9
use Jasny\HttpDigest\Negotiation\DigestNegotiator;
10
use Jasny\HttpDigest\Negotiation\WantDigest;
11
12
/**
13
 * Create and verify HTTP Digests.
14
 */
15
class HttpDigest
16
{
17
    protected const ALGOS = [
18
        'MD5' => 'md5',
19
        'SHA' => 'sha1',
20
        'SHA-256' => 'sha256',
21
        'SHA-512' => 'sha512',
22
    ];
23
24
    /**
25
     * @var DigestNegotiator
26
     */
27
    protected $negotiator;
28
29
    /**
30
     * Supported algorithms with priority (RFC 7231).
31
     * @var string[]
32
     */
33
    protected $priorities;
34
35
36
    /**
37
     * Class construction.
38
     *
39
     * @param string|string[]  $priorities  RFC 7231 strings
40
     * @param DigestNegotiator $negotiator
41
     */
42 15
    public function __construct($priorities, DigestNegotiator $negotiator = null)
43
    {
44 15
        $this->priorities = $this->getSupportedPriorities($priorities);
45 14
        $this->negotiator = $negotiator ?? new DigestNegotiator();
46 14
    }
47
48
    /**
49
     * Set the priorities.
50
     *
51
     * @param string|string[] $priorities  RFC 7231 strings
52
     * @return static
53
     */
54 3
    public function withPriorities($priorities)
55
    {
56 3
        $supportedPriorities = $this->getSupportedPriorities($priorities);
57
        ;
58
59 2
        if ($this->priorities === $supportedPriorities) {
60 2
            return $this;
61
        }
62
63 2
        $clone = clone $this;
64 2
        $clone->priorities = $supportedPriorities;
65
66 2
        return $clone;
67
    }
68
69
    /**
70
     * Get the priorities.
71
     *
72
     * @return string[]
73
     */
74 2
    public function getPriorities(): array
75
    {
76 2
        return $this->priorities;
77
    }
78
79
    /**
80
     * Get the priorities as `Want-Digest` header string.
81
     *
82
     * @return string
83
     */
84 2
    public function getWantDigest(): string
85
    {
86 2
        return join(', ', $this->priorities);
87
    }
88
89
    /**
90
     * Get the priorities for supported algorithms.
91
     *
92
     * @param string|string[] $priorities  RFC 7231 strings (content negotiation).
93
     * @return array
94
     */
95 15
    protected function getSupportedPriorities($priorities): array
96
    {
97 15
        i\type_check($priorities, ['string', 'array']);
98
99 15
        $supportedPriorities = Pipeline::with(is_string($priorities) ? explode(',', $priorities): $priorities)
100 15
            ->typeCheck('string')
101 15
            ->map(i\function_partial('trim', __))
0 ignored issues
show
The function function_partial was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

101
            ->map(/** @scrutinizer ignore-call */ i\function_partial('trim', __))
Loading history...
The constant __ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
102 15
            ->map(i\function_partial('explode', ';', __, 2))
103 15
            ->column(1, 0)
104
            ->mapKeys(function ($_, string $algo) {
105 15
                return strtoupper($algo);
106 15
            })
107
            ->filter(function ($_, string $algo) {
108 15
                return array_key_exists($algo, self::ALGOS);
109 15
            })
110 15
            ->sortKeys(SORT_STRING)
111
            ->map(function (?string $q, string $algo) {
112 14
                return $algo . ($q !== null && $q !== '' ? ';' . $q : '');
113 15
            })
114 15
            ->values()
115 15
            ->toArray();
116
117 15
        if (count($supportedPriorities) === 0) {
118 2
            throw new \InvalidArgumentException('None of the algorithms specified in the priorities are supported');
119
        }
120
121 14
        return $supportedPriorities;
122
    }
123
124
125
    /**
126
     * Create a digest from the body.
127
     *
128
     * @param string $body
129
     * @return string
130
     */
131 3
    public function create(string $body): string
132
    {
133
        /** @var WantDigest $best */
134 3
        $best = $this->negotiator->getBest('*', $this->priorities);
135 3
        $algo = strtoupper($best->getValue());
136
137 3
        $hash = hash(self::ALGOS[$algo], $body, true);
138
139 3
        return sprintf('%s=%s', $algo, base64_encode($hash));
140
    }
141
142
    /**
143
     * Validate a digest is correct for the body.
144
     *
145
     * @param string $body
146
     * @param string $digest
147
     * @throws HttpDigestException
148
     */
149 8
    public function verify(string $body, string $digest): void
150
    {
151 8
        [$algo, $hash64] = explode('=', $digest, 2) + [1 => ''];
152
153 8
        if ($this->negotiator->getBest($algo, $this->priorities) === null) {
154 1
            throw new HttpDigestException('Unsupported digest hashing algorithm: '. $algo);
155
        }
156
157 7
        $hash = base64_decode($hash64, true);
158
159 7
        if ($hash === false) {
160 1
            throw new HttpDigestException('Corrupt digest hash');
161
        }
162
163 6
        $expectedHash = hash(self::ALGOS[$algo], $body, true);
164
165 6
        if ($hash !== $expectedHash) {
166 3
            throw new HttpDigestException('Incorrect digest hash');
167
        }
168 3
    }
169
}
170