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