├── .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 |
--------------------------------------------------------------------------------