Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
PayloadSigner
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
4 / 4
8
100.00% covered (success)
100.00%
1 / 1
 setKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sign
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 verify
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Queue\Process;
16
17/**
18 * Payload signer class - HMAC-signs/verifies the serialized bytes every
19 * storage adapter writes, closing the unserialize() object-injection gap
20 * for anyone who can write to the underlying storage directly (a
21 * compromised Redis instance, SQL injection elsewhere in the host app, a
22 * writable queue directory). Static-only, mirroring
23 * Laravel\SerializableClosure::setSecretKey()'s own static-global shape -
24 * a familiar idiom already present in this project's dependency tree.
25 *
26 * When no key is configured (the default), sign()/verify() are exact
27 * pass-throughs - zero behavior change from every adapter's current,
28 * unsigned serialize()/unserialize() round-trip. Once setKey() is called
29 * (recommended once, at application bootstrap, before any queue/worker
30 * operation), sign() prepends a raw 32-byte HMAC-SHA256 and verify()
31 * checks it with hash_equals() before returning the payload bytes - a
32 * failed check means the caller must never pass those bytes to
33 * unserialize() at all.
34 *
35 * @category   Pop
36 * @package    Pop\Queue
37 * @author     Nick Sagona, III <nick@popphp.org>
38 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
39 * @license    https://www.popphp.org/license     New BSD License
40 * @version    3.0.0
41 */
42final class PayloadSigner
43{
44
45    /**
46     * Signing key. Null (the default) disables signing entirely - sign()
47     * and verify() both become pass-throughs.
48     * @var ?string
49     */
50    protected static ?string $key = null;
51
52    /**
53     * Set the signing key. Pass null to disable signing again.
54     *
55     * @param  ?string $key
56     * @return void
57     */
58    public static function setKey(?string $key): void
59    {
60        self::$key = $key;
61    }
62
63    /**
64     * Whether a signing key is currently configured
65     *
66     * @return bool
67     */
68    public static function hasKey(): bool
69    {
70        return (self::$key !== null);
71    }
72
73    /**
74     * Sign a payload. Pass-through when no key is configured.
75     *
76     * @param  string $payload
77     * @return string
78     */
79    public static function sign(string $payload): string
80    {
81        if (self::$key === null) {
82            return $payload;
83        }
84
85        return hash_hmac('sha256', $payload, self::$key, true) . $payload;
86    }
87
88    /**
89     * Verify a signed payload, returning the original payload bytes on
90     * success. Pass-through when no key is configured. Returns false on
91     * any verification failure (missing/truncated signature, tampered
92     * signature, tampered payload, or a payload signed with a different
93     * key) - callers must never unserialize() the input when this returns
94     * false.
95     *
96     * @param  string $signed
97     * @return string|false
98     */
99    public static function verify(string $signed): string|false
100    {
101        if (self::$key === null) {
102            return $signed;
103        }
104
105        if (strlen($signed) < 32) {
106            return false;
107        }
108
109        $mac  = substr($signed, 0, 32);
110        $body = substr($signed, 32);
111
112        return hash_equals(hash_hmac('sha256', $body, self::$key, true), $mac) ? $body : false;
113    }
114
115}