1<?php
2
3/**
4 * This file is part of the FreeDSx SASL package.
5 *
6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace FreeDSx\Sasl;
13
14use FreeDSx\Sasl\Exception\SaslException;
15use FreeDSx\Sasl\Mechanism\MechanismInterface;
16
17/**
18 * Given an array of mechanism names, choose the best one available.
19 *
20 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
21 */
22class MechanismSelector
23{
24    /**
25     * @var MechanismInterface[]
26     */
27    protected $mechanisms;
28
29    /**
30     * @param MechanismInterface[] $mechanisms
31     */
32    public function __construct(array $mechanisms)
33    {
34        $this->mechanisms = $mechanisms;
35    }
36
37    /**
38     * @throws SaslException
39     */
40    public function select(array $choices = [], array $options = []): MechanismInterface
41    {
42        $mechs = $this->getAvailableMechsFromChoices($choices, $options);
43
44        return $this->selectMech($mechs);
45    }
46
47    /**
48     * From RFC 4422:
49     *
50     *   Mechanism negotiation is protocol specific.
51     *
52     *   Commonly, a protocol will specify that the server advertises
53     *   supported and available mechanisms to the client via some facility
54     *   provided by the protocol, and the client will then select the "best"
55     *   mechanism from this list that it supports and finds suitable.
56     *
57     * So basically we are on our own in determining the best available mechanism from a list. This really seems like
58     * something that should have been included in the RFC. There is SSF, but that was never formalized into an RFC and
59     * some vendors have different ways of calculating it, making it somewhat less meaningful.
60     *
61     * @param MechanismInterface[] $available
62     * @return MechanismInterface
63     * @throws SaslException
64     */
65    protected function selectMech(array $available): MechanismInterface
66    {
67        # We sort the mechanisms by:
68        #  1. Key size first.
69        #  2. Privacy / encryption support.
70        #  3. Integrity / signing support.
71        #  4. Authentication support (anonymous should be at the bottom...)
72        #  5. Authentication that is not plain text.
73        usort($available, function (MechanismInterface $mechA, MechanismInterface $mechB) {
74            $strengthA = $mechA->securityStrength();
75            $strengthB = $mechB->securityStrength();
76
77            # We need to invert the boolean checks, expect for plain text (logic is already inverted).
78            return (int)!$strengthA->supportsPrivacy() <=> (int)!$strengthB->supportsPrivacy()
79                ?: (int)!$strengthA->supportsIntegrity() <=> (int)!$strengthB->supportsIntegrity()
80                ?: (int)$strengthA->maxKeySize() <=> (int)$strengthB->maxKeySize()
81                ?: (int)!$strengthA->supportsAuth() <=> (int)!$strengthB->supportsAuth()
82                ?: (int)$strengthA->isPlainTextAuth() <=> (int)$strengthB->isPlainTextAuth();
83        });
84        $first = array_shift($available);
85
86        if (!$first instanceof MechanismInterface) {
87            throw new SaslException('No supported SASL mechanisms could be found.');
88        }
89
90        return $first;
91    }
92
93    /**
94     * @param string[] $choices
95     * @param array $options
96     * @return MechanismInterface[]
97     * @throws SaslException
98     */
99    protected function getAvailableMechsFromChoices(array $choices, array $options): array
100    {
101        $available = $this->filterFromChoices($choices);
102        if (count($available) === 0) {
103            $this->throwException($choices);
104        }
105
106        $available = $this->filterOptions($available, $options);
107        if (count($available) === 0) {
108            $this->throwException($choices);
109        }
110
111        return $available;
112    }
113
114    /**
115     * @param string[] $choices
116     * @return MechanismInterface[]
117     */
118    protected function filterFromChoices(array $choices): array
119    {
120        if (count($choices) === 0) {
121            return $this->mechanisms;
122        }
123        $filtered = [];
124
125        foreach ($this->mechanisms as $choice) {
126            if (in_array($choice->getName(), $choices, true)) {
127                $filtered[] = $choice;
128            }
129        }
130
131        return $filtered;
132    }
133
134    /**
135     * @param MechanismInterface[] $available
136     * @param array $options
137     * @return MechanismInterface[]
138     */
139    protected function filterOptions(array $available, array $options): array
140    {
141        $useIntegrity = $options['use_integrity'] ?? false;
142        $usePrivacy = $options['use_privacy'] ?? false;
143
144        # Don't need to worry whether it supports integrity or privacy...
145        if ($usePrivacy === false && $useIntegrity === false) {
146            return $available;
147        }
148        $supportsInt = [];
149        $supportsPriv = [];
150
151        # Filter to those only those supporting integrity...
152        if ($useIntegrity === true) {
153            $supportsInt = array_filter($available, function (MechanismInterface $mech) use ($useIntegrity) {
154                return $mech->securityStrength()->supportsIntegrity() === $useIntegrity;
155            });
156        }
157        # Filter to those only supporting privacy...
158        if ($usePrivacy === true) {
159            $supportsPriv = array_filter($available, function (MechanismInterface $mech) use ($usePrivacy) {
160                return $mech->securityStrength()->supportsPrivacy() === $usePrivacy;
161            });
162        }
163
164        return array_unique(array_merge($supportsInt, $supportsPriv), SORT_REGULAR);
165    }
166
167    /**
168     * @throws SaslException
169     */
170    protected function throwException(array $choices = []): void
171    {
172        throw new SaslException(sprintf(
173            'No supported SASL mechanisms could be found from the provided choices: %s',
174            implode($choices, ', ')
175        ));
176    }
177}
178