├── .gitignore ├── LICENSE.txt ├── README.md ├── doc └── sakl_manual.docx ├── driver └── P25RadioSerialModem.inf └── src ├── SoftwareAuthKeyLoader.sln └── SoftwareAuthKeyLoader ├── Actions.cs ├── App.config ├── Kmm ├── AlgorithmId.cs ├── DeleteAuthenticationKeyCommand.cs ├── DeleteAuthenticationKeyResponse.cs ├── InventoryCommandListActiveSuId.cs ├── InventoryCommandListSuIdItems.cs ├── InventoryResponseListActiveSuId.cs ├── InventoryResponseListSuIdItems.cs ├── InventoryType.cs ├── KmmBody.cs ├── KmmFrame.cs ├── LoadAuthenticationKeyCommand.cs ├── LoadAuthenticationKeyResponse.cs ├── MessageId.cs ├── NegativeAcknowledgement.cs ├── ResponseKind.cs ├── Status.cs ├── SuId.cs └── SuIdStatus.cs ├── Mono └── Options.cs ├── Network.cs ├── Output.cs ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── Settings.cs └── SoftwareAuthKeyLoader.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 2 | 3 | # Windows thumbnail cache files 4 | Thumbs.db 5 | ehthumbs.db 6 | ehthumbs_vista.db 7 | 8 | # Dump file 9 | *.stackdump 10 | 11 | # Folder config file 12 | [Dd]esktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msix 21 | *.msm 22 | *.msp 23 | 24 | # Windows shortcuts 25 | *.lnk 26 | 27 | ## https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 28 | 29 | # User-specific files 30 | *.rsuser 31 | *.suo 32 | *.user 33 | *.userosscache 34 | *.sln.docstates 35 | 36 | # User-specific files (MonoDevelop/Xamarin Studio) 37 | *.userprefs 38 | 39 | # Build results 40 | [Dd]ebug/ 41 | [Dd]ebugPublic/ 42 | [Rr]elease/ 43 | [Rr]eleases/ 44 | x64/ 45 | x86/ 46 | [Aa][Rr][Mm]/ 47 | [Aa][Rr][Mm]64/ 48 | bld/ 49 | [Bb]in/ 50 | [Oo]bj/ 51 | [Ll]og/ 52 | 53 | # Visual Studio 2015/2017 cache/options directory 54 | .vs/ 55 | # Uncomment if you have tasks that create the project's static files in wwwroot 56 | #wwwroot/ 57 | 58 | # Visual Studio 2017 auto generated files 59 | Generated\ Files/ 60 | 61 | # MSTest test Results 62 | [Tt]est[Rr]esult*/ 63 | [Bb]uild[Ll]og.* 64 | 65 | # NUNIT 66 | *.VisualState.xml 67 | TestResult.xml 68 | 69 | # Build Results of an ATL Project 70 | [Dd]ebugPS/ 71 | [Rr]eleasePS/ 72 | dlldata.c 73 | 74 | # Benchmark Results 75 | BenchmarkDotNet.Artifacts/ 76 | 77 | # .NET Core 78 | project.lock.json 79 | project.fragment.lock.json 80 | artifacts/ 81 | 82 | # StyleCop 83 | StyleCopReport.xml 84 | 85 | # Files built by Visual Studio 86 | *_i.c 87 | *_p.c 88 | *_h.h 89 | *.ilk 90 | *.meta 91 | *.obj 92 | *.iobj 93 | *.pch 94 | *.pdb 95 | *.ipdb 96 | *.pgc 97 | *.pgd 98 | *.rsp 99 | *.sbr 100 | *.tlb 101 | *.tli 102 | *.tlh 103 | *.tmp 104 | *.tmp_proj 105 | *_wpftmp.csproj 106 | *.log 107 | *.vspscc 108 | *.vssscc 109 | .builds 110 | *.pidb 111 | *.svclog 112 | *.scc 113 | 114 | # Chutzpah Test files 115 | _Chutzpah* 116 | 117 | # Visual C++ cache files 118 | ipch/ 119 | *.aps 120 | *.ncb 121 | *.opendb 122 | *.opensdf 123 | *.sdf 124 | *.cachefile 125 | *.VC.db 126 | *.VC.VC.opendb 127 | 128 | # Visual Studio profiler 129 | *.psess 130 | *.vsp 131 | *.vspx 132 | *.sap 133 | 134 | # Visual Studio Trace Files 135 | *.e2e 136 | 137 | # TFS 2012 Local Workspace 138 | $tf/ 139 | 140 | # Guidance Automation Toolkit 141 | *.gpState 142 | 143 | # ReSharper is a .NET coding add-in 144 | _ReSharper*/ 145 | *.[Rr]e[Ss]harper 146 | *.DotSettings.user 147 | 148 | # JustCode is a .NET coding add-in 149 | .JustCode 150 | 151 | # TeamCity is a build add-in 152 | _TeamCity* 153 | 154 | # DotCover is a Code Coverage Tool 155 | *.dotCover 156 | 157 | # AxoCover is a Code Coverage Tool 158 | .axoCover/* 159 | !.axoCover/settings.json 160 | 161 | # Visual Studio code coverage results 162 | *.coverage 163 | *.coveragexml 164 | 165 | # NCrunch 166 | _NCrunch_* 167 | .*crunch*.local.xml 168 | nCrunchTemp_* 169 | 170 | # MightyMoose 171 | *.mm.* 172 | AutoTest.Net/ 173 | 174 | # Web workbench (sass) 175 | .sass-cache/ 176 | 177 | # Installshield output folder 178 | [Ee]xpress/ 179 | 180 | # DocProject is a documentation generator add-in 181 | DocProject/buildhelp/ 182 | DocProject/Help/*.HxT 183 | DocProject/Help/*.HxC 184 | DocProject/Help/*.hhc 185 | DocProject/Help/*.hhk 186 | DocProject/Help/*.hhp 187 | DocProject/Help/Html2 188 | DocProject/Help/html 189 | 190 | # Click-Once directory 191 | publish/ 192 | 193 | # Publish Web Output 194 | *.[Pp]ublish.xml 195 | *.azurePubxml 196 | # Note: Comment the next line if you want to checkin your web deploy settings, 197 | # but database connection strings (with potential passwords) will be unencrypted 198 | *.pubxml 199 | *.publishproj 200 | 201 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 202 | # checkin your Azure Web App publish settings, but sensitive information contained 203 | # in these scripts will be unencrypted 204 | PublishScripts/ 205 | 206 | # NuGet Packages 207 | *.nupkg 208 | # The packages folder can be ignored because of Package Restore 209 | **/[Pp]ackages/* 210 | # except build/, which is used as an MSBuild target. 211 | !**/[Pp]ackages/build/ 212 | # Uncomment if necessary however generally it will be regenerated when needed 213 | #!**/[Pp]ackages/repositories.config 214 | # NuGet v3's project.json files produces more ignorable files 215 | *.nuget.props 216 | *.nuget.targets 217 | 218 | # Microsoft Azure Build Output 219 | csx/ 220 | *.build.csdef 221 | 222 | # Microsoft Azure Emulator 223 | ecf/ 224 | rcf/ 225 | 226 | # Windows Store app package directories and files 227 | AppPackages/ 228 | BundleArtifacts/ 229 | Package.StoreAssociation.xml 230 | _pkginfo.txt 231 | *.appx 232 | 233 | # Visual Studio cache files 234 | # files ending in .cache can be ignored 235 | *.[Cc]ache 236 | # but keep track of directories ending in .cache 237 | !?*.[Cc]ache/ 238 | 239 | # Others 240 | ClientBin/ 241 | ~$* 242 | *~ 243 | *.dbmdl 244 | *.dbproj.schemaview 245 | *.jfm 246 | *.pfx 247 | *.publishsettings 248 | orleans.codegen.cs 249 | 250 | # Including strong name files can present a security risk 251 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 252 | #*.snk 253 | 254 | # Since there are multiple workflows, uncomment next line to ignore bower_components 255 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 256 | #bower_components/ 257 | # ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true 258 | **/wwwroot/lib/ 259 | 260 | # RIA/Silverlight projects 261 | Generated_Code/ 262 | 263 | # Backup & report files from converting an old project file 264 | # to a newer Visual Studio version. Backup files are not needed, 265 | # because we have git ;-) 266 | _UpgradeReport_Files/ 267 | Backup*/ 268 | UpgradeLog*.XML 269 | UpgradeLog*.htm 270 | ServiceFabricBackup/ 271 | *.rptproj.bak 272 | 273 | # SQL Server files 274 | *.mdf 275 | *.ldf 276 | *.ndf 277 | 278 | # Business Intelligence projects 279 | *.rdl.data 280 | *.bim.layout 281 | *.bim_*.settings 282 | *.rptproj.rsuser 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio LightSwitch build output 304 | **/*.HTMLClient/GeneratedArtifacts 305 | **/*.DesktopClient/GeneratedArtifacts 306 | **/*.DesktopClient/ModelManifest.xml 307 | **/*.Server/GeneratedArtifacts 308 | **/*.Server/ModelManifest.xml 309 | _Pvt_Extensions 310 | 311 | # Paket dependency manager 312 | .paket/paket.exe 313 | paket-files/ 314 | 315 | # FAKE - F# Make 316 | .fake/ 317 | 318 | # JetBrains Rider 319 | .idea/ 320 | *.sln.iml 321 | 322 | # CodeRush personal settings 323 | .cr/personal 324 | 325 | # Python Tools for Visual Studio (PTVS) 326 | __pycache__/ 327 | *.pyc 328 | 329 | # Cake - Uncomment if you are using it 330 | # tools/** 331 | # !tools/packages.config 332 | 333 | # Tabs Studio 334 | *.tss 335 | 336 | # Telerik's JustMock configuration file 337 | *.jmconfig 338 | 339 | # BizTalk build output 340 | *.btp.cs 341 | *.btm.cs 342 | *.odx.cs 343 | *.xsd.cs 344 | 345 | # OpenCover UI analysis results 346 | OpenCover/ 347 | 348 | # Azure Stream Analytics local run output 349 | ASALocalRun/ 350 | 351 | # MSBuild Binary and Structured Log 352 | *.binlog 353 | 354 | # NVidia Nsight GPU debugger configuration file 355 | *.nvuser 356 | 357 | # MFractors (Xamarin productivity tool) working folder 358 | .mfractor/ 359 | 360 | # Local History for Visual Studio 361 | .localhistory/ 362 | 363 | # BeatPulse healthcheck temp database 364 | healthchecksdb 365 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Daniel Dugger 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoftwareAuthKeyLoader 2 | 3 | Software P25 Link Layer Authentication Key Loader (Windows .NET Console Application) 4 | 5 | Supports all Manual Rekeying Features for Authentication per TIA-102.AACD-A 6 | 7 | Download: [latest release](https://github.com/duggerd/SoftwareAuthKeyLoader/releases) 8 | 9 | Disclaimer 10 | ---------- 11 | 12 | This utility is not intended for use in a production setting – security was not a consideration of the design. If you are operating a production system, use a commercial keyloader that supports P25 link layer authentication. 13 | 14 | The subscriber identity and the link layer authentication key loaded into to the radio need to be entered into the trunking system in order for the authentication process to be successful. This tool by itself will not allow access to a link layer authentication protected trunking system. 15 | 16 | Documentation 17 | ------------- 18 | 19 | See `doc\sakl_manual.docx` (source) or `sakl_manual.pdf` (release) 20 | 21 | Usage 22 | ----- 23 | 24 | ``` 25 | Usage: sakl.exe [OPTIONS] 26 | 27 | Options: 28 | -h, -?, --help show this message and exit 29 | -q, --quiet do not show output 30 | -v, --verbose show debug messages 31 | -i, --ip=VALUE radio ip address [default 192.168.128.1] 32 | -p, --port=VALUE radio udp port number [default 49165] 33 | -t, --timeout=VALUE radio receive timeout (ms) [default 5000] 34 | -l, --load load key 35 | -z, --zeroize zeroize key(s) 36 | -r, --read read key(s) 37 | -d, --device device scope 38 | -a, --active active scope 39 | -n, --named named scope 40 | -w, --wacn=VALUE wacn id (hex) 41 | -s, --system=VALUE system id (hex) 42 | -u, --unit=VALUE unit id (hex) 43 | -k, --key=VALUE aes-128 encryption key (hex) 44 | 45 | Examples: 46 | load key to the active suid 47 | /load /active /key 000102030405060708090a0b0c0d0e0f 48 | 49 | load key to the specified suid 50 | /load /named /wacn a4398 /system f10 /unit 99b584 /key 000102030405060708090a0b0c0d0e0f 51 | 52 | zeroize all keys 53 | /zeroize /device 54 | 55 | zeroize active key 56 | /zeroize /active 57 | 58 | zeroize specified key 59 | /zeroize /named /wacn a4398 /system f10 /unit 99b584 60 | 61 | read all keys 62 | /read /device 63 | 64 | read active key 65 | /read /active 66 | ``` 67 | 68 | License 69 | ------- 70 | 71 | SoftwareAuthKeyLoader is distributed under the MIT License. 72 | 73 | Included open-source libraries: 74 | 75 | * Mono.Options: MIT License 76 | 77 | TODO 78 | ---- 79 | 80 | * Unit tests 81 | * Testing with SUs other than Motorola APX and ASTRO25 (XTS/XTL) 82 | -------------------------------------------------------------------------------- /doc/sakl_manual.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duggerd/SoftwareAuthKeyLoader/55c445dfab7ea30c91905b0e820119749b8bc2c6/doc/sakl_manual.docx -------------------------------------------------------------------------------- /driver/P25RadioSerialModem.inf: -------------------------------------------------------------------------------- 1 | ; Generic Project 25 Radio Serial Modem Setup File 2 | ; Copyright 2019 Daniel Dugger 3 | 4 | ; Changelog: 5 | ; 1.0.0.0 (2019-01-17) - Initial Release 6 | 7 | [Version] 8 | Signature="$WINDOWS NT$" 9 | Class=Modem 10 | ClassGUID={4D36E96D-E325-11CE-BFC1-08002BE10318} 11 | Provider=%Project25% 12 | DriverVer=01/17/2019,1.0.0.0 13 | 14 | [Manufacturer] 15 | %Project25% = Project25,NTx86,NTamd64 16 | 17 | [Project25.NTx86] 18 | %Modem1% = Modem1, PNPC031 19 | 20 | [Project25.NTamd64] 21 | %Modem1% = Modem1, PNPC031 22 | 23 | [Modem1] 24 | AddReg=All, MfgAddReg, Modem1.AddReg, SERIAL 25 | 26 | [All] 27 | HKR,,FriendlyDriver,,Unimodem.vxd 28 | HKR,,DevLoader,,*VCOMM 29 | HKR,,PortSubClass,1,02 30 | HKR,,ConfigDialog,,modemui.dll 31 | HKR,,EnumPropPages,,"modemui.dll,EnumPropPages" 32 | 33 | [MfgAddReg] 34 | HKR, Init, 1,, "+++" 35 | HKR, Init, 2,, "NoResponse" 36 | HKR, Monitor, 1,, "None" 37 | HKR, Answer, 1,, "" 38 | HKR, Answer, 2,, "NoResponse" 39 | HKR, Settings, DialPrefix,, "CLIENT" 40 | HKR, Responses, "CLIENTSERVER", 1, 02, 00, 00, 00, 00, 00, 00,00,00,00 41 | 42 | [Modem1.AddReg] 43 | HKR,, Properties, 1, 00,00,00,00, 00,00,00,00, 00,00,00,00, 00,00,00,00, 00,00,00,00, 00,00,00,00, 00,C2,01,00, 00,C2,01,00 44 | HKR,, DCB, 1, 1C,00,00,00, 80,25,00,00, 15,20,00,00, 00,00, 0a,00, 0a,00, 08, 00, 00, 11, 13, 00, 00, 00 ; 9600-8-N-1 45 | ;HKR,, DCB, 1, 1C,00,00,00, 00,4B,00,00, 15,20,00,00, 00,00, 0a,00, 0a,00, 08, 00, 00, 11, 13, 00, 00, 00 ; 19200-8-N-1 46 | ;HKR,, DCB, 1, 1C,00,00,00, 00,E1,00,00, 15,20,00,00, 00,00, 0a,00, 0a,00, 08, 00, 00, 11, 13, 00, 00, 00 ; 57600-8-N-1 47 | ;HKR,, DCB, 1, 1C,00,00,00, 00,C2,01,00, 15,20,00,00, 00,00, 0a,00, 0a,00, 08, 00, 00, 11, 13, 00, 00, 00 ; 115200-8-N-1 48 | 49 | [SERIAL] 50 | HKR,, DeviceType, 1, 00 51 | 52 | [Strings] 53 | Project25 = "Project 25" 54 | Modem1 = "P25 Radio Serial Modem" 55 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.106 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoftwareAuthKeyLoader", "SoftwareAuthKeyLoader\SoftwareAuthKeyLoader.csproj", "{842D4E38-B6E6-4CE8-8B99-0AD5A65596C1}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {842D4E38-B6E6-4CE8-8B99-0AD5A65596C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {842D4E38-B6E6-4CE8-8B99-0AD5A65596C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {842D4E38-B6E6-4CE8-8B99-0AD5A65596C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {842D4E38-B6E6-4CE8-8B99-0AD5A65596C1}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {5390F311-8C78-4136-80A7-B88DB5C0E2BD} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Actions.cs: -------------------------------------------------------------------------------- 1 | using SoftwareAuthKeyLoader.Kmm; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace SoftwareAuthKeyLoader 11 | { 12 | internal static class Actions 13 | { 14 | public static int LoadAuthenticationKey(bool targetSpecificSuId, int wacnId, int systemId, int unitId, byte[] key) 15 | { 16 | Output.DebugLine("LoadAuthenticationKey() :: targetSpecificSuId: {0}, wacnId: 0x{1:X}, systemId: 0x{2:X}, unitId: 0x{3:X}, key (hex): {4}", targetSpecificSuId, wacnId, systemId, unitId, BitConverter.ToString(key)); 17 | 18 | SuId commandSuId = new SuId(wacnId, systemId, unitId); 19 | Output.DebugLine("command suid - {0}", commandSuId.ToString()); 20 | 21 | LoadAuthenticationKeyCommand commandKmmBody = new LoadAuthenticationKeyCommand(targetSpecificSuId, commandSuId, key); 22 | Output.DebugLine("command kmm body - {0}", commandKmmBody.ToString()); 23 | 24 | KmmFrame commandKmmFrame = new KmmFrame(commandKmmBody); 25 | Output.DebugLine("command kmm frame - {0}", commandKmmFrame.ToString()); 26 | byte[] toRadio = commandKmmFrame.ToBytes(); 27 | 28 | byte[] fromRadio; 29 | 30 | try 31 | { 32 | fromRadio = Network.QueryRadio(toRadio); 33 | } 34 | catch (Exception ex) 35 | { 36 | Output.ErrorLine("unable to connect to radio: {0}", ex.Message); 37 | return -1; 38 | } 39 | 40 | KmmFrame responseKmmFrame = new KmmFrame(fromRadio); 41 | Output.DebugLine("response kmm frame - {0}", responseKmmFrame.ToString()); 42 | 43 | KmmBody responseKmmBody = responseKmmFrame.KmmBody; 44 | 45 | if (responseKmmBody is LoadAuthenticationKeyResponse) 46 | { 47 | Output.DebugLine("received LoadAuthenticationKeyResponse kmm"); 48 | LoadAuthenticationKeyResponse loadAuthenticationKeyResponse = responseKmmBody as LoadAuthenticationKeyResponse; 49 | Output.DebugLine("response kmm body - {0}", loadAuthenticationKeyResponse.ToString()); 50 | Output.DebugLine("response suid - {0}", loadAuthenticationKeyResponse.SuId.ToString()); 51 | 52 | if (loadAuthenticationKeyResponse.AssignmentSuccess == true && loadAuthenticationKeyResponse.Status == Status.CommandWasPerformed) 53 | { 54 | return 0; 55 | } 56 | else 57 | { 58 | Output.ErrorLine("abnormal response - assignment success: {0}, status: {1} (0x{2:X2})", loadAuthenticationKeyResponse.AssignmentSuccess, loadAuthenticationKeyResponse.Status.ToString(), (byte)loadAuthenticationKeyResponse.Status); 59 | return -1; 60 | } 61 | } 62 | else if (responseKmmBody is NegativeAcknowledgement) 63 | { 64 | Output.ErrorLine("received NegativeAcknowledgement kmm"); 65 | NegativeAcknowledgement negativeAcknowledgement = responseKmmBody as NegativeAcknowledgement; 66 | Output.DebugLine("response kmm body - {0}", negativeAcknowledgement.ToString()); 67 | return -1; 68 | } 69 | else 70 | { 71 | Output.ErrorLine("received unexpected kmm"); 72 | return -1; 73 | } 74 | } 75 | 76 | public static int DeleteAuthenticationKey(bool targetSpecificSuId, bool deleteAllKeys, int wacnId, int systemId, int unitId) 77 | { 78 | Output.DebugLine("DeleteAuthenticationKey() :: targetSpecificSuId: {0}, deleteAllKeys: {1}, wacnId: 0x{2:X}, systemId: 0x{3:X}, unitId: 0x{4:X}", targetSpecificSuId, deleteAllKeys, wacnId, systemId, unitId); 79 | 80 | SuId commandSuId = new SuId(wacnId, systemId, unitId); 81 | Output.DebugLine("command suid - {0}", commandSuId.ToString()); 82 | 83 | DeleteAuthenticationKeyCommand commandKmmBody = new DeleteAuthenticationKeyCommand(targetSpecificSuId, deleteAllKeys, commandSuId); 84 | Output.DebugLine("command kmm body - {0}", commandKmmBody.ToString()); 85 | 86 | KmmFrame commandKmmFrame = new KmmFrame(commandKmmBody); 87 | Output.DebugLine("command kmm frame - {0}", commandKmmFrame.ToString()); 88 | byte[] toRadio = commandKmmFrame.ToBytes(); 89 | 90 | byte[] fromRadio; 91 | 92 | try 93 | { 94 | fromRadio = Network.QueryRadio(toRadio); 95 | } 96 | catch (Exception ex) 97 | { 98 | Output.ErrorLine("unable to connect to radio: {0}", ex.Message); 99 | return -1; 100 | } 101 | 102 | KmmFrame responseKmmFrame = new KmmFrame(fromRadio); 103 | Output.DebugLine("response kmm frame - {0}", responseKmmFrame.ToString()); 104 | 105 | KmmBody responseKmmBody = responseKmmFrame.KmmBody; 106 | 107 | if (responseKmmBody is DeleteAuthenticationKeyResponse) 108 | { 109 | Output.DebugLine("received DeleteAuthenticationKeyResponse kmm"); 110 | DeleteAuthenticationKeyResponse deleteAuthenticationKeyResponse = responseKmmBody as DeleteAuthenticationKeyResponse; 111 | Output.DebugLine("response kmm body - {0}", deleteAuthenticationKeyResponse.ToString()); 112 | Output.DebugLine("response suid - {0}", deleteAuthenticationKeyResponse.SuId.ToString()); 113 | 114 | if (deleteAuthenticationKeyResponse.Status == Status.CommandWasPerformed) 115 | { 116 | return 0; 117 | } 118 | else 119 | { 120 | Output.ErrorLine("abnormal response - status: {0} (0x{1:X2})", deleteAuthenticationKeyResponse.Status.ToString(), (byte)deleteAuthenticationKeyResponse.Status); 121 | return -1; 122 | } 123 | } 124 | else if (responseKmmBody is NegativeAcknowledgement) 125 | { 126 | Output.ErrorLine("received NegativeAcknowledgement kmm"); 127 | NegativeAcknowledgement negativeAcknowledgement = responseKmmBody as NegativeAcknowledgement; 128 | Output.DebugLine("response kmm body - {0}", negativeAcknowledgement.ToString()); 129 | return -1; 130 | } 131 | else 132 | { 133 | Output.ErrorLine("received unexpected kmm"); 134 | return -1; 135 | } 136 | } 137 | 138 | public static int ListActiveSuId() 139 | { 140 | Output.DebugLine("ListActiveSuId()"); 141 | 142 | InventoryCommandListActiveSuId commandKmmBody = new InventoryCommandListActiveSuId(); 143 | Output.DebugLine("command kmm body - {0}", commandKmmBody.ToString()); 144 | 145 | KmmFrame commandKmmFrame = new KmmFrame(commandKmmBody); 146 | Output.DebugLine("command kmm frame - {0}", commandKmmFrame.ToString()); 147 | byte[] toRadio = commandKmmFrame.ToBytes(); 148 | 149 | byte[] fromRadio; 150 | 151 | try 152 | { 153 | fromRadio = Network.QueryRadio(toRadio); 154 | } 155 | catch (Exception ex) 156 | { 157 | Output.ErrorLine("unable to connect to radio: {0}", ex.Message); 158 | return -1; 159 | } 160 | 161 | KmmFrame responseKmmFrame = new KmmFrame(fromRadio); 162 | Output.DebugLine("response kmm frame - {0}", responseKmmFrame.ToString()); 163 | 164 | KmmBody responseKmmBody = responseKmmFrame.KmmBody; 165 | 166 | if (responseKmmBody is InventoryResponseListActiveSuId) 167 | { 168 | Output.DebugLine("received InventoryResponseListActiveSuId kmm"); 169 | InventoryResponseListActiveSuId inventoryResponseListActiveSuId = responseKmmBody as InventoryResponseListActiveSuId; 170 | Output.DebugLine("response kmm body - {0}", inventoryResponseListActiveSuId.ToString()); 171 | Output.DebugLine("response suid - {0}", inventoryResponseListActiveSuId.SuId.ToString()); 172 | 173 | if (inventoryResponseListActiveSuId.Status == Status.CommandWasPerformed) 174 | { 175 | Output.InfoLine("WACN: 0x{0:X}, System: 0x{1:X}, Unit: 0x{2:X}, Key Assigned: {3}, Is Active: {4}", inventoryResponseListActiveSuId.SuId.WacnId, inventoryResponseListActiveSuId.SuId.SystemId, inventoryResponseListActiveSuId.SuId.UnitId, inventoryResponseListActiveSuId.KeyAssigned, inventoryResponseListActiveSuId.ActiveSuId); 176 | return 0; 177 | } 178 | else 179 | { 180 | Output.ErrorLine("abnormal response - status: {0} (0x{1:X2})", inventoryResponseListActiveSuId.Status.ToString(), (byte)inventoryResponseListActiveSuId.Status); 181 | return -1; 182 | } 183 | } 184 | else if (responseKmmBody is NegativeAcknowledgement) 185 | { 186 | Output.ErrorLine("received NegativeAcknowledgement kmm"); 187 | NegativeAcknowledgement negativeAcknowledgement = responseKmmBody as NegativeAcknowledgement; 188 | Output.DebugLine("response kmm body - {0}", negativeAcknowledgement.ToString()); 189 | return -1; 190 | } 191 | else 192 | { 193 | Output.ErrorLine("received unexpected kmm"); 194 | return -1; 195 | } 196 | } 197 | 198 | public static int ListSuIdItems() 199 | { 200 | Output.DebugLine("ListSuIdItems()"); 201 | 202 | bool needsAnotherRun = true; 203 | int inventoryMarker = 0; 204 | 205 | while (needsAnotherRun) 206 | { 207 | InventoryCommandListSuIdItems commandKmmBody = new InventoryCommandListSuIdItems(inventoryMarker, 59); 208 | Output.DebugLine("command kmm body - {0}", commandKmmBody.ToString()); 209 | 210 | KmmFrame commandKmmFrame = new KmmFrame(commandKmmBody); 211 | Output.DebugLine("command kmm frame - {0}", commandKmmFrame.ToString()); 212 | byte[] toRadio = commandKmmFrame.ToBytes(); 213 | 214 | byte[] fromRadio; 215 | 216 | try 217 | { 218 | fromRadio = Network.QueryRadio(toRadio); 219 | } 220 | catch (Exception ex) 221 | { 222 | Output.ErrorLine("unable to connect to radio: {0}", ex.Message); 223 | return -1; 224 | } 225 | 226 | KmmFrame responseKmmFrame = new KmmFrame(fromRadio); 227 | Output.DebugLine("response kmm frame - {0}", responseKmmFrame.ToString()); 228 | 229 | KmmBody responseKmmBody = responseKmmFrame.KmmBody; 230 | 231 | if (responseKmmBody is InventoryResponseListSuIdItems) 232 | { 233 | Output.DebugLine("received InventoryResponseListSuIdItems kmm"); 234 | InventoryResponseListSuIdItems inventoryResponseListSuIdItems = responseKmmBody as InventoryResponseListSuIdItems; 235 | Output.DebugLine("response kmm body - {0}", inventoryResponseListSuIdItems.ToString()); 236 | 237 | inventoryMarker = inventoryResponseListSuIdItems.InventoryMarker; 238 | Output.DebugLine("inventory marker - {0}", inventoryMarker); 239 | 240 | if (inventoryMarker > 0) 241 | { 242 | needsAnotherRun = true; 243 | } 244 | else 245 | { 246 | needsAnotherRun = false; 247 | } 248 | 249 | foreach (SuIdStatus responseSuIdStatus in inventoryResponseListSuIdItems.SuIdStatuses) 250 | { 251 | Output.InfoLine("WACN: 0x{0:X}, System: 0x{1:X}, Unit: 0x{2:X}, Key Assigned: {3}, Is Active: {4}", responseSuIdStatus.SuId.WacnId, responseSuIdStatus.SuId.SystemId, responseSuIdStatus.SuId.UnitId, responseSuIdStatus.KeyAssigned, responseSuIdStatus.ActiveSuId); 252 | } 253 | } 254 | else if (responseKmmBody is NegativeAcknowledgement) 255 | { 256 | Output.ErrorLine("received NegativeAcknowledgement kmm"); 257 | NegativeAcknowledgement negativeAcknowledgement = responseKmmBody as NegativeAcknowledgement; 258 | Output.DebugLine("response kmm body - {0}", negativeAcknowledgement.ToString()); 259 | return -1; 260 | } 261 | else 262 | { 263 | Output.ErrorLine("received unexpected kmm"); 264 | return -1; 265 | } 266 | } 267 | 268 | return 0; 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/AlgorithmId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public enum AlgorithmId : byte 10 | { 11 | Clear = 0x80, 12 | AES128 = 0x85 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/DeleteAuthenticationKeyCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SoftwareAuthKeyLoader.Kmm 9 | { 10 | public class DeleteAuthenticationKeyCommand : KmmBody 11 | { 12 | public bool TargetSpecificSuId { get; private set; } 13 | 14 | public bool DeleteAllKeys { get; private set; } 15 | 16 | public SuId SuId { get; private set; } 17 | 18 | public override MessageId MessageId 19 | { 20 | get 21 | { 22 | return MessageId.DeleteAuthenticationKeyCommand; 23 | } 24 | } 25 | 26 | public override ResponseKind ResponseKind 27 | { 28 | get 29 | { 30 | return ResponseKind.Immediate; 31 | } 32 | } 33 | 34 | public DeleteAuthenticationKeyCommand(bool targetSpecificSuId, bool deleteAllKeys, SuId suId) 35 | { 36 | if (suId == null) 37 | { 38 | throw new ArgumentNullException("suId"); 39 | } 40 | 41 | TargetSpecificSuId = targetSpecificSuId; 42 | DeleteAllKeys = deleteAllKeys; 43 | SuId = suId; 44 | } 45 | 46 | public override byte[] ToBytes() 47 | { 48 | byte[] contents = new byte[8]; 49 | 50 | /* authentication instruction */ 51 | BitArray authenticationInstruction = new BitArray(8, false); 52 | authenticationInstruction.Set(0, TargetSpecificSuId); 53 | authenticationInstruction.Set(1, DeleteAllKeys); 54 | authenticationInstruction.CopyTo(contents, 0); 55 | 56 | /* suid */ 57 | byte[] suId = SuId.ToBytes(); 58 | Array.Copy(suId, 0, contents, 1, suId.Length); 59 | 60 | return contents; 61 | } 62 | 63 | protected override void Parse(byte[] contents) 64 | { 65 | throw new NotImplementedException(); 66 | } 67 | 68 | public override string ToString() 69 | { 70 | return string.Format("[TargetSpecificSuId: {0}, DeleteAllKeys: {1}, SuId: {2}]", TargetSpecificSuId, DeleteAllKeys, SuId.ToString()); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/DeleteAuthenticationKeyResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class DeleteAuthenticationKeyResponse : KmmBody 10 | { 11 | public Status Status { get; private set; } 12 | 13 | public int NumKeysDeleted { get; private set; } 14 | 15 | public SuId SuId { get; private set; } 16 | 17 | public override MessageId MessageId 18 | { 19 | get 20 | { 21 | return MessageId.DeleteAuthenticationKeyResponse; 22 | } 23 | } 24 | 25 | public override ResponseKind ResponseKind 26 | { 27 | get 28 | { 29 | return ResponseKind.None; 30 | } 31 | } 32 | 33 | public DeleteAuthenticationKeyResponse(byte[] contents) 34 | { 35 | Parse(contents); 36 | } 37 | 38 | public override byte[] ToBytes() 39 | { 40 | throw new NotImplementedException(); 41 | } 42 | 43 | protected override void Parse(byte[] contents) 44 | { 45 | if (contents.Length != 10) 46 | { 47 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected 10, got {0} - {1}", contents.Length.ToString(), BitConverter.ToString(contents))); 48 | } 49 | 50 | /* suid */ 51 | byte[] suId = new byte[7]; 52 | Array.Copy(contents, 0, suId, 0, 7); 53 | SuId = new SuId(suId); 54 | 55 | /* number of keys deleted */ 56 | NumKeysDeleted |= (contents[7] & 0xFF) << 8; 57 | NumKeysDeleted |= contents[8] & 0xFF; 58 | 59 | /* status */ 60 | Status = (Status)contents[9]; 61 | } 62 | 63 | public override string ToString() 64 | { 65 | return string.Format("[SuId: {0}, NumKeysDeleted: {1}, Status: {2} (0x{3:X2})]", SuId.ToString(), NumKeysDeleted, Status.ToString(), (byte)Status); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/InventoryCommandListActiveSuId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SoftwareAuthKeyLoader.Kmm 9 | { 10 | public class InventoryCommandListActiveSuId : KmmBody 11 | { 12 | public override MessageId MessageId 13 | { 14 | get 15 | { 16 | return MessageId.InventoryCommand; 17 | } 18 | } 19 | 20 | public InventoryType InventoryType 21 | { 22 | get 23 | { 24 | return InventoryType.ListActiveSuId; 25 | } 26 | } 27 | 28 | public override ResponseKind ResponseKind 29 | { 30 | get 31 | { 32 | return ResponseKind.Immediate; 33 | } 34 | } 35 | 36 | public InventoryCommandListActiveSuId() 37 | { 38 | } 39 | 40 | public override byte[] ToBytes() 41 | { 42 | byte[] contents = new byte[1]; 43 | 44 | /* inventory type */ 45 | contents[0] = (byte)InventoryType; 46 | 47 | return contents; 48 | } 49 | 50 | protected override void Parse(byte[] contents) 51 | { 52 | throw new NotImplementedException(); 53 | } 54 | 55 | public override string ToString() 56 | { 57 | return string.Format("[InventoryType: {0} (0x{1:X2})]", InventoryType.ToString(), (byte)InventoryType); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/InventoryCommandListSuIdItems.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SoftwareAuthKeyLoader.Kmm 9 | { 10 | public class InventoryCommandListSuIdItems : KmmBody 11 | { 12 | public int InventoryMarker { get; private set; } 13 | 14 | public int MaxSuIdRequested { get; private set; } 15 | 16 | public override MessageId MessageId 17 | { 18 | get 19 | { 20 | return MessageId.InventoryCommand; 21 | } 22 | } 23 | 24 | public InventoryType InventoryType 25 | { 26 | get 27 | { 28 | return InventoryType.ListSuIdItems; 29 | } 30 | } 31 | 32 | public override ResponseKind ResponseKind 33 | { 34 | get 35 | { 36 | return ResponseKind.Immediate; 37 | } 38 | } 39 | 40 | public InventoryCommandListSuIdItems(int inventoryMarker, int maxSuIdRequested) 41 | { 42 | if (inventoryMarker < 0 || inventoryMarker > 0xFFFFFF) 43 | { 44 | throw new ArgumentOutOfRangeException("inventoryMarker"); 45 | } 46 | 47 | if (maxSuIdRequested < 0 || maxSuIdRequested > 0xFFFF) 48 | { 49 | throw new ArgumentOutOfRangeException("maxSuIdRequested"); 50 | } 51 | 52 | InventoryMarker = inventoryMarker; 53 | MaxSuIdRequested = maxSuIdRequested; 54 | } 55 | 56 | public override byte[] ToBytes() 57 | { 58 | byte[] contents = new byte[6]; 59 | 60 | /* inventory type */ 61 | contents[0] = (byte)InventoryType; 62 | 63 | /* inventory marker */ 64 | contents[1] = (byte)((InventoryMarker >> 16) & 0xFF); 65 | contents[2] = (byte)((InventoryMarker >> 8) & 0xFF); 66 | contents[3] = (byte)(InventoryMarker & 0xFF); 67 | 68 | /* max number of suid requested */ 69 | contents[4] = (byte)((MaxSuIdRequested >> 8) & 0xFF); 70 | contents[5] = (byte)(MaxSuIdRequested & 0xFF); 71 | 72 | return contents; 73 | } 74 | 75 | protected override void Parse(byte[] contents) 76 | { 77 | throw new NotImplementedException(); 78 | } 79 | 80 | public override string ToString() 81 | { 82 | return string.Format("[InventoryType: {0} (0x{1:X2}), InventoryMarker: {2}, MaxSuIdRequested: {3}]", InventoryType.ToString(), (byte)InventoryType, InventoryMarker, MaxSuIdRequested); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/InventoryResponseListActiveSuId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class InventoryResponseListActiveSuId : KmmBody 10 | { 11 | public InventoryType InventoryType { get; private set; } 12 | 13 | public bool ActiveSuId { get; private set; } 14 | 15 | public bool KeyAssigned { get; private set; } 16 | 17 | public SuId SuId { get; private set; } 18 | 19 | public Status Status { get; private set; } 20 | 21 | public override MessageId MessageId 22 | { 23 | get 24 | { 25 | return MessageId.InventoryResponse; 26 | } 27 | } 28 | 29 | public override ResponseKind ResponseKind 30 | { 31 | get 32 | { 33 | return ResponseKind.None; 34 | } 35 | } 36 | 37 | public InventoryResponseListActiveSuId(byte[] contents) 38 | { 39 | Parse(contents); 40 | } 41 | 42 | public override byte[] ToBytes() 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | 47 | protected override void Parse(byte[] contents) 48 | { 49 | if (contents.Length != 10) 50 | { 51 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected 10, got {0} - {1}", contents.Length.ToString(), BitConverter.ToString(contents))); 52 | } 53 | 54 | /* inventory type */ 55 | InventoryType = (InventoryType)contents[0]; 56 | 57 | /* inventory instruction */ 58 | ActiveSuId = Convert.ToBoolean(contents[1] & 0x01); 59 | KeyAssigned = Convert.ToBoolean(contents[1] & 0x02); 60 | 61 | /* suid */ 62 | byte[] suId = new byte[7]; 63 | Array.Copy(contents, 2, suId, 0, 7); 64 | SuId = new SuId(suId); 65 | 66 | /* status */ 67 | Status = (Status)contents[9]; 68 | } 69 | 70 | public override string ToString() 71 | { 72 | return string.Format("[ActiveSuId: {0}, KeyAssigned: {1}, SuId: {2}, Status: {3} (0x{4:X2})]", ActiveSuId, KeyAssigned, SuId.ToString(), Status.ToString(), (byte)Status); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/InventoryResponseListSuIdItems.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class InventoryResponseListSuIdItems : KmmBody 10 | { 11 | public InventoryType InventoryType { get; private set; } 12 | 13 | public int InventoryMarker { get; private set; } 14 | 15 | public int NumberOfItems { get; private set; } 16 | 17 | public List SuIdStatuses { get; private set; } 18 | 19 | public override MessageId MessageId 20 | { 21 | get 22 | { 23 | return MessageId.InventoryResponse; 24 | } 25 | } 26 | 27 | public override ResponseKind ResponseKind 28 | { 29 | get 30 | { 31 | return ResponseKind.None; 32 | } 33 | } 34 | 35 | public InventoryResponseListSuIdItems(byte[] contents) 36 | { 37 | Parse(contents); 38 | } 39 | 40 | public override byte[] ToBytes() 41 | { 42 | throw new NotImplementedException(); 43 | } 44 | 45 | protected override void Parse(byte[] contents) 46 | { 47 | if (contents.Length < 5) 48 | { 49 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected at least 5, got {0} - {1}", contents.Length.ToString(), BitConverter.ToString(contents))); 50 | } 51 | 52 | /* inventory type */ 53 | InventoryType = (InventoryType)contents[0]; 54 | 55 | /* inventory marker */ 56 | contents[1] = (byte)((InventoryMarker >> 16) & 0xFF); 57 | contents[2] = (byte)((InventoryMarker >> 8) & 0xFF); 58 | contents[3] = (byte)(InventoryMarker & 0xFF); 59 | 60 | /* number of items */ 61 | contents[4] = (byte)(NumberOfItems & 0xFF); 62 | 63 | /* suid and k status */ 64 | List suIdStatuses = new List(); 65 | 66 | if ((NumberOfItems == 0) && (contents.Length == 5)) 67 | { 68 | return; 69 | } 70 | else if (((NumberOfItems * 8) % (contents.Length - 5)) == 0) 71 | { 72 | for (int i = 0; i < (NumberOfItems * 8); i++) 73 | { 74 | byte[] suIdStatus = new byte[7]; 75 | Array.Copy(contents, 5 + (i * 8), suIdStatus, 0, 8); 76 | suIdStatuses.Add(new SuIdStatus(suIdStatus)); 77 | } 78 | } 79 | else 80 | { 81 | throw new Exception("the number of items field and the length of the messages does not match"); 82 | } 83 | } 84 | 85 | public override string ToString() 86 | { 87 | return string.Format("[InventoryType: {0}, InventoryMarker: {1}, NumberOfItems: {2}]", InventoryType, InventoryMarker, NumberOfItems); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/InventoryType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public enum InventoryType : byte 10 | { 11 | ListActiveSuId = 0xF7, 12 | ListSuIdItems = 0xF8 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/KmmBody.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public abstract class KmmBody 10 | { 11 | public abstract MessageId MessageId { get; } 12 | 13 | public abstract ResponseKind ResponseKind { get; } 14 | 15 | public abstract byte[] ToBytes(); 16 | 17 | protected abstract void Parse(byte[] contents); 18 | 19 | public override abstract string ToString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/KmmFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SoftwareAuthKeyLoader.Kmm 9 | { 10 | public class KmmFrame 11 | { 12 | public KmmBody KmmBody { get; private set; } 13 | 14 | public KmmFrame(KmmBody kmmBody) 15 | { 16 | if (kmmBody == null) 17 | { 18 | throw new ArgumentNullException("kmmBody"); 19 | } 20 | 21 | KmmBody = kmmBody; 22 | } 23 | 24 | public KmmFrame(byte[] contents) 25 | { 26 | Parse(contents); 27 | } 28 | 29 | public byte[] ToBytes() 30 | { 31 | byte[] body = KmmBody.ToBytes(); 32 | 33 | int length = 24 + body.Length; 34 | 35 | byte[] contents = new byte[length]; 36 | 37 | /* version */ 38 | contents[0] = 0x00; 39 | 40 | /* mfid */ 41 | contents[1] = 0x00; 42 | 43 | /* algorithm id */ 44 | contents[2] = (byte)AlgorithmId.Clear; 45 | 46 | /* key id */ 47 | contents[3] = 0x00; 48 | contents[4] = 0x00; 49 | 50 | /* message indicator */ 51 | contents[5] = 0x00; 52 | contents[6] = 0x00; 53 | contents[7] = 0x00; 54 | contents[8] = 0x00; 55 | contents[9] = 0x00; 56 | contents[10] = 0x00; 57 | contents[11] = 0x00; 58 | contents[12] = 0x00; 59 | contents[13] = 0x00; 60 | 61 | /* KMM */ 62 | 63 | /* message id */ 64 | contents[14] = (byte)KmmBody.MessageId; 65 | 66 | /* message length */ 67 | int messageLength = 7 + body.Length; 68 | contents[15] = (byte)((messageLength >> 8) & 0xFF); 69 | contents[16] = (byte)(messageLength & 0xFF); 70 | 71 | /* message format */ 72 | BitArray messageFormat = new BitArray(8, false); 73 | messageFormat.Set(7, Convert.ToBoolean(((byte)KmmBody.ResponseKind & 0x02) >> 1)); 74 | messageFormat.Set(6, Convert.ToBoolean((byte)KmmBody.ResponseKind & 0x01)); 75 | messageFormat.CopyTo(contents, 17); 76 | 77 | /* destination rsi */ 78 | contents[18] = 0xFF; 79 | contents[19] = 0xFF; 80 | contents[20] = 0xFF; 81 | 82 | /* source rsi */ 83 | contents[21] = 0xFF; 84 | contents[22] = 0xFF; 85 | contents[23] = 0xFF; 86 | 87 | /* message body */ 88 | Array.Copy(body, 0, contents, 24, body.Length); 89 | 90 | return contents; 91 | } 92 | 93 | private void Parse(byte[] contents) 94 | { 95 | if (contents.Length <= 17) 96 | { 97 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected at least 17, got {0} - {1}", contents.Length.ToString(), BitConverter.ToString(contents))); 98 | } 99 | 100 | byte messageId = contents[14]; 101 | 102 | int messageLength = 0; 103 | messageLength |= (contents[15] & 0xFF) << 8; 104 | messageLength |= contents[16] & 0xFF; 105 | 106 | int messageBodyLength = messageLength - 7; 107 | byte[] messageBody = new byte[messageBodyLength]; 108 | Array.Copy(contents, 24, messageBody, 0, messageBodyLength); 109 | 110 | if ((MessageId)messageId == MessageId.InventoryResponse) 111 | { 112 | if (messageBody.Length > 0) 113 | { 114 | InventoryType inventoryType = (InventoryType)messageBody[0]; 115 | 116 | if (inventoryType == InventoryType.ListActiveSuId) 117 | { 118 | KmmBody kmmBody = new InventoryResponseListActiveSuId(messageBody); 119 | KmmBody = kmmBody; 120 | } 121 | else 122 | { 123 | throw new Exception(string.Format("unknown inventory response type: 0x{0:X2}", (byte)inventoryType)); 124 | } 125 | } 126 | else 127 | { 128 | throw new Exception("inventory response length zero"); 129 | } 130 | } 131 | else if ((MessageId)messageId == MessageId.NegativeAcknowledgement) 132 | { 133 | KmmBody kmmBody = new NegativeAcknowledgement(messageBody); 134 | KmmBody = kmmBody; 135 | } 136 | else if ((MessageId)messageId == MessageId.LoadAuthenticationKeyResponse) 137 | { 138 | KmmBody kmmBody = new LoadAuthenticationKeyResponse(messageBody); 139 | KmmBody = kmmBody; 140 | } 141 | else if ((MessageId)messageId == MessageId.DeleteAuthenticationKeyResponse) 142 | { 143 | KmmBody kmmBody = new DeleteAuthenticationKeyResponse(messageBody); 144 | KmmBody = kmmBody; 145 | } 146 | else 147 | { 148 | throw new Exception(string.Format("unknown kmm - message id: 0x{0:X2}", messageId)); 149 | } 150 | } 151 | 152 | public override string ToString() 153 | { 154 | return string.Format("[MessageId: {0} (0x{1:X2})]", KmmBody.MessageId.ToString(), (byte)KmmBody.MessageId); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/LoadAuthenticationKeyCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SoftwareAuthKeyLoader.Kmm 9 | { 10 | public class LoadAuthenticationKeyCommand : KmmBody 11 | { 12 | public bool TargetSpecificSuId { get; private set; } 13 | 14 | public SuId SuId { get; private set; } 15 | 16 | public AlgorithmId InnerAlgorithmId { get; private set; } 17 | 18 | public byte[] Key { get; private set; } 19 | 20 | public override MessageId MessageId 21 | { 22 | get 23 | { 24 | return MessageId.LoadAuthenticationKeyCommand; 25 | } 26 | } 27 | 28 | public override ResponseKind ResponseKind 29 | { 30 | get 31 | { 32 | return ResponseKind.Immediate; 33 | } 34 | } 35 | 36 | public LoadAuthenticationKeyCommand(bool targetSpecificSuId, SuId suId, byte[] key) 37 | { 38 | if (suId == null) 39 | { 40 | throw new ArgumentNullException("suId"); 41 | } 42 | 43 | if (key.Length != 16) 44 | { 45 | throw new ArgumentOutOfRangeException("key", string.Format("length mismatch - expected 16, got {0} - {1}", key.Length.ToString(), BitConverter.ToString(key))); 46 | } 47 | 48 | TargetSpecificSuId = targetSpecificSuId; 49 | SuId = suId; 50 | InnerAlgorithmId = AlgorithmId.AES128; 51 | Key = key; 52 | } 53 | 54 | public override byte[] ToBytes() 55 | { 56 | int length = 14 + Key.Length; 57 | 58 | byte[] contents = new byte[length]; 59 | 60 | /* DECRYPTION INSTRUCTION BLOCK */ 61 | 62 | /* decryption instruction format */ 63 | contents[0] = 0x00; 64 | 65 | /* outer algorithm id */ 66 | contents[1] = (byte)AlgorithmId.Clear; 67 | 68 | /* key id */ 69 | contents[2] = 0x00; 70 | contents[3] = 0x00; 71 | 72 | /* AUTHENTICATION BLOCK */ 73 | 74 | /* authentication instruction */ 75 | BitArray authenticationInstruction = new BitArray(8, false); 76 | authenticationInstruction.Set(0, TargetSpecificSuId); 77 | authenticationInstruction.CopyTo(contents, 4); 78 | 79 | /* suid */ 80 | byte[] suId = SuId.ToBytes(); 81 | Array.Copy(suId, 0, contents, 5, suId.Length); 82 | 83 | /* inner algoritm id */ 84 | contents[12] = (byte)InnerAlgorithmId; 85 | 86 | /* key length */ 87 | contents[13] = (byte)Key.Length; 88 | 89 | /* key data */ 90 | Array.Copy(Key, 0, contents, 14, Key.Length); 91 | 92 | return contents; 93 | } 94 | 95 | protected override void Parse(byte[] contents) 96 | { 97 | throw new NotImplementedException(); 98 | } 99 | 100 | public override string ToString() 101 | { 102 | return string.Format("[TargetSpecificSuId: {0}, SuId: {1}, InnerAlgorithmId: {2} 0x{3:X2}, Key (hex): {4}]", TargetSpecificSuId, SuId.ToString(), InnerAlgorithmId.ToString(), (byte)InnerAlgorithmId, BitConverter.ToString(Key)); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/LoadAuthenticationKeyResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class LoadAuthenticationKeyResponse : KmmBody 10 | { 11 | public bool AssignmentSuccess { get; private set; } 12 | 13 | public SuId SuId { get; private set; } 14 | 15 | public Status Status { get; private set; } 16 | 17 | public override MessageId MessageId 18 | { 19 | get 20 | { 21 | return MessageId.LoadAuthenticationKeyResponse; 22 | } 23 | } 24 | 25 | public override ResponseKind ResponseKind 26 | { 27 | get 28 | { 29 | return ResponseKind.None; 30 | } 31 | } 32 | 33 | public LoadAuthenticationKeyResponse(byte[] contents) 34 | { 35 | Parse(contents); 36 | } 37 | 38 | public override byte[] ToBytes() 39 | { 40 | throw new NotImplementedException(); 41 | } 42 | 43 | protected override void Parse(byte[] contents) 44 | { 45 | if (contents.Length != 9) 46 | { 47 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected 9, got {0} - {1}", contents.Length.ToString(), BitConverter.ToString(contents))); 48 | } 49 | 50 | /* authentication instruction */ 51 | AssignmentSuccess = Convert.ToBoolean(contents[0] & 0x01); 52 | 53 | /* suid */ 54 | byte[] suId = new byte[7]; 55 | Array.Copy(contents, 1, suId, 0, 7); 56 | SuId = new SuId(suId); 57 | 58 | /* status */ 59 | Status = (Status)contents[8]; 60 | } 61 | 62 | public override string ToString() 63 | { 64 | return string.Format("[AssignmentSuccess: {0}, SuId: {1}, Status: {2} (0x{3:X2})]", AssignmentSuccess, SuId.ToString(), Status.ToString(), (byte)Status); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/MessageId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public enum MessageId : byte 10 | { 11 | InventoryCommand = 0x0D, 12 | InventoryResponse = 0x0E, 13 | NegativeAcknowledgement = 0x16, 14 | LoadAuthenticationKeyCommand = 0x28, 15 | LoadAuthenticationKeyResponse = 0x29, 16 | DeleteAuthenticationKeyCommand = 0x2A, 17 | DeleteAuthenticationKeyResponse = 0x2B 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/NegativeAcknowledgement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class NegativeAcknowledgement : KmmBody 10 | { 11 | public MessageId AcknowledgedMessageId { get; private set; } 12 | 13 | public Status Status { get; private set; } 14 | 15 | public override MessageId MessageId 16 | { 17 | get 18 | { 19 | return MessageId.NegativeAcknowledgement; 20 | } 21 | } 22 | 23 | public override ResponseKind ResponseKind 24 | { 25 | get 26 | { 27 | return ResponseKind.None; 28 | } 29 | } 30 | 31 | public NegativeAcknowledgement(byte[] contents) 32 | { 33 | Parse(contents); 34 | } 35 | 36 | public override byte[] ToBytes() 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | 41 | protected override void Parse(byte[] contents) 42 | { 43 | if (contents.Length != 2) 44 | { 45 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected 2, got {0} - {1}", contents.Length.ToString(), BitConverter.ToString(contents))); 46 | } 47 | 48 | /* acknowledged message id */ 49 | AcknowledgedMessageId = (MessageId)contents[0]; 50 | 51 | /* status */ 52 | Status = (Status)contents[1]; 53 | } 54 | 55 | public override string ToString() 56 | { 57 | return string.Format("[AcknowledgedMessageId: {0} (0x{1:X2}), Status: {2} (0x{3:X2})]", AcknowledgedMessageId.ToString(), (byte)AcknowledgedMessageId, Status.ToString(), (byte)Status); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/ResponseKind.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public enum ResponseKind : byte 10 | { 11 | None, 12 | Delayed, 13 | Immediate 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/Status.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public enum Status : byte 10 | { 11 | CommandWasPerformed, 12 | CommandCouldNotBePerformed, 13 | ItemDoesNotExist, 14 | InvalidMessageId, 15 | InvalidChecksumOrMac, 16 | OutOfMemory, 17 | CouldNotDecryptMessage, 18 | InvalidMessageNumber, 19 | InvalidKeyId, 20 | InvalidAlgorithmId, 21 | InvalidMfId, 22 | ModuleFailure, 23 | MiAllZeros, 24 | Keyfail, 25 | InvalidWacnIdOrSystemId, 26 | InvalidSubscriberId, 27 | Unknown = 0xFF 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/SuId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class SuId 10 | { 11 | public int WacnId { get; private set; } 12 | 13 | public int SystemId { get; private set; } 14 | 15 | public int UnitId { get; private set; } 16 | 17 | public SuId(int wacnId, int systemId, int unitId) 18 | { 19 | if (WacnId < 0 || WacnId > 0xFFFFF) 20 | { 21 | throw new ArgumentOutOfRangeException("wacnId"); 22 | } 23 | 24 | if (SystemId < 0 || SystemId > 0xFFF) 25 | { 26 | throw new ArgumentOutOfRangeException("systemId"); 27 | } 28 | 29 | if (UnitId < 0 || UnitId > 0xFFFFFF) 30 | { 31 | throw new ArgumentOutOfRangeException("unitId"); 32 | } 33 | 34 | WacnId = wacnId; 35 | SystemId = systemId; 36 | UnitId = unitId; 37 | } 38 | 39 | public SuId(byte[] contents) 40 | { 41 | Parse(contents); 42 | } 43 | 44 | public byte[] ToBytes() 45 | { 46 | byte[] contents = new byte[7]; 47 | 48 | contents[0] = (byte)((WacnId >> 12) & 0xFF); 49 | contents[1] = (byte)((WacnId >> 4) & 0xFF); 50 | contents[2] |= (byte)((WacnId << 4) & 0xF0); 51 | contents[2] |= (byte)((SystemId >> 8) & 0x0F); 52 | contents[3] = (byte)(SystemId & 0xFF); 53 | contents[4] = (byte)((UnitId >> 16) & 0xFF); 54 | contents[5] = (byte)((UnitId >> 8) & 0xFF); 55 | contents[6] = (byte)(UnitId & 0xFF); 56 | 57 | return contents; 58 | } 59 | 60 | public void Parse(byte[] contents) 61 | { 62 | if (contents.Length != 7) 63 | { 64 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected 7, got {0} - {0}", contents.Length.ToString(), BitConverter.ToString(contents))); 65 | } 66 | 67 | WacnId |= (contents[0] & 0xFF) << 12; 68 | WacnId |= (contents[1] & 0xFF) << 4; 69 | WacnId |= (contents[2] >> 4) & 0x0F; 70 | SystemId |= (contents[2] & 0x0F) << 8; 71 | SystemId |= contents[3] & 0xFF; 72 | UnitId |= (contents[4] & 0xFF) << 16; 73 | UnitId |= (contents[5] & 0xFF) << 8; 74 | UnitId |= contents[6] & 0xFF; 75 | } 76 | 77 | public override string ToString() 78 | { 79 | return string.Format("[WacnId: 0x{0:X}, SystemId: 0x{1:X}, UnitId: 0x{2:X}]", WacnId, SystemId, UnitId); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Kmm/SuIdStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader.Kmm 8 | { 9 | public class SuIdStatus 10 | { 11 | public SuId SuId { get; private set; } 12 | 13 | public bool KeyAssigned { get; private set; } 14 | 15 | public bool ActiveSuId { get; private set; } 16 | 17 | public SuIdStatus(byte[] contents) 18 | { 19 | Parse(contents); 20 | } 21 | 22 | public byte[] ToBytes() 23 | { 24 | throw new NotImplementedException(); 25 | } 26 | 27 | public void Parse(byte[] contents) 28 | { 29 | if (contents.Length != 8) 30 | { 31 | throw new ArgumentOutOfRangeException("contents", string.Format("length mismatch - expected 7, got {0} - {0}", contents.Length.ToString(), BitConverter.ToString(contents))); 32 | } 33 | 34 | /* suid */ 35 | byte[] suId = new byte[7]; 36 | Array.Copy(contents, 0, suId, 0, 7); 37 | SuId = new SuId(suId); 38 | 39 | /* k status */ 40 | KeyAssigned = Convert.ToBoolean(contents[7] & 0x01); 41 | ActiveSuId = Convert.ToBoolean(contents[7] & 0x02); 42 | } 43 | 44 | public override string ToString() 45 | { 46 | return string.Format("[SuId: {0}, KeyAssigned: {1}, ActiveSuId: {2}]", SuId.ToString(), KeyAssigned, ActiveSuId); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Mono/Options.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Options.cs 3 | // 4 | // Authors: 5 | // Jonathan Pryor , 6 | // Federico Di Gregorio 7 | // Rolf Bjarne Kvinge 8 | // 9 | // Copyright (C) 2008 Novell (http://www.novell.com) 10 | // Copyright (C) 2009 Federico Di Gregorio. 11 | // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com) 12 | // Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com) 13 | // 14 | // Permission is hereby granted, free of charge, to any person obtaining 15 | // a copy of this software and associated documentation files (the 16 | // "Software"), to deal in the Software without restriction, including 17 | // without limitation the rights to use, copy, modify, merge, publish, 18 | // distribute, sublicense, and/or sell copies of the Software, and to 19 | // permit persons to whom the Software is furnished to do so, subject to 20 | // the following conditions: 21 | // 22 | // The above copyright notice and this permission notice shall be 23 | // included in all copies or substantial portions of the Software. 24 | // 25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | // 33 | 34 | // Compile With: 35 | // mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll -t:library 36 | // mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll -t:library 37 | // 38 | // The LINQ version just changes the implementation of 39 | // OptionSet.Parse(IEnumerable), and confers no semantic changes. 40 | 41 | // 42 | // A Getopt::Long-inspired option parsing library for C#. 43 | // 44 | // Mono.Options.OptionSet is built upon a key/value table, where the 45 | // key is a option format string and the value is a delegate that is 46 | // invoked when the format string is matched. 47 | // 48 | // Option format strings: 49 | // Regex-like BNF Grammar: 50 | // name: .+ 51 | // type: [=:] 52 | // sep: ( [^{}]+ | '{' .+ '}' )? 53 | // aliases: ( name type sep ) ( '|' name type sep )* 54 | // 55 | // Each '|'-delimited name is an alias for the associated action. If the 56 | // format string ends in a '=', it has a required value. If the format 57 | // string ends in a ':', it has an optional value. If neither '=' or ':' 58 | // is present, no value is supported. `=' or `:' need only be defined on one 59 | // alias, but if they are provided on more than one they must be consistent. 60 | // 61 | // Each alias portion may also end with a "key/value separator", which is used 62 | // to split option values if the option accepts > 1 value. If not specified, 63 | // it defaults to '=' and ':'. If specified, it can be any character except 64 | // '{' and '}' OR the *string* between '{' and '}'. If no separator should be 65 | // used (i.e. the separate values should be distinct arguments), then "{}" 66 | // should be used as the separator. 67 | // 68 | // Options are extracted either from the current option by looking for 69 | // the option name followed by an '=' or ':', or is taken from the 70 | // following option IFF: 71 | // - The current option does not contain a '=' or a ':' 72 | // - The current option requires a value (i.e. not a Option type of ':') 73 | // 74 | // The `name' used in the option format string does NOT include any leading 75 | // option indicator, such as '-', '--', or '/'. All three of these are 76 | // permitted/required on any named option. 77 | // 78 | // Option bundling is permitted so long as: 79 | // - '-' is used to start the option group 80 | // - all of the bundled options are a single character 81 | // - at most one of the bundled options accepts a value, and the value 82 | // provided starts from the next character to the end of the string. 83 | // 84 | // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' 85 | // as '-Dname=value'. 86 | // 87 | // Option processing is disabled by specifying "--". All options after "--" 88 | // are returned by OptionSet.Parse() unchanged and unprocessed. 89 | // 90 | // Unprocessed options are returned from OptionSet.Parse(). 91 | // 92 | // Examples: 93 | // int verbose = 0; 94 | // OptionSet p = new OptionSet () 95 | // .Add ("v", v => ++verbose) 96 | // .Add ("name=|value=", v => Console.WriteLine (v)); 97 | // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); 98 | // 99 | // The above would parse the argument string array, and would invoke the 100 | // lambda expression three times, setting `verbose' to 3 when complete. 101 | // It would also print out "A" and "B" to standard output. 102 | // The returned array would contain the string "extra". 103 | // 104 | // C# 3.0 collection initializers are supported and encouraged: 105 | // var p = new OptionSet () { 106 | // { "h|?|help", v => ShowHelp () }, 107 | // }; 108 | // 109 | // System.ComponentModel.TypeConverter is also supported, allowing the use of 110 | // custom data types in the callback type; TypeConverter.ConvertFromString() 111 | // is used to convert the value option to an instance of the specified 112 | // type: 113 | // 114 | // var p = new OptionSet () { 115 | // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, 116 | // }; 117 | // 118 | // Random other tidbits: 119 | // - Boolean options (those w/o '=' or ':' in the option format string) 120 | // are explicitly enabled if they are followed with '+', and explicitly 121 | // disabled if they are followed with '-': 122 | // string a = null; 123 | // var p = new OptionSet () { 124 | // { "a", s => a = s }, 125 | // }; 126 | // p.Parse (new string[]{"-a"}); // sets v != null 127 | // p.Parse (new string[]{"-a+"}); // sets v != null 128 | // p.Parse (new string[]{"-a-"}); // sets v == null 129 | // 130 | 131 | // 132 | // Mono.Options.CommandSet allows easily having separate commands and 133 | // associated command options, allowing creation of a *suite* along the 134 | // lines of **git**(1), **svn**(1), etc. 135 | // 136 | // CommandSet allows intermixing plain text strings for `--help` output, 137 | // Option values -- as supported by OptionSet -- and Command instances, 138 | // which have a name, optional help text, and an optional OptionSet. 139 | // 140 | // var suite = new CommandSet ("suite-name") { 141 | // // Use strings and option values, as with OptionSet 142 | // "usage: suite-name COMMAND [OPTIONS]+", 143 | // { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 }, 144 | // // Commands may also be specified 145 | // new Command ("command-name", "command help") { 146 | // Options = new OptionSet {/*...*/}, 147 | // Run = args => { /*...*/}, 148 | // }, 149 | // new MyCommandSubclass (), 150 | // }; 151 | // return suite.Run (new string[]{...}); 152 | // 153 | // CommandSet provides a `help` command, and forwards `help COMMAND` 154 | // to the registered Command instance by invoking Command.Invoke() 155 | // with `--help` as an option. 156 | // 157 | 158 | using System; 159 | using System.Collections; 160 | using System.Collections.Generic; 161 | using System.Collections.ObjectModel; 162 | using System.ComponentModel; 163 | using System.Globalization; 164 | using System.IO; 165 | #if PCL 166 | using System.Reflection; 167 | #else 168 | using System.Runtime.Serialization; 169 | using System.Security.Permissions; 170 | #endif 171 | using System.Text; 172 | using System.Text.RegularExpressions; 173 | 174 | #if LINQ 175 | using System.Linq; 176 | #endif 177 | 178 | #if TEST 179 | using NDesk.Options; 180 | #endif 181 | 182 | #if PCL 183 | using MessageLocalizerConverter = System.Func; 184 | #else 185 | using MessageLocalizerConverter = System.Converter; 186 | #endif 187 | 188 | #if NDESK_OPTIONS 189 | namespace NDesk.Options 190 | #else 191 | namespace Mono.Options 192 | #endif 193 | { 194 | static class StringCoda { 195 | 196 | public static IEnumerable WrappedLines (string self, params int[] widths) 197 | { 198 | IEnumerable w = widths; 199 | return WrappedLines (self, w); 200 | } 201 | 202 | public static IEnumerable WrappedLines (string self, IEnumerable widths) 203 | { 204 | if (widths == null) 205 | throw new ArgumentNullException ("widths"); 206 | return CreateWrappedLinesIterator (self, widths); 207 | } 208 | 209 | private static IEnumerable CreateWrappedLinesIterator (string self, IEnumerable widths) 210 | { 211 | if (string.IsNullOrEmpty (self)) { 212 | yield return string.Empty; 213 | yield break; 214 | } 215 | using (IEnumerator ewidths = widths.GetEnumerator ()) { 216 | bool? hw = null; 217 | int width = GetNextWidth (ewidths, int.MaxValue, ref hw); 218 | int start = 0, end; 219 | do { 220 | end = GetLineEnd (start, width, self); 221 | char c = self [end-1]; 222 | if (char.IsWhiteSpace (c)) 223 | --end; 224 | bool needContinuation = end != self.Length && !IsEolChar (c); 225 | string continuation = ""; 226 | if (needContinuation) { 227 | --end; 228 | continuation = "-"; 229 | } 230 | string line = self.Substring (start, end - start) + continuation; 231 | yield return line; 232 | start = end; 233 | if (char.IsWhiteSpace (c)) 234 | ++start; 235 | width = GetNextWidth (ewidths, width, ref hw); 236 | } while (start < self.Length); 237 | } 238 | } 239 | 240 | private static int GetNextWidth (IEnumerator ewidths, int curWidth, ref bool? eValid) 241 | { 242 | if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) { 243 | curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth; 244 | // '.' is any character, - is for a continuation 245 | const string minWidth = ".-"; 246 | if (curWidth < minWidth.Length) 247 | throw new ArgumentOutOfRangeException ("widths", 248 | string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); 249 | return curWidth; 250 | } 251 | // no more elements, use the last element. 252 | return curWidth; 253 | } 254 | 255 | private static bool IsEolChar (char c) 256 | { 257 | return !char.IsLetterOrDigit (c); 258 | } 259 | 260 | private static int GetLineEnd (int start, int length, string description) 261 | { 262 | int end = System.Math.Min (start + length, description.Length); 263 | int sep = -1; 264 | for (int i = start; i < end; ++i) { 265 | if (description [i] == '\n') 266 | return i+1; 267 | if (IsEolChar (description [i])) 268 | sep = i+1; 269 | } 270 | if (sep == -1 || end == description.Length) 271 | return end; 272 | return sep; 273 | } 274 | } 275 | 276 | public class OptionValueCollection : IList, IList { 277 | 278 | List values = new List (); 279 | OptionContext c; 280 | 281 | internal OptionValueCollection (OptionContext c) 282 | { 283 | this.c = c; 284 | } 285 | 286 | #region ICollection 287 | void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} 288 | bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} 289 | object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} 290 | #endregion 291 | 292 | #region ICollection 293 | public void Add (string item) {values.Add (item);} 294 | public void Clear () {values.Clear ();} 295 | public bool Contains (string item) {return values.Contains (item);} 296 | public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} 297 | public bool Remove (string item) {return values.Remove (item);} 298 | public int Count {get {return values.Count;}} 299 | public bool IsReadOnly {get {return false;}} 300 | #endregion 301 | 302 | #region IEnumerable 303 | IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} 304 | #endregion 305 | 306 | #region IEnumerable 307 | public IEnumerator GetEnumerator () {return values.GetEnumerator ();} 308 | #endregion 309 | 310 | #region IList 311 | int IList.Add (object value) {return (values as IList).Add (value);} 312 | bool IList.Contains (object value) {return (values as IList).Contains (value);} 313 | int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} 314 | void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} 315 | void IList.Remove (object value) {(values as IList).Remove (value);} 316 | void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} 317 | bool IList.IsFixedSize {get {return false;}} 318 | object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} 319 | #endregion 320 | 321 | #region IList 322 | public int IndexOf (string item) {return values.IndexOf (item);} 323 | public void Insert (int index, string item) {values.Insert (index, item);} 324 | public void RemoveAt (int index) {values.RemoveAt (index);} 325 | 326 | private void AssertValid (int index) 327 | { 328 | if (c.Option == null) 329 | throw new InvalidOperationException ("OptionContext.Option is null."); 330 | if (index >= c.Option.MaxValueCount) 331 | throw new ArgumentOutOfRangeException ("index"); 332 | if (c.Option.OptionValueType == OptionValueType.Required && 333 | index >= values.Count) 334 | throw new OptionException (string.Format ( 335 | c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), 336 | c.OptionName); 337 | } 338 | 339 | public string this [int index] { 340 | get { 341 | AssertValid (index); 342 | return index >= values.Count ? null : values [index]; 343 | } 344 | set { 345 | values [index] = value; 346 | } 347 | } 348 | #endregion 349 | 350 | public List ToList () 351 | { 352 | return new List (values); 353 | } 354 | 355 | public string[] ToArray () 356 | { 357 | return values.ToArray (); 358 | } 359 | 360 | public override string ToString () 361 | { 362 | return string.Join (", ", values.ToArray ()); 363 | } 364 | } 365 | 366 | public class OptionContext { 367 | private Option option; 368 | private string name; 369 | private int index; 370 | private OptionSet set; 371 | private OptionValueCollection c; 372 | 373 | public OptionContext (OptionSet set) 374 | { 375 | this.set = set; 376 | this.c = new OptionValueCollection (this); 377 | } 378 | 379 | public Option Option { 380 | get {return option;} 381 | set {option = value;} 382 | } 383 | 384 | public string OptionName { 385 | get {return name;} 386 | set {name = value;} 387 | } 388 | 389 | public int OptionIndex { 390 | get {return index;} 391 | set {index = value;} 392 | } 393 | 394 | public OptionSet OptionSet { 395 | get {return set;} 396 | } 397 | 398 | public OptionValueCollection OptionValues { 399 | get {return c;} 400 | } 401 | } 402 | 403 | public enum OptionValueType { 404 | None, 405 | Optional, 406 | Required, 407 | } 408 | 409 | public abstract class Option { 410 | string prototype, description; 411 | string[] names; 412 | OptionValueType type; 413 | int count; 414 | string[] separators; 415 | bool hidden; 416 | 417 | protected Option (string prototype, string description) 418 | : this (prototype, description, 1, false) 419 | { 420 | } 421 | 422 | protected Option (string prototype, string description, int maxValueCount) 423 | : this (prototype, description, maxValueCount, false) 424 | { 425 | } 426 | 427 | protected Option (string prototype, string description, int maxValueCount, bool hidden) 428 | { 429 | if (prototype == null) 430 | throw new ArgumentNullException ("prototype"); 431 | if (prototype.Length == 0) 432 | throw new ArgumentException ("Cannot be the empty string.", "prototype"); 433 | if (maxValueCount < 0) 434 | throw new ArgumentOutOfRangeException ("maxValueCount"); 435 | 436 | this.prototype = prototype; 437 | this.description = description; 438 | this.count = maxValueCount; 439 | this.names = (this is OptionSet.Category) 440 | // append GetHashCode() so that "duplicate" categories have distinct 441 | // names, e.g. adding multiple "" categories should be valid. 442 | ? new[]{prototype + this.GetHashCode ()} 443 | : prototype.Split ('|'); 444 | 445 | if (this is OptionSet.Category || this is CommandOption) 446 | return; 447 | 448 | this.type = ParsePrototype (); 449 | this.hidden = hidden; 450 | 451 | if (this.count == 0 && type != OptionValueType.None) 452 | throw new ArgumentException ( 453 | "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + 454 | "OptionValueType.Optional.", 455 | "maxValueCount"); 456 | if (this.type == OptionValueType.None && maxValueCount > 1) 457 | throw new ArgumentException ( 458 | string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), 459 | "maxValueCount"); 460 | if (Array.IndexOf (names, "<>") >= 0 && 461 | ((names.Length == 1 && this.type != OptionValueType.None) || 462 | (names.Length > 1 && this.MaxValueCount > 1))) 463 | throw new ArgumentException ( 464 | "The default option handler '<>' cannot require values.", 465 | "prototype"); 466 | } 467 | 468 | public string Prototype {get {return prototype;}} 469 | public string Description {get {return description;}} 470 | public OptionValueType OptionValueType {get {return type;}} 471 | public int MaxValueCount {get {return count;}} 472 | public bool Hidden {get {return hidden;}} 473 | 474 | public string[] GetNames () 475 | { 476 | return (string[]) names.Clone (); 477 | } 478 | 479 | public string[] GetValueSeparators () 480 | { 481 | if (separators == null) 482 | return new string [0]; 483 | return (string[]) separators.Clone (); 484 | } 485 | 486 | protected static T Parse (string value, OptionContext c) 487 | { 488 | Type tt = typeof (T); 489 | #if PCL 490 | TypeInfo ti = tt.GetTypeInfo (); 491 | #else 492 | Type ti = tt; 493 | #endif 494 | bool nullable = 495 | ti.IsValueType && 496 | ti.IsGenericType && 497 | !ti.IsGenericTypeDefinition && 498 | ti.GetGenericTypeDefinition () == typeof (Nullable<>); 499 | #if PCL 500 | Type targetType = nullable ? tt.GenericTypeArguments [0] : tt; 501 | #else 502 | Type targetType = nullable ? tt.GetGenericArguments () [0] : tt; 503 | #endif 504 | T t = default (T); 505 | try { 506 | if (value != null) { 507 | #if PCL 508 | if (targetType.GetTypeInfo ().IsEnum) 509 | t = (T) Enum.Parse (targetType, value, true); 510 | else 511 | t = (T) Convert.ChangeType (value, targetType); 512 | #else 513 | TypeConverter conv = TypeDescriptor.GetConverter (targetType); 514 | t = (T) conv.ConvertFromString (value); 515 | #endif 516 | } 517 | } 518 | catch (Exception e) { 519 | throw new OptionException ( 520 | string.Format ( 521 | c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), 522 | value, targetType.Name, c.OptionName), 523 | c.OptionName, e); 524 | } 525 | return t; 526 | } 527 | 528 | internal string[] Names {get {return names;}} 529 | internal string[] ValueSeparators {get {return separators;}} 530 | 531 | static readonly char[] NameTerminator = new char[]{'=', ':'}; 532 | 533 | private OptionValueType ParsePrototype () 534 | { 535 | char type = '\0'; 536 | List seps = new List (); 537 | for (int i = 0; i < names.Length; ++i) { 538 | string name = names [i]; 539 | if (name.Length == 0) 540 | throw new ArgumentException ("Empty option names are not supported.", "prototype"); 541 | 542 | int end = name.IndexOfAny (NameTerminator); 543 | if (end == -1) 544 | continue; 545 | names [i] = name.Substring (0, end); 546 | if (type == '\0' || type == name [end]) 547 | type = name [end]; 548 | else 549 | throw new ArgumentException ( 550 | string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), 551 | "prototype"); 552 | AddSeparators (name, end, seps); 553 | } 554 | 555 | if (type == '\0') 556 | return OptionValueType.None; 557 | 558 | if (count <= 1 && seps.Count != 0) 559 | throw new ArgumentException ( 560 | string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), 561 | "prototype"); 562 | if (count > 1) { 563 | if (seps.Count == 0) 564 | this.separators = new string[]{":", "="}; 565 | else if (seps.Count == 1 && seps [0].Length == 0) 566 | this.separators = null; 567 | else 568 | this.separators = seps.ToArray (); 569 | } 570 | 571 | return type == '=' ? OptionValueType.Required : OptionValueType.Optional; 572 | } 573 | 574 | private static void AddSeparators (string name, int end, ICollection seps) 575 | { 576 | int start = -1; 577 | for (int i = end+1; i < name.Length; ++i) { 578 | switch (name [i]) { 579 | case '{': 580 | if (start != -1) 581 | throw new ArgumentException ( 582 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name), 583 | "prototype"); 584 | start = i+1; 585 | break; 586 | case '}': 587 | if (start == -1) 588 | throw new ArgumentException ( 589 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name), 590 | "prototype"); 591 | seps.Add (name.Substring (start, i-start)); 592 | start = -1; 593 | break; 594 | default: 595 | if (start == -1) 596 | seps.Add (name [i].ToString ()); 597 | break; 598 | } 599 | } 600 | if (start != -1) 601 | throw new ArgumentException ( 602 | string.Format ("Ill-formed name/value separator found in \"{0}\".", name), 603 | "prototype"); 604 | } 605 | 606 | public void Invoke (OptionContext c) 607 | { 608 | OnParseComplete (c); 609 | c.OptionName = null; 610 | c.Option = null; 611 | c.OptionValues.Clear (); 612 | } 613 | 614 | protected abstract void OnParseComplete (OptionContext c); 615 | 616 | internal void InvokeOnParseComplete (OptionContext c) 617 | { 618 | OnParseComplete (c); 619 | } 620 | 621 | public override string ToString () 622 | { 623 | return Prototype; 624 | } 625 | } 626 | 627 | public abstract class ArgumentSource { 628 | 629 | protected ArgumentSource () 630 | { 631 | } 632 | 633 | public abstract string[] GetNames (); 634 | public abstract string Description { get; } 635 | public abstract bool GetArguments (string value, out IEnumerable replacement); 636 | 637 | #if !PCL || NETSTANDARD1_3 638 | public static IEnumerable GetArgumentsFromFile (string file) 639 | { 640 | return GetArguments (File.OpenText (file), true); 641 | } 642 | #endif 643 | 644 | public static IEnumerable GetArguments (TextReader reader) 645 | { 646 | return GetArguments (reader, false); 647 | } 648 | 649 | // Cribbed from mcs/driver.cs:LoadArgs(string) 650 | static IEnumerable GetArguments (TextReader reader, bool close) 651 | { 652 | try { 653 | StringBuilder arg = new StringBuilder (); 654 | 655 | string line; 656 | while ((line = reader.ReadLine ()) != null) { 657 | int t = line.Length; 658 | 659 | for (int i = 0; i < t; i++) { 660 | char c = line [i]; 661 | 662 | if (c == '"' || c == '\'') { 663 | char end = c; 664 | 665 | for (i++; i < t; i++){ 666 | c = line [i]; 667 | 668 | if (c == end) 669 | break; 670 | arg.Append (c); 671 | } 672 | } else if (c == ' ') { 673 | if (arg.Length > 0) { 674 | yield return arg.ToString (); 675 | arg.Length = 0; 676 | } 677 | } else 678 | arg.Append (c); 679 | } 680 | if (arg.Length > 0) { 681 | yield return arg.ToString (); 682 | arg.Length = 0; 683 | } 684 | } 685 | } 686 | finally { 687 | if (close) 688 | reader.Dispose (); 689 | } 690 | } 691 | } 692 | 693 | #if !PCL || NETSTANDARD1_3 694 | public class ResponseFileSource : ArgumentSource { 695 | 696 | public override string[] GetNames () 697 | { 698 | return new string[]{"@file"}; 699 | } 700 | 701 | public override string Description { 702 | get {return "Read response file for more options.";} 703 | } 704 | 705 | public override bool GetArguments (string value, out IEnumerable replacement) 706 | { 707 | if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) { 708 | replacement = null; 709 | return false; 710 | } 711 | replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1)); 712 | return true; 713 | } 714 | } 715 | #endif 716 | 717 | #if !PCL 718 | [Serializable] 719 | #endif 720 | public class OptionException : Exception { 721 | private string option; 722 | 723 | public OptionException () 724 | { 725 | } 726 | 727 | public OptionException (string message, string optionName) 728 | : base (message) 729 | { 730 | this.option = optionName; 731 | } 732 | 733 | public OptionException (string message, string optionName, Exception innerException) 734 | : base (message, innerException) 735 | { 736 | this.option = optionName; 737 | } 738 | 739 | #if !PCL 740 | protected OptionException (SerializationInfo info, StreamingContext context) 741 | : base (info, context) 742 | { 743 | this.option = info.GetString ("OptionName"); 744 | } 745 | #endif 746 | 747 | public string OptionName { 748 | get {return this.option;} 749 | } 750 | 751 | #if !PCL 752 | #pragma warning disable 618 // SecurityPermissionAttribute is obsolete 753 | [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)] 754 | #pragma warning restore 618 755 | public override void GetObjectData (SerializationInfo info, StreamingContext context) 756 | { 757 | base.GetObjectData (info, context); 758 | info.AddValue ("OptionName", option); 759 | } 760 | #endif 761 | } 762 | 763 | public delegate void OptionAction (TKey key, TValue value); 764 | 765 | public class OptionSet : KeyedCollection 766 | { 767 | public OptionSet () 768 | : this (null) 769 | { 770 | } 771 | 772 | public OptionSet (MessageLocalizerConverter localizer) 773 | { 774 | this.roSources = new ReadOnlyCollection (sources); 775 | this.localizer = localizer; 776 | if (this.localizer == null) { 777 | this.localizer = delegate (string f) { 778 | return f; 779 | }; 780 | } 781 | } 782 | 783 | MessageLocalizerConverter localizer; 784 | 785 | public MessageLocalizerConverter MessageLocalizer { 786 | get {return localizer;} 787 | internal set {localizer = value;} 788 | } 789 | 790 | List sources = new List (); 791 | ReadOnlyCollection roSources; 792 | 793 | public ReadOnlyCollection ArgumentSources { 794 | get {return roSources;} 795 | } 796 | 797 | 798 | protected override string GetKeyForItem (Option item) 799 | { 800 | if (item == null) 801 | throw new ArgumentNullException ("option"); 802 | if (item.Names != null && item.Names.Length > 0) 803 | return item.Names [0]; 804 | // This should never happen, as it's invalid for Option to be 805 | // constructed w/o any names. 806 | throw new InvalidOperationException ("Option has no names!"); 807 | } 808 | 809 | [Obsolete ("Use KeyedCollection.this[string]")] 810 | protected Option GetOptionForName (string option) 811 | { 812 | if (option == null) 813 | throw new ArgumentNullException ("option"); 814 | try { 815 | return base [option]; 816 | } 817 | catch (KeyNotFoundException) { 818 | return null; 819 | } 820 | } 821 | 822 | protected override void InsertItem (int index, Option item) 823 | { 824 | base.InsertItem (index, item); 825 | AddImpl (item); 826 | } 827 | 828 | protected override void RemoveItem (int index) 829 | { 830 | Option p = Items [index]; 831 | base.RemoveItem (index); 832 | // KeyedCollection.RemoveItem() handles the 0th item 833 | for (int i = 1; i < p.Names.Length; ++i) { 834 | Dictionary.Remove (p.Names [i]); 835 | } 836 | } 837 | 838 | protected override void SetItem (int index, Option item) 839 | { 840 | base.SetItem (index, item); 841 | AddImpl (item); 842 | } 843 | 844 | private void AddImpl (Option option) 845 | { 846 | if (option == null) 847 | throw new ArgumentNullException ("option"); 848 | List added = new List (option.Names.Length); 849 | try { 850 | // KeyedCollection.InsertItem/SetItem handle the 0th name. 851 | for (int i = 1; i < option.Names.Length; ++i) { 852 | Dictionary.Add (option.Names [i], option); 853 | added.Add (option.Names [i]); 854 | } 855 | } 856 | catch (Exception) { 857 | foreach (string name in added) 858 | Dictionary.Remove (name); 859 | throw; 860 | } 861 | } 862 | 863 | public OptionSet Add (string header) 864 | { 865 | if (header == null) 866 | throw new ArgumentNullException ("header"); 867 | Add (new Category (header)); 868 | return this; 869 | } 870 | 871 | internal sealed class Category : Option { 872 | 873 | // Prototype starts with '=' because this is an invalid prototype 874 | // (see Option.ParsePrototype(), and thus it'll prevent Category 875 | // instances from being accidentally used as normal options. 876 | public Category (string description) 877 | : base ("=:Category:= " + description, description) 878 | { 879 | } 880 | 881 | protected override void OnParseComplete (OptionContext c) 882 | { 883 | throw new NotSupportedException ("Category.OnParseComplete should not be invoked."); 884 | } 885 | } 886 | 887 | 888 | public new OptionSet Add (Option option) 889 | { 890 | base.Add (option); 891 | return this; 892 | } 893 | 894 | sealed class ActionOption : Option { 895 | Action action; 896 | 897 | public ActionOption (string prototype, string description, int count, Action action) 898 | : this (prototype, description, count, action, false) 899 | { 900 | } 901 | 902 | public ActionOption (string prototype, string description, int count, Action action, bool hidden) 903 | : base (prototype, description, count, hidden) 904 | { 905 | if (action == null) 906 | throw new ArgumentNullException ("action"); 907 | this.action = action; 908 | } 909 | 910 | protected override void OnParseComplete (OptionContext c) 911 | { 912 | action (c.OptionValues); 913 | } 914 | } 915 | 916 | public OptionSet Add (string prototype, Action action) 917 | { 918 | return Add (prototype, null, action); 919 | } 920 | 921 | public OptionSet Add (string prototype, string description, Action action) 922 | { 923 | return Add (prototype, description, action, false); 924 | } 925 | 926 | public OptionSet Add (string prototype, string description, Action action, bool hidden) 927 | { 928 | if (action == null) 929 | throw new ArgumentNullException ("action"); 930 | Option p = new ActionOption (prototype, description, 1, 931 | delegate (OptionValueCollection v) { action (v [0]); }, hidden); 932 | base.Add (p); 933 | return this; 934 | } 935 | 936 | public OptionSet Add (string prototype, OptionAction action) 937 | { 938 | return Add (prototype, null, action); 939 | } 940 | 941 | public OptionSet Add (string prototype, string description, OptionAction action) 942 | { 943 | return Add (prototype, description, action, false); 944 | } 945 | 946 | public OptionSet Add (string prototype, string description, OptionAction action, bool hidden) { 947 | if (action == null) 948 | throw new ArgumentNullException ("action"); 949 | Option p = new ActionOption (prototype, description, 2, 950 | delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden); 951 | base.Add (p); 952 | return this; 953 | } 954 | 955 | sealed class ActionOption : Option { 956 | Action action; 957 | 958 | public ActionOption (string prototype, string description, Action action) 959 | : base (prototype, description, 1) 960 | { 961 | if (action == null) 962 | throw new ArgumentNullException ("action"); 963 | this.action = action; 964 | } 965 | 966 | protected override void OnParseComplete (OptionContext c) 967 | { 968 | action (Parse (c.OptionValues [0], c)); 969 | } 970 | } 971 | 972 | sealed class ActionOption : Option { 973 | OptionAction action; 974 | 975 | public ActionOption (string prototype, string description, OptionAction action) 976 | : base (prototype, description, 2) 977 | { 978 | if (action == null) 979 | throw new ArgumentNullException ("action"); 980 | this.action = action; 981 | } 982 | 983 | protected override void OnParseComplete (OptionContext c) 984 | { 985 | action ( 986 | Parse (c.OptionValues [0], c), 987 | Parse (c.OptionValues [1], c)); 988 | } 989 | } 990 | 991 | public OptionSet Add (string prototype, Action action) 992 | { 993 | return Add (prototype, null, action); 994 | } 995 | 996 | public OptionSet Add (string prototype, string description, Action action) 997 | { 998 | return Add (new ActionOption (prototype, description, action)); 999 | } 1000 | 1001 | public OptionSet Add (string prototype, OptionAction action) 1002 | { 1003 | return Add (prototype, null, action); 1004 | } 1005 | 1006 | public OptionSet Add (string prototype, string description, OptionAction action) 1007 | { 1008 | return Add (new ActionOption (prototype, description, action)); 1009 | } 1010 | 1011 | public OptionSet Add (ArgumentSource source) 1012 | { 1013 | if (source == null) 1014 | throw new ArgumentNullException ("source"); 1015 | sources.Add (source); 1016 | return this; 1017 | } 1018 | 1019 | protected virtual OptionContext CreateOptionContext () 1020 | { 1021 | return new OptionContext (this); 1022 | } 1023 | 1024 | public List Parse (IEnumerable arguments) 1025 | { 1026 | if (arguments == null) 1027 | throw new ArgumentNullException ("arguments"); 1028 | OptionContext c = CreateOptionContext (); 1029 | c.OptionIndex = -1; 1030 | bool process = true; 1031 | List unprocessed = new List (); 1032 | Option def = Contains ("<>") ? this ["<>"] : null; 1033 | ArgumentEnumerator ae = new ArgumentEnumerator (arguments); 1034 | foreach (string argument in ae) { 1035 | ++c.OptionIndex; 1036 | if (argument == "--") { 1037 | process = false; 1038 | continue; 1039 | } 1040 | if (!process) { 1041 | Unprocessed (unprocessed, def, c, argument); 1042 | continue; 1043 | } 1044 | if (AddSource (ae, argument)) 1045 | continue; 1046 | if (!Parse (argument, c)) 1047 | Unprocessed (unprocessed, def, c, argument); 1048 | } 1049 | if (c.Option != null) 1050 | c.Option.Invoke (c); 1051 | return unprocessed; 1052 | } 1053 | 1054 | class ArgumentEnumerator : IEnumerable { 1055 | List> sources = new List> (); 1056 | 1057 | public ArgumentEnumerator (IEnumerable arguments) 1058 | { 1059 | sources.Add (arguments.GetEnumerator ()); 1060 | } 1061 | 1062 | public void Add (IEnumerable arguments) 1063 | { 1064 | sources.Add (arguments.GetEnumerator ()); 1065 | } 1066 | 1067 | public IEnumerator GetEnumerator () 1068 | { 1069 | do { 1070 | IEnumerator c = sources [sources.Count-1]; 1071 | if (c.MoveNext ()) 1072 | yield return c.Current; 1073 | else { 1074 | c.Dispose (); 1075 | sources.RemoveAt (sources.Count-1); 1076 | } 1077 | } while (sources.Count > 0); 1078 | } 1079 | 1080 | IEnumerator IEnumerable.GetEnumerator () 1081 | { 1082 | return GetEnumerator (); 1083 | } 1084 | } 1085 | 1086 | bool AddSource (ArgumentEnumerator ae, string argument) 1087 | { 1088 | foreach (ArgumentSource source in sources) { 1089 | IEnumerable replacement; 1090 | if (!source.GetArguments (argument, out replacement)) 1091 | continue; 1092 | ae.Add (replacement); 1093 | return true; 1094 | } 1095 | return false; 1096 | } 1097 | 1098 | private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument) 1099 | { 1100 | if (def == null) { 1101 | extra.Add (argument); 1102 | return false; 1103 | } 1104 | c.OptionValues.Add (argument); 1105 | c.Option = def; 1106 | c.Option.Invoke (c); 1107 | return false; 1108 | } 1109 | 1110 | private readonly Regex ValueOption = new Regex ( 1111 | @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); 1112 | 1113 | protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) 1114 | { 1115 | if (argument == null) 1116 | throw new ArgumentNullException ("argument"); 1117 | 1118 | flag = name = sep = value = null; 1119 | Match m = ValueOption.Match (argument); 1120 | if (!m.Success) { 1121 | return false; 1122 | } 1123 | flag = m.Groups ["flag"].Value; 1124 | name = m.Groups ["name"].Value; 1125 | if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { 1126 | sep = m.Groups ["sep"].Value; 1127 | value = m.Groups ["value"].Value; 1128 | } 1129 | return true; 1130 | } 1131 | 1132 | protected virtual bool Parse (string argument, OptionContext c) 1133 | { 1134 | if (c.Option != null) { 1135 | ParseValue (argument, c); 1136 | return true; 1137 | } 1138 | 1139 | string f, n, s, v; 1140 | if (!GetOptionParts (argument, out f, out n, out s, out v)) 1141 | return false; 1142 | 1143 | Option p; 1144 | if (Contains (n)) { 1145 | p = this [n]; 1146 | c.OptionName = f + n; 1147 | c.Option = p; 1148 | switch (p.OptionValueType) { 1149 | case OptionValueType.None: 1150 | c.OptionValues.Add (n); 1151 | c.Option.Invoke (c); 1152 | break; 1153 | case OptionValueType.Optional: 1154 | case OptionValueType.Required: 1155 | ParseValue (v, c); 1156 | break; 1157 | } 1158 | return true; 1159 | } 1160 | // no match; is it a bool option? 1161 | if (ParseBool (argument, n, c)) 1162 | return true; 1163 | // is it a bundled option? 1164 | if (ParseBundledValue (f, string.Concat (n + s + v), c)) 1165 | return true; 1166 | 1167 | return false; 1168 | } 1169 | 1170 | private void ParseValue (string option, OptionContext c) 1171 | { 1172 | if (option != null) 1173 | foreach (string o in c.Option.ValueSeparators != null 1174 | ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) 1175 | : new string[]{option}) { 1176 | c.OptionValues.Add (o); 1177 | } 1178 | if (c.OptionValues.Count == c.Option.MaxValueCount || 1179 | c.Option.OptionValueType == OptionValueType.Optional) 1180 | c.Option.Invoke (c); 1181 | else if (c.OptionValues.Count > c.Option.MaxValueCount) { 1182 | throw new OptionException (localizer (string.Format ( 1183 | "Error: Found {0} option values when expecting {1}.", 1184 | c.OptionValues.Count, c.Option.MaxValueCount)), 1185 | c.OptionName); 1186 | } 1187 | } 1188 | 1189 | private bool ParseBool (string option, string n, OptionContext c) 1190 | { 1191 | Option p; 1192 | string rn; 1193 | if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && 1194 | Contains ((rn = n.Substring (0, n.Length-1)))) { 1195 | p = this [rn]; 1196 | string v = n [n.Length-1] == '+' ? option : null; 1197 | c.OptionName = option; 1198 | c.Option = p; 1199 | c.OptionValues.Add (v); 1200 | p.Invoke (c); 1201 | return true; 1202 | } 1203 | return false; 1204 | } 1205 | 1206 | private bool ParseBundledValue (string f, string n, OptionContext c) 1207 | { 1208 | if (f != "-") 1209 | return false; 1210 | for (int i = 0; i < n.Length; ++i) { 1211 | Option p; 1212 | string opt = f + n [i].ToString (); 1213 | string rn = n [i].ToString (); 1214 | if (!Contains (rn)) { 1215 | if (i == 0) 1216 | return false; 1217 | throw new OptionException (string.Format (localizer ( 1218 | "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null); 1219 | } 1220 | p = this [rn]; 1221 | switch (p.OptionValueType) { 1222 | case OptionValueType.None: 1223 | Invoke (c, opt, n, p); 1224 | break; 1225 | case OptionValueType.Optional: 1226 | case OptionValueType.Required: { 1227 | string v = n.Substring (i+1); 1228 | c.Option = p; 1229 | c.OptionName = opt; 1230 | ParseValue (v.Length != 0 ? v : null, c); 1231 | return true; 1232 | } 1233 | default: 1234 | throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); 1235 | } 1236 | } 1237 | return true; 1238 | } 1239 | 1240 | private static void Invoke (OptionContext c, string name, string value, Option option) 1241 | { 1242 | c.OptionName = name; 1243 | c.Option = option; 1244 | c.OptionValues.Add (value); 1245 | option.Invoke (c); 1246 | } 1247 | 1248 | private const int OptionWidth = 29; 1249 | private const int Description_FirstWidth = 80 - OptionWidth; 1250 | private const int Description_RemWidth = 80 - OptionWidth - 2; 1251 | 1252 | static readonly string CommandHelpIndentStart = new string (' ', OptionWidth); 1253 | static readonly string CommandHelpIndentRemaining = new string (' ', OptionWidth + 2); 1254 | 1255 | public void WriteOptionDescriptions (TextWriter o) 1256 | { 1257 | foreach (Option p in this) { 1258 | int written = 0; 1259 | 1260 | if (p.Hidden) 1261 | continue; 1262 | 1263 | Category c = p as Category; 1264 | if (c != null) { 1265 | WriteDescription (o, p.Description, "", 80, 80); 1266 | continue; 1267 | } 1268 | CommandOption co = p as CommandOption; 1269 | if (co != null) { 1270 | WriteCommandDescription (o, co.Command, co.CommandName); 1271 | continue; 1272 | } 1273 | 1274 | if (!WriteOptionPrototype (o, p, ref written)) 1275 | continue; 1276 | 1277 | if (written < OptionWidth) 1278 | o.Write (new string (' ', OptionWidth - written)); 1279 | else { 1280 | o.WriteLine (); 1281 | o.Write (new string (' ', OptionWidth)); 1282 | } 1283 | 1284 | WriteDescription (o, p.Description, new string (' ', OptionWidth+2), 1285 | Description_FirstWidth, Description_RemWidth); 1286 | } 1287 | 1288 | foreach (ArgumentSource s in sources) { 1289 | string[] names = s.GetNames (); 1290 | if (names == null || names.Length == 0) 1291 | continue; 1292 | 1293 | int written = 0; 1294 | 1295 | Write (o, ref written, " "); 1296 | Write (o, ref written, names [0]); 1297 | for (int i = 1; i < names.Length; ++i) { 1298 | Write (o, ref written, ", "); 1299 | Write (o, ref written, names [i]); 1300 | } 1301 | 1302 | if (written < OptionWidth) 1303 | o.Write (new string (' ', OptionWidth - written)); 1304 | else { 1305 | o.WriteLine (); 1306 | o.Write (new string (' ', OptionWidth)); 1307 | } 1308 | 1309 | WriteDescription (o, s.Description, new string (' ', OptionWidth+2), 1310 | Description_FirstWidth, Description_RemWidth); 1311 | } 1312 | } 1313 | 1314 | internal void WriteCommandDescription (TextWriter o, Command c, string commandName) 1315 | { 1316 | var name = new string (' ', 8) + (commandName ?? c.Name); 1317 | if (name.Length < OptionWidth - 1) { 1318 | WriteDescription (o, name + new string (' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); 1319 | } else { 1320 | WriteDescription (o, name, "", 80, 80); 1321 | WriteDescription (o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); 1322 | } 1323 | } 1324 | 1325 | void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth) 1326 | { 1327 | bool indent = false; 1328 | foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) { 1329 | if (indent) 1330 | o.Write (prefix); 1331 | o.WriteLine (line); 1332 | indent = true; 1333 | } 1334 | } 1335 | 1336 | bool WriteOptionPrototype (TextWriter o, Option p, ref int written) 1337 | { 1338 | string[] names = p.Names; 1339 | 1340 | int i = GetNextOptionIndex (names, 0); 1341 | if (i == names.Length) 1342 | return false; 1343 | 1344 | if (names [i].Length == 1) { 1345 | Write (o, ref written, " -"); 1346 | Write (o, ref written, names [0]); 1347 | } 1348 | else { 1349 | Write (o, ref written, " --"); 1350 | Write (o, ref written, names [0]); 1351 | } 1352 | 1353 | for ( i = GetNextOptionIndex (names, i+1); 1354 | i < names.Length; i = GetNextOptionIndex (names, i+1)) { 1355 | Write (o, ref written, ", "); 1356 | Write (o, ref written, names [i].Length == 1 ? "-" : "--"); 1357 | Write (o, ref written, names [i]); 1358 | } 1359 | 1360 | if (p.OptionValueType == OptionValueType.Optional || 1361 | p.OptionValueType == OptionValueType.Required) { 1362 | if (p.OptionValueType == OptionValueType.Optional) { 1363 | Write (o, ref written, localizer ("[")); 1364 | } 1365 | Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); 1366 | string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 1367 | ? p.ValueSeparators [0] 1368 | : " "; 1369 | for (int c = 1; c < p.MaxValueCount; ++c) { 1370 | Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); 1371 | } 1372 | if (p.OptionValueType == OptionValueType.Optional) { 1373 | Write (o, ref written, localizer ("]")); 1374 | } 1375 | } 1376 | return true; 1377 | } 1378 | 1379 | static int GetNextOptionIndex (string[] names, int i) 1380 | { 1381 | while (i < names.Length && names [i] == "<>") { 1382 | ++i; 1383 | } 1384 | return i; 1385 | } 1386 | 1387 | static void Write (TextWriter o, ref int n, string s) 1388 | { 1389 | n += s.Length; 1390 | o.Write (s); 1391 | } 1392 | 1393 | static string GetArgumentName (int index, int maxIndex, string description) 1394 | { 1395 | var matches = Regex.Matches (description ?? "", @"(?<=(? 1 1404 | if (maxIndex > 1 && parts.Length == 2 && 1405 | parts[0] == index.ToString (CultureInfo.InvariantCulture)) { 1406 | argName = parts[1]; 1407 | } 1408 | } 1409 | 1410 | if (string.IsNullOrEmpty (argName)) { 1411 | argName = maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); 1412 | } 1413 | return argName; 1414 | } 1415 | 1416 | private static string GetDescription (string description) 1417 | { 1418 | if (description == null) 1419 | return string.Empty; 1420 | StringBuilder sb = new StringBuilder (description.Length); 1421 | int start = -1; 1422 | for (int i = 0; i < description.Length; ++i) { 1423 | switch (description [i]) { 1424 | case '{': 1425 | if (i == start) { 1426 | sb.Append ('{'); 1427 | start = -1; 1428 | } 1429 | else if (start < 0) 1430 | start = i + 1; 1431 | break; 1432 | case '}': 1433 | if (start < 0) { 1434 | if ((i+1) == description.Length || description [i+1] != '}') 1435 | throw new InvalidOperationException ("Invalid option description: " + description); 1436 | ++i; 1437 | sb.Append ("}"); 1438 | } 1439 | else { 1440 | sb.Append (description.Substring (start, i - start)); 1441 | start = -1; 1442 | } 1443 | break; 1444 | case ':': 1445 | if (start < 0) 1446 | goto default; 1447 | start = i + 1; 1448 | break; 1449 | default: 1450 | if (start < 0) 1451 | sb.Append (description [i]); 1452 | break; 1453 | } 1454 | } 1455 | return sb.ToString (); 1456 | } 1457 | 1458 | private static IEnumerable GetLines (string description, int firstWidth, int remWidth) 1459 | { 1460 | return StringCoda.WrappedLines (description, firstWidth, remWidth); 1461 | } 1462 | } 1463 | 1464 | public class Command 1465 | { 1466 | public string Name {get;} 1467 | public string Help {get;} 1468 | 1469 | public OptionSet Options {get; set;} 1470 | public Action> Run {get; set;} 1471 | 1472 | public CommandSet CommandSet {get; internal set;} 1473 | 1474 | public Command (string name, string help = null) 1475 | { 1476 | if (string.IsNullOrEmpty (name)) 1477 | throw new ArgumentNullException (nameof (name)); 1478 | 1479 | Name = NormalizeCommandName (name); 1480 | Help = help; 1481 | } 1482 | 1483 | static string NormalizeCommandName (string name) 1484 | { 1485 | var value = new StringBuilder (name.Length); 1486 | var space = false; 1487 | for (int i = 0; i < name.Length; ++i) { 1488 | if (!char.IsWhiteSpace (name, i)) { 1489 | space = false; 1490 | value.Append (name [i]); 1491 | } 1492 | else if (!space) { 1493 | space = true; 1494 | value.Append (' '); 1495 | } 1496 | } 1497 | return value.ToString (); 1498 | } 1499 | 1500 | public virtual int Invoke (IEnumerable arguments) 1501 | { 1502 | var rest = Options?.Parse (arguments) ?? arguments; 1503 | Run?.Invoke (rest); 1504 | return 0; 1505 | } 1506 | } 1507 | 1508 | class CommandOption : Option 1509 | { 1510 | public Command Command {get;} 1511 | public string CommandName {get;} 1512 | 1513 | // Prototype starts with '=' because this is an invalid prototype 1514 | // (see Option.ParsePrototype(), and thus it'll prevent Category 1515 | // instances from being accidentally used as normal options. 1516 | public CommandOption (Command command, string commandName = null, bool hidden = false) 1517 | : base ("=:Command:= " + (commandName ?? command?.Name), (commandName ?? command?.Name), maxValueCount: 0, hidden: hidden) 1518 | { 1519 | if (command == null) 1520 | throw new ArgumentNullException (nameof (command)); 1521 | Command = command; 1522 | CommandName = commandName ?? command.Name; 1523 | } 1524 | 1525 | protected override void OnParseComplete (OptionContext c) 1526 | { 1527 | throw new NotSupportedException ("CommandOption.OnParseComplete should not be invoked."); 1528 | } 1529 | } 1530 | 1531 | class HelpOption : Option 1532 | { 1533 | Option option; 1534 | CommandSet commands; 1535 | 1536 | public HelpOption (CommandSet commands, Option d) 1537 | : base (d.Prototype, d.Description, d.MaxValueCount, d.Hidden) 1538 | { 1539 | this.commands = commands; 1540 | this.option = d; 1541 | } 1542 | 1543 | protected override void OnParseComplete (OptionContext c) 1544 | { 1545 | commands.showHelp = true; 1546 | 1547 | option?.InvokeOnParseComplete (c); 1548 | } 1549 | } 1550 | 1551 | class CommandOptionSet : OptionSet 1552 | { 1553 | CommandSet commands; 1554 | 1555 | public CommandOptionSet (CommandSet commands, MessageLocalizerConverter localizer) 1556 | : base (localizer) 1557 | { 1558 | this.commands = commands; 1559 | } 1560 | 1561 | protected override void SetItem (int index, Option item) 1562 | { 1563 | if (ShouldWrapOption (item)) { 1564 | base.SetItem (index, new HelpOption (commands, item)); 1565 | return; 1566 | } 1567 | base.SetItem (index, item); 1568 | } 1569 | 1570 | bool ShouldWrapOption (Option item) 1571 | { 1572 | if (item == null) 1573 | return false; 1574 | var help = item as HelpOption; 1575 | if (help != null) 1576 | return false; 1577 | foreach (var n in item.Names) { 1578 | if (n == "help") 1579 | return true; 1580 | } 1581 | return false; 1582 | } 1583 | 1584 | protected override void InsertItem (int index, Option item) 1585 | { 1586 | if (ShouldWrapOption (item)) { 1587 | base.InsertItem (index, new HelpOption (commands, item)); 1588 | return; 1589 | } 1590 | base.InsertItem (index, item); 1591 | } 1592 | } 1593 | 1594 | public class CommandSet : KeyedCollection 1595 | { 1596 | readonly string suite; 1597 | 1598 | OptionSet options; 1599 | TextWriter outWriter; 1600 | TextWriter errorWriter; 1601 | 1602 | internal List NestedCommandSets; 1603 | 1604 | internal HelpCommand help; 1605 | 1606 | internal bool showHelp; 1607 | 1608 | internal OptionSet Options => options; 1609 | 1610 | #if !PCL || NETSTANDARD1_3 1611 | public CommandSet(string suite, MessageLocalizerConverter localizer = null) 1612 | : this(suite, Console.Out, Console.Error, localizer) 1613 | { 1614 | } 1615 | #endif 1616 | 1617 | public CommandSet (string suite, TextWriter output, TextWriter error, MessageLocalizerConverter localizer = null) 1618 | { 1619 | if (suite == null) 1620 | throw new ArgumentNullException (nameof (suite)); 1621 | if (output == null) 1622 | throw new ArgumentNullException (nameof (output)); 1623 | if (error == null) 1624 | throw new ArgumentNullException (nameof (error)); 1625 | 1626 | this.suite = suite; 1627 | options = new CommandOptionSet (this, localizer); 1628 | outWriter = output; 1629 | errorWriter = error; 1630 | } 1631 | 1632 | public string Suite => suite; 1633 | public TextWriter Out => outWriter; 1634 | public TextWriter Error => errorWriter; 1635 | public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer; 1636 | 1637 | protected override string GetKeyForItem (Command item) 1638 | { 1639 | return item?.Name; 1640 | } 1641 | 1642 | public new CommandSet Add (Command value) 1643 | { 1644 | if (value == null) 1645 | throw new ArgumentNullException (nameof (value)); 1646 | AddCommand (value); 1647 | options.Add (new CommandOption (value)); 1648 | return this; 1649 | } 1650 | 1651 | void AddCommand (Command value) 1652 | { 1653 | if (value.CommandSet != null && value.CommandSet != this) { 1654 | throw new ArgumentException ("Command instances can only be added to a single CommandSet.", nameof (value)); 1655 | } 1656 | value.CommandSet = this; 1657 | if (value.Options != null) { 1658 | value.Options.MessageLocalizer = options.MessageLocalizer; 1659 | } 1660 | 1661 | base.Add (value); 1662 | 1663 | help = help ?? value as HelpCommand; 1664 | } 1665 | 1666 | public CommandSet Add (string header) 1667 | { 1668 | options.Add (header); 1669 | return this; 1670 | } 1671 | 1672 | public CommandSet Add (Option option) 1673 | { 1674 | options.Add (option); 1675 | return this; 1676 | } 1677 | 1678 | public CommandSet Add (string prototype, Action action) 1679 | { 1680 | options.Add (prototype, action); 1681 | return this; 1682 | } 1683 | 1684 | public CommandSet Add (string prototype, string description, Action action) 1685 | { 1686 | options.Add (prototype, description, action); 1687 | return this; 1688 | } 1689 | 1690 | public CommandSet Add (string prototype, string description, Action action, bool hidden) 1691 | { 1692 | options.Add (prototype, description, action, hidden); 1693 | return this; 1694 | } 1695 | 1696 | public CommandSet Add (string prototype, OptionAction action) 1697 | { 1698 | options.Add (prototype, action); 1699 | return this; 1700 | } 1701 | 1702 | public CommandSet Add (string prototype, string description, OptionAction action) 1703 | { 1704 | options.Add (prototype, description, action); 1705 | return this; 1706 | } 1707 | 1708 | public CommandSet Add (string prototype, string description, OptionAction action, bool hidden) 1709 | { 1710 | options.Add (prototype, description, action, hidden); 1711 | return this; 1712 | } 1713 | 1714 | public CommandSet Add (string prototype, Action action) 1715 | { 1716 | options.Add (prototype, null, action); 1717 | return this; 1718 | } 1719 | 1720 | public CommandSet Add (string prototype, string description, Action action) 1721 | { 1722 | options.Add (prototype, description, action); 1723 | return this; 1724 | } 1725 | 1726 | public CommandSet Add (string prototype, OptionAction action) 1727 | { 1728 | options.Add (prototype, action); 1729 | return this; 1730 | } 1731 | 1732 | public CommandSet Add (string prototype, string description, OptionAction action) 1733 | { 1734 | options.Add (prototype, description, action); 1735 | return this; 1736 | } 1737 | 1738 | public CommandSet Add (ArgumentSource source) 1739 | { 1740 | options.Add (source); 1741 | return this; 1742 | } 1743 | 1744 | public CommandSet Add (CommandSet nestedCommands) 1745 | { 1746 | if (nestedCommands == null) 1747 | throw new ArgumentNullException (nameof (nestedCommands)); 1748 | 1749 | if (NestedCommandSets == null) { 1750 | NestedCommandSets = new List (); 1751 | } 1752 | 1753 | if (!AlreadyAdded (nestedCommands)) { 1754 | NestedCommandSets.Add (nestedCommands); 1755 | foreach (var o in nestedCommands.options) { 1756 | if (o is CommandOption c) { 1757 | options.Add (new CommandOption (c.Command, $"{nestedCommands.Suite} {c.CommandName}")); 1758 | } 1759 | else { 1760 | options.Add (o); 1761 | } 1762 | } 1763 | } 1764 | 1765 | nestedCommands.options = this.options; 1766 | nestedCommands.outWriter = this.outWriter; 1767 | nestedCommands.errorWriter = this.errorWriter; 1768 | 1769 | return this; 1770 | } 1771 | 1772 | bool AlreadyAdded (CommandSet value) 1773 | { 1774 | if (value == this) 1775 | return true; 1776 | if (NestedCommandSets == null) 1777 | return false; 1778 | foreach (var nc in NestedCommandSets) { 1779 | if (nc.AlreadyAdded (value)) 1780 | return true; 1781 | } 1782 | return false; 1783 | } 1784 | 1785 | public IEnumerable GetCompletions (string prefix = null) 1786 | { 1787 | string rest; 1788 | ExtractToken (ref prefix, out rest); 1789 | 1790 | foreach (var command in this) { 1791 | if (command.Name.StartsWith (prefix, StringComparison.OrdinalIgnoreCase)) { 1792 | yield return command.Name; 1793 | } 1794 | } 1795 | 1796 | if (NestedCommandSets == null) 1797 | yield break; 1798 | 1799 | foreach (var subset in NestedCommandSets) { 1800 | if (subset.Suite.StartsWith (prefix, StringComparison.OrdinalIgnoreCase)) { 1801 | foreach (var c in subset.GetCompletions (rest)) { 1802 | yield return $"{subset.Suite} {c}"; 1803 | } 1804 | } 1805 | } 1806 | } 1807 | 1808 | static void ExtractToken (ref string input, out string rest) 1809 | { 1810 | rest = ""; 1811 | input = input ?? ""; 1812 | 1813 | int top = input.Length; 1814 | for (int i = 0; i < top; i++) { 1815 | if (char.IsWhiteSpace (input [i])) 1816 | continue; 1817 | 1818 | for (int j = i; j < top; j++) { 1819 | if (char.IsWhiteSpace (input [j])) { 1820 | rest = input.Substring (j).Trim (); 1821 | input = input.Substring (i, j).Trim (); 1822 | return; 1823 | } 1824 | } 1825 | rest = ""; 1826 | if (i != 0) 1827 | input = input.Substring (i).Trim (); 1828 | return; 1829 | } 1830 | } 1831 | 1832 | public int Run (IEnumerable arguments) 1833 | { 1834 | if (arguments == null) 1835 | throw new ArgumentNullException (nameof (arguments)); 1836 | 1837 | this.showHelp = false; 1838 | if (help == null) { 1839 | help = new HelpCommand (); 1840 | AddCommand (help); 1841 | } 1842 | Action setHelp = v => showHelp = v != null; 1843 | if (!options.Contains ("help")) { 1844 | options.Add ("help", "", setHelp, hidden: true); 1845 | } 1846 | if (!options.Contains ("?")) { 1847 | options.Add ("?", "", setHelp, hidden: true); 1848 | } 1849 | var extra = options.Parse (arguments); 1850 | if (extra.Count == 0) { 1851 | if (showHelp) { 1852 | return help.Invoke (extra); 1853 | } 1854 | Out.WriteLine (options.MessageLocalizer ($"Use `{Suite} help` for usage.")); 1855 | return 1; 1856 | } 1857 | var command = GetCommand (extra); 1858 | if (command == null) { 1859 | help.WriteUnknownCommand (extra [0]); 1860 | return 1; 1861 | } 1862 | if (showHelp) { 1863 | if (command.Options?.Contains ("help") ?? true) { 1864 | extra.Add ("--help"); 1865 | return command.Invoke (extra); 1866 | } 1867 | command.Options.WriteOptionDescriptions (Out); 1868 | return 0; 1869 | } 1870 | return command.Invoke (extra); 1871 | } 1872 | 1873 | internal Command GetCommand (List extra) 1874 | { 1875 | return TryGetLocalCommand (extra) ?? TryGetNestedCommand (extra); 1876 | } 1877 | 1878 | Command TryGetLocalCommand (List extra) 1879 | { 1880 | var name = extra [0]; 1881 | if (Contains (name)) { 1882 | extra.RemoveAt (0); 1883 | return this [name]; 1884 | } 1885 | for (int i = 1; i < extra.Count; ++i) { 1886 | name = name + " " + extra [i]; 1887 | if (!Contains (name)) 1888 | continue; 1889 | extra.RemoveRange (0, i+1); 1890 | return this [name]; 1891 | } 1892 | return null; 1893 | } 1894 | 1895 | Command TryGetNestedCommand (List extra) 1896 | { 1897 | if (NestedCommandSets == null) 1898 | return null; 1899 | 1900 | var nestedCommands = NestedCommandSets.Find (c => c.Suite == extra [0]); 1901 | if (nestedCommands == null) 1902 | return null; 1903 | 1904 | var extraCopy = new List (extra); 1905 | extraCopy.RemoveAt (0); 1906 | if (extraCopy.Count == 0) 1907 | return null; 1908 | 1909 | var command = nestedCommands.GetCommand (extraCopy); 1910 | if (command != null) { 1911 | extra.Clear (); 1912 | extra.AddRange (extraCopy); 1913 | return command; 1914 | } 1915 | return null; 1916 | } 1917 | } 1918 | 1919 | public class HelpCommand : Command 1920 | { 1921 | public HelpCommand () 1922 | : base ("help", help: "Show this message and exit") 1923 | { 1924 | } 1925 | 1926 | public override int Invoke (IEnumerable arguments) 1927 | { 1928 | var extra = new List (arguments ?? new string [0]); 1929 | var _ = CommandSet.Options.MessageLocalizer; 1930 | if (extra.Count == 0) { 1931 | CommandSet.Options.WriteOptionDescriptions (CommandSet.Out); 1932 | return 0; 1933 | } 1934 | var command = CommandSet.GetCommand (extra); 1935 | if (command == this || extra.Contains ("--help")) { 1936 | CommandSet.Out.WriteLine (_ ($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]")); 1937 | CommandSet.Out.WriteLine (_ ($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command.")); 1938 | CommandSet.Out.WriteLine (); 1939 | CommandSet.Out.WriteLine (_ ($"Available commands:")); 1940 | CommandSet.Out.WriteLine (); 1941 | var commands = GetCommands (); 1942 | commands.Sort ((x, y) => string.Compare (x.Key, y.Key, StringComparison.OrdinalIgnoreCase)); 1943 | foreach (var c in commands) { 1944 | if (c.Key == "help") { 1945 | continue; 1946 | } 1947 | CommandSet.Options.WriteCommandDescription (CommandSet.Out, c.Value, c.Key); 1948 | } 1949 | CommandSet.Options.WriteCommandDescription (CommandSet.Out, CommandSet.help, "help"); 1950 | return 0; 1951 | } 1952 | if (command == null) { 1953 | WriteUnknownCommand (extra [0]); 1954 | return 1; 1955 | } 1956 | if (command.Options != null) { 1957 | command.Options.WriteOptionDescriptions (CommandSet.Out); 1958 | return 0; 1959 | } 1960 | return command.Invoke (new [] { "--help" }); 1961 | } 1962 | 1963 | List> GetCommands () 1964 | { 1965 | var commands = new List> (); 1966 | 1967 | foreach (var c in CommandSet) { 1968 | commands.Add (new KeyValuePair(c.Name, c)); 1969 | } 1970 | 1971 | if (CommandSet.NestedCommandSets == null) 1972 | return commands; 1973 | 1974 | foreach (var nc in CommandSet.NestedCommandSets) { 1975 | AddNestedCommands (commands, "", nc); 1976 | } 1977 | 1978 | return commands; 1979 | } 1980 | 1981 | void AddNestedCommands (List> commands, string outer, CommandSet value) 1982 | { 1983 | foreach (var v in value) { 1984 | commands.Add (new KeyValuePair($"{outer}{value.Suite} {v.Name}", v)); 1985 | } 1986 | if (value.NestedCommandSets == null) 1987 | return; 1988 | foreach (var nc in value.NestedCommandSets) { 1989 | AddNestedCommands (commands, $"{outer}{value.Suite} ", nc); 1990 | } 1991 | } 1992 | 1993 | internal void WriteUnknownCommand (string unknownCommand) 1994 | { 1995 | CommandSet.Error.WriteLine (CommandSet.Options.MessageLocalizer ($"{CommandSet.Suite}: Unknown command: {unknownCommand}")); 1996 | CommandSet.Error.WriteLine (CommandSet.Options.MessageLocalizer ($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage.")); 1997 | } 1998 | } 1999 | } 2000 | 2001 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Network.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace SoftwareAuthKeyLoader 10 | { 11 | internal static class Network 12 | { 13 | public static IPAddress ParseIpAddress(string ipAddress) 14 | { 15 | return IPAddress.Parse(ipAddress); 16 | } 17 | 18 | public static int ParseUdpPort(string udpPort) 19 | { 20 | int udpPortNumber = int.Parse(udpPort); 21 | 22 | if (udpPortNumber >= 1 && udpPortNumber <= 65535) 23 | { 24 | return udpPortNumber; 25 | } 26 | else 27 | { 28 | throw new ArgumentOutOfRangeException("udpPortNumber"); 29 | } 30 | } 31 | 32 | public static int ParseTimeout(string timeout) 33 | { 34 | int timeoutValue = int.Parse(timeout); 35 | 36 | if (timeoutValue > 0) 37 | { 38 | return timeoutValue; 39 | } 40 | else 41 | { 42 | throw new ArgumentOutOfRangeException("timeout"); 43 | } 44 | } 45 | 46 | public static byte[] QueryRadio(byte[] toRadio) 47 | { 48 | string ipAddress = Settings.IpAddress.ToString(); 49 | int udpPort = Settings.UdpPort; 50 | int timeout = Settings.Timeout; 51 | Output.DebugLine("ip address: {0}, udp port: {1}, receive timeout: {2}", ipAddress, udpPort, timeout); 52 | using (UdpClient udpClient = new UdpClient(ipAddress, udpPort)) 53 | { 54 | Output.DebugLine("sending {0} bytes to radio - {1}", toRadio.Length, BitConverter.ToString(toRadio)); 55 | udpClient.Client.ReceiveTimeout = timeout; 56 | udpClient.Send(toRadio, toRadio.Length); 57 | IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); 58 | byte[] fromRadio = udpClient.Receive(ref remoteEndPoint); 59 | Output.DebugLine("received {0} bytes from radio - {1}", fromRadio.Length, BitConverter.ToString(fromRadio)); 60 | return fromRadio; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Output.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SoftwareAuthKeyLoader 8 | { 9 | internal class Output 10 | { 11 | public enum Level 12 | { 13 | None, 14 | Error, 15 | Info, 16 | Debug 17 | } 18 | 19 | public static void Write(Level level, string format, params object[] args) 20 | { 21 | if (Settings.OutputLevel >= level) 22 | { 23 | Console.Write(format, args); 24 | } 25 | } 26 | 27 | public static void WriteLine(Level level) 28 | { 29 | if (Settings.OutputLevel >= level) 30 | { 31 | Console.WriteLine(); 32 | } 33 | } 34 | 35 | public static void WriteLine(Level level, string format, params object[] args) 36 | { 37 | if (Settings.OutputLevel >= level) 38 | { 39 | Console.WriteLine(format, args); 40 | } 41 | } 42 | 43 | public static void Error(string format, params object[] args) 44 | { 45 | Write(Level.Error, format, args); 46 | } 47 | 48 | public static void ErrorLine(string format, params object[] args) 49 | { 50 | WriteLine(Level.Error, format, args); 51 | } 52 | 53 | public static void ErrorLine() 54 | { 55 | WriteLine(Level.Error); 56 | } 57 | 58 | public static void Info(string format, params object[] args) 59 | { 60 | Write(Level.Info, format, args); 61 | } 62 | 63 | public static void InfoLine(string format, params object[] args) 64 | { 65 | WriteLine(Level.Info, format, args); 66 | } 67 | 68 | public static void InfoLine() 69 | { 70 | WriteLine(Level.Info); 71 | } 72 | 73 | public static void Debug(string format, params object[] args) 74 | { 75 | Write(Level.Debug, format, args); 76 | } 77 | 78 | public static void DebugLine(string format, params object[] args) 79 | { 80 | WriteLine(Level.Debug, format, args); 81 | } 82 | 83 | public static void DebugLine() 84 | { 85 | WriteLine(Level.Debug); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Program.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace SoftwareAuthKeyLoader 11 | { 12 | class Program 13 | { 14 | static int Main(string[] args) 15 | { 16 | bool help = false; 17 | bool quiet = false; 18 | bool verbose = false; 19 | string ip = "192.168.128.1"; 20 | string port = "49165"; 21 | string timeout = "5000"; 22 | bool load = false; 23 | bool zeroize = false; 24 | bool read = false; 25 | bool device = false; 26 | bool active = false; 27 | bool named = false; 28 | string wacn = string.Empty; 29 | string system = string.Empty; 30 | string unit = string.Empty; 31 | string key = string.Empty; 32 | 33 | Settings.OutputLevel = Output.Level.Info; // default to info to show command line option parsing errors 34 | 35 | OptionSet commandLineOptions = new OptionSet 36 | { 37 | { "h|?|help", "show this message and exit", h => help = h != null }, 38 | { "q|quiet", "do not show output", q => quiet = q != null }, 39 | { "v|verbose", "show debug messages", v => verbose = v != null }, 40 | { "i=|ip=", "radio ip address [default 192.168.128.1]", i => ip = i }, 41 | { "p=|port=", "radio udp port number [default 49165]", p => port = p }, 42 | { "t=|timeout=", "radio receive timeout (ms) [default 5000]", t => timeout = t }, 43 | { "l|load", "load key", l => load = l != null }, 44 | { "z|zeroize", "zeroize key(s)", z => zeroize = z != null }, 45 | { "r|read", "read key(s)", r => read = r != null }, 46 | { "d|device", "device scope", d => device = d != null }, 47 | { "a|active", "active scope", a => active = a != null }, 48 | { "n|named", "named scope", n => named = n != null }, 49 | { "w=|wacn=", "wacn id (hex)", w => wacn = w }, 50 | { "s=|system=", "system id (hex)", s => system = s }, 51 | { "u=|unit=", "unit id (hex)", u => unit = u }, 52 | { "k=|key=", "aes-128 encryption key (hex)", k => key = k } 53 | }; 54 | 55 | try 56 | { 57 | commandLineOptions.Parse(args); 58 | } 59 | catch (OptionException ex) 60 | { 61 | Output.ErrorLine(ex.Message); 62 | ExitPrompt(); 63 | return -1; 64 | } 65 | 66 | if (quiet && verbose) 67 | { 68 | Output.ErrorLine("quiet and verbose both specified"); 69 | ExitPrompt(); 70 | return -1; 71 | } 72 | else if (quiet) 73 | { 74 | Settings.OutputLevel = Output.Level.None; 75 | } 76 | else if (verbose) 77 | { 78 | Settings.OutputLevel = Output.Level.Debug; 79 | } 80 | else 81 | { 82 | Settings.OutputLevel = Output.Level.Info; 83 | } 84 | 85 | Output.InfoLine("Software P25 Link Layer Authentication Key Loader"); 86 | Output.InfoLine("Supports Manual Rekeying Features for Authentication per TIA-102.AACD-A"); 87 | Output.InfoLine("Copyright 2019 Daniel Dugger"); 88 | Output.InfoLine("Version: {0}", Settings.ApplicationVersion); 89 | Output.InfoLine("*** NOT FOR PRODUCTION USE ***"); 90 | Output.InfoLine(); 91 | 92 | if (help || args.Length == 0) 93 | { 94 | ShowHelp(commandLineOptions); 95 | ExitPrompt(); 96 | return 0; 97 | } 98 | 99 | Output.DebugLine("help: {0}", help); 100 | Output.DebugLine("quiet: {0}", quiet); 101 | Output.DebugLine("verbose: {0}", verbose); 102 | Output.DebugLine("ip: {0}", ip); 103 | Output.DebugLine("port: {0}", port); 104 | Output.DebugLine("timeout: {0}", timeout); 105 | Output.DebugLine("load: {0}", load); 106 | Output.DebugLine("zeroize: {0}", zeroize); 107 | Output.DebugLine("read: {0}", read); 108 | Output.DebugLine("device: {0}", device); 109 | Output.DebugLine("active: {0}", active); 110 | Output.DebugLine("named: {0}", named); 111 | Output.DebugLine("wacn: {0}", wacn); 112 | Output.DebugLine("system: {0}", system); 113 | Output.DebugLine("unit: {0}", unit); 114 | Output.DebugLine("key: {0}", key); 115 | 116 | try 117 | { 118 | Settings.IpAddress = Network.ParseIpAddress(ip); 119 | } 120 | catch (Exception ex) 121 | { 122 | Output.ErrorLine("ip invalid: {0}", ex.Message); 123 | ExitPrompt(); 124 | return -1; 125 | } 126 | 127 | try 128 | { 129 | Settings.UdpPort = Network.ParseUdpPort(port); 130 | } 131 | catch (Exception ex) 132 | { 133 | Output.ErrorLine("port invalid: {0}", ex.Message); 134 | ExitPrompt(); 135 | return -1; 136 | } 137 | 138 | try 139 | { 140 | Settings.Timeout = Network.ParseTimeout(timeout); 141 | } 142 | catch (Exception ex) 143 | { 144 | Output.ErrorLine("timeout invalid: {0}", ex.Message); 145 | ExitPrompt(); 146 | return -1; 147 | } 148 | 149 | if ((load && zeroize) || (load && read) || (zeroize && read) || (load && zeroize && read)) 150 | { 151 | Output.ErrorLine("multiple actions specified"); 152 | ExitPrompt(); 153 | return -1; 154 | } 155 | 156 | if ((device && active) || (device && named) || (active && named) || (device && active && named)) 157 | { 158 | Output.ErrorLine("multiple scopes specified"); 159 | ExitPrompt(); 160 | return -1; 161 | } 162 | 163 | byte[] keyData = new byte[0]; 164 | 165 | if (load) 166 | { 167 | if (key.Equals(string.Empty)) 168 | { 169 | Output.ErrorLine("key missing"); 170 | ExitPrompt(); 171 | return -1; 172 | } 173 | 174 | if (!OnlyContainsHexCharacters(key)) 175 | { 176 | Output.ErrorLine("key invalid: contains character(s) other than [0-9] [a-f] [A-F]"); 177 | ExitPrompt(); 178 | return -1; 179 | } 180 | 181 | if (key.Length != 32) 182 | { 183 | Output.ErrorLine("key invalid: expected 32 characters, got {0}", key.Length); 184 | ExitPrompt(); 185 | return -1; 186 | } 187 | 188 | try 189 | { 190 | keyData = ByteStringToByteArray(key); 191 | } 192 | catch (Exception ex) 193 | { 194 | Output.ErrorLine("key invalid: {0}", ex.Message); 195 | ExitPrompt(); 196 | return -1; 197 | } 198 | } 199 | 200 | int wacnId = 0; 201 | int systemId = 0; 202 | int unitId = 0; 203 | 204 | if (named) 205 | { 206 | if (wacn.Equals(string.Empty)) 207 | { 208 | Output.ErrorLine("wacn missing"); 209 | ExitPrompt(); 210 | return -1; 211 | } 212 | 213 | if (!OnlyContainsHexCharacters(wacn)) 214 | { 215 | Output.ErrorLine("wacn invalid: contains character(s) other than [0-9] [a-f] [A-F]"); 216 | ExitPrompt(); 217 | return -1; 218 | } 219 | 220 | if (wacn.Length > 5) 221 | { 222 | Output.ErrorLine("wacn invalid: expected max 5 characters, got {0}", key.Length); 223 | ExitPrompt(); 224 | return -1; 225 | } 226 | 227 | try 228 | { 229 | wacnId = int.Parse(wacn, NumberStyles.HexNumber); 230 | } 231 | catch (Exception ex) 232 | { 233 | Output.ErrorLine("wacn invalid: {0}", ex.Message); 234 | ExitPrompt(); 235 | return -1; 236 | } 237 | 238 | if (system.Equals(string.Empty)) 239 | { 240 | Output.ErrorLine("system missing"); 241 | ExitPrompt(); 242 | return -1; 243 | } 244 | 245 | if (!OnlyContainsHexCharacters(system)) 246 | { 247 | Output.ErrorLine("system invalid: contains character(s) other than [0-9] [a-f] [A-F]"); 248 | ExitPrompt(); 249 | return -1; 250 | } 251 | 252 | if (system.Length > 3) 253 | { 254 | Output.ErrorLine("system invalid: expected max 3 characters, got {0}", key.Length); 255 | ExitPrompt(); 256 | return -1; 257 | } 258 | 259 | try 260 | { 261 | systemId = int.Parse(system, NumberStyles.HexNumber); 262 | } 263 | catch (Exception ex) 264 | { 265 | Output.ErrorLine("system invalid: {0}", ex.Message); 266 | ExitPrompt(); 267 | return -1; 268 | } 269 | 270 | if (unit.Equals(string.Empty)) 271 | { 272 | Output.ErrorLine("unit missing"); 273 | ExitPrompt(); 274 | return -1; 275 | } 276 | 277 | if (!OnlyContainsHexCharacters(unit)) 278 | { 279 | Output.ErrorLine("unit invalid: contains character(s) other than [0-9] [a-f] [A-F]"); 280 | ExitPrompt(); 281 | return -1; 282 | } 283 | 284 | if (unit.Length > 6) 285 | { 286 | Output.ErrorLine("unit invalid: expected max 6 characters, got {0}", key.Length); 287 | ExitPrompt(); 288 | return -1; 289 | } 290 | 291 | try 292 | { 293 | unitId = int.Parse(unit, NumberStyles.HexNumber); 294 | } 295 | catch (Exception ex) 296 | { 297 | Output.ErrorLine("unit invalid: {0}", ex.Message); 298 | ExitPrompt(); 299 | return -1; 300 | } 301 | } 302 | 303 | if (load) 304 | { 305 | if (device) 306 | { 307 | Output.ErrorLine("device scope not supported for load action"); 308 | ExitPrompt(); 309 | return -1; 310 | } 311 | else if (active) 312 | { 313 | int result = -1; 314 | 315 | try 316 | { 317 | result = Actions.LoadAuthenticationKey(false, 0, 0, 0, keyData); 318 | } 319 | catch (Exception ex) 320 | { 321 | Output.ErrorLine("error during load operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 322 | ExitPrompt(); 323 | return -1; 324 | } 325 | 326 | if (result == 0) 327 | { 328 | Output.InfoLine("Loaded authentication key successfully"); 329 | } 330 | else 331 | { 332 | Output.ErrorLine("Error while loading authentication key"); 333 | } 334 | 335 | ExitPrompt(); 336 | return result; 337 | } 338 | else if (named) 339 | { 340 | int result = -1; 341 | 342 | try 343 | { 344 | result = Actions.LoadAuthenticationKey(true, wacnId, systemId, unitId, keyData); 345 | 346 | } 347 | catch (Exception ex) 348 | { 349 | Output.ErrorLine("error during load operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 350 | ExitPrompt(); 351 | return -1; 352 | } 353 | 354 | if (result == 0) 355 | { 356 | Output.InfoLine("Loaded authentication key successfully"); 357 | } 358 | else 359 | { 360 | Output.ErrorLine("Error while loading authentication key"); 361 | } 362 | 363 | ExitPrompt(); 364 | return result; 365 | } 366 | else 367 | { 368 | Output.ErrorLine("scope missing (only active and named scopes are supported for load action)"); 369 | ExitPrompt(); 370 | return -1; 371 | } 372 | } 373 | else if (zeroize) 374 | { 375 | if (device) 376 | { 377 | int result = -1; 378 | 379 | try 380 | { 381 | result = Actions.DeleteAuthenticationKey(false, true, 0, 0, 0); 382 | } 383 | catch (Exception ex) 384 | { 385 | Output.ErrorLine("error during zeroize operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 386 | ExitPrompt(); 387 | return -1; 388 | } 389 | 390 | if (result == 0) 391 | { 392 | Output.InfoLine("Zeroized all authentication keys successfully"); 393 | } 394 | else 395 | { 396 | Output.ErrorLine("Error while zeroizing all authentication keys"); 397 | } 398 | 399 | ExitPrompt(); 400 | return result; 401 | } 402 | else if (active) 403 | { 404 | int result = -1; 405 | 406 | try 407 | { 408 | result = Actions.DeleteAuthenticationKey(false, false, 0, 0, 0); 409 | } 410 | catch (Exception ex) 411 | { 412 | Output.ErrorLine("error during zeroize active operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 413 | ExitPrompt(); 414 | return -1; 415 | } 416 | 417 | if (result == 0) 418 | { 419 | Output.InfoLine("Zeroized active authentication key successfully"); 420 | } 421 | else 422 | { 423 | Output.ErrorLine("Error while zeroizing active authentication key"); 424 | } 425 | 426 | ExitPrompt(); 427 | return result; 428 | } 429 | else if (named) 430 | { 431 | int result = -1; 432 | 433 | try 434 | { 435 | result = Actions.DeleteAuthenticationKey(true, false, wacnId, systemId, unitId); 436 | } 437 | catch (Exception ex) 438 | { 439 | Output.ErrorLine("error during zeroize named operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 440 | ExitPrompt(); 441 | return -1; 442 | } 443 | 444 | if (result == 0) 445 | { 446 | Output.InfoLine("Zeroized named authentication key successfully"); 447 | } 448 | else 449 | { 450 | Output.ErrorLine("Error while zeroizing named authentication key"); 451 | } 452 | 453 | ExitPrompt(); 454 | return result; 455 | } 456 | else 457 | { 458 | Output.ErrorLine("scope missing"); 459 | ExitPrompt(); 460 | return -1; 461 | } 462 | } 463 | else if (read) 464 | { 465 | if (device) 466 | { 467 | int result = -1; 468 | 469 | try 470 | { 471 | result = Actions.ListSuIdItems(); 472 | } 473 | catch (Exception ex) 474 | { 475 | Output.ErrorLine("error during read device operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 476 | ExitPrompt(); 477 | return -1; 478 | } 479 | 480 | if (result == 0) 481 | { 482 | Output.InfoLine("Read device authentication key(s) successfully"); 483 | } 484 | else 485 | { 486 | Output.ErrorLine("Error while reading device authentication key(s)"); 487 | } 488 | 489 | ExitPrompt(); 490 | return result; 491 | } 492 | else if (active) 493 | { 494 | int result = -1; 495 | 496 | try 497 | { 498 | result = Actions.ListActiveSuId(); 499 | } 500 | catch (Exception ex) 501 | { 502 | Output.ErrorLine("error during read active operation: {0}\r\n{1}\r\n{2}", ex.Message, ex.TargetSite, ex.StackTrace); 503 | ExitPrompt(); 504 | return -1; 505 | } 506 | 507 | if (result == 0) 508 | { 509 | Output.InfoLine("Read active authentication key successfully"); 510 | } 511 | else 512 | { 513 | Output.ErrorLine("Error while reading active authentication key"); 514 | } 515 | 516 | ExitPrompt(); 517 | return result; 518 | } 519 | else if (named) 520 | { 521 | Output.ErrorLine("named scope not supported for read action"); 522 | ExitPrompt(); 523 | return -1; 524 | } 525 | else 526 | { 527 | Output.ErrorLine("scope missing (only device and active scopes are supported for read action)"); 528 | ExitPrompt(); 529 | return -1; 530 | } 531 | } 532 | else 533 | { 534 | Output.ErrorLine("action missing"); 535 | ExitPrompt(); 536 | return -1; 537 | } 538 | } 539 | 540 | private static bool OnlyContainsHexCharacters(string input) 541 | { 542 | return Regex.IsMatch(input, @"\A\b[0-9a-fA-F]+\b\Z"); 543 | } 544 | 545 | private static byte[] ByteStringToByteArray(string hex) 546 | { 547 | int NumberChars = hex.Length; 548 | byte[] bytes = new byte[NumberChars / 2]; 549 | for (int i = 0; i < NumberChars; i += 2) 550 | { 551 | bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); 552 | } 553 | return bytes; 554 | } 555 | 556 | private static void ExitPrompt() 557 | { 558 | Output.InfoLine("Exiting..."); 559 | //Output.InfoLine("Press any key to exit..."); 560 | //Console.ReadKey(); 561 | } 562 | 563 | private static void ShowHelp(OptionSet optionSet) 564 | { 565 | Output.InfoLine("Usage: sakl.exe [OPTIONS]"); 566 | Output.InfoLine(); 567 | 568 | Output.InfoLine("Options:"); 569 | if (Settings.OutputLevel >= Output.Level.Info) 570 | { 571 | optionSet.WriteOptionDescriptions(Console.Out); 572 | } 573 | Output.InfoLine(); 574 | 575 | Output.InfoLine("Examples:"); 576 | Output.InfoLine(" load key to the active suid"); 577 | Output.InfoLine(" /load /active /key 000102030405060708090a0b0c0d0e0f"); 578 | Output.InfoLine(); 579 | Output.InfoLine(" load key to the specified suid"); 580 | Output.InfoLine(" /load /named /wacn a4398 /system f10 /unit 99b584 /key 000102030405060708090a0b0c0d0e0f"); 581 | Output.InfoLine(); 582 | Output.InfoLine(" zeroize all keys"); 583 | Output.InfoLine(" /zeroize /device"); 584 | Output.InfoLine(); 585 | Output.InfoLine(" zeroize active key"); 586 | Output.InfoLine(" /zeroize /active"); 587 | Output.InfoLine(); 588 | Output.InfoLine(" zeroize specified key"); 589 | Output.InfoLine(" /zeroize /named /wacn a4398 /system f10 /unit 99b584"); 590 | Output.InfoLine(); 591 | Output.InfoLine(" read all keys"); 592 | Output.InfoLine(" /read /device"); 593 | Output.InfoLine(); 594 | Output.InfoLine(" read active key"); 595 | Output.InfoLine(" /read /active"); 596 | Output.InfoLine(); 597 | } 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SoftwareAuthKeyLoader")] 9 | [assembly: AssemblyDescription("Software P25 Radio Authentication Key Loader")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SoftwareAuthKeyLoader")] 13 | [assembly: AssemblyCopyright("Copyright 2019 Daniel Dugger")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("842d4e38-b6e6-4ce8-8b99-0ad5a65596c1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace SoftwareAuthKeyLoader 10 | { 11 | internal class Settings 12 | { 13 | public static string ApplicationVersion { get; private set; } 14 | 15 | public static string ApplicationTarget { get; private set; } 16 | 17 | public static Output.Level OutputLevel { get; set; } 18 | 19 | public static IPAddress IpAddress { get; set; } 20 | 21 | public static int UdpPort { get; set; } 22 | 23 | public static int Timeout { get; set; } 24 | 25 | static Settings() 26 | { 27 | ApplicationVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SoftwareAuthKeyLoader/SoftwareAuthKeyLoader.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {842D4E38-B6E6-4CE8-8B99-0AD5A65596C1} 8 | Exe 9 | SoftwareAuthKeyLoader 10 | SoftwareAuthKeyLoader 11 | v4.6.1 12 | 512 13 | true 14 | true 15 | sakl 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | false 27 | 28 | 29 | AnyCPU 30 | none 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | --------------------------------------------------------------------------------