├── .gitattributes
├── .github
└── workflows
│ ├── dotnet.yml
│ └── publish-nuget_release.yml
├── .gitignore
├── CHANGE-LOG.md
├── LICENSE
├── README.md
├── benchmarks
├── ChaCha20Cipher.cs
├── Program.cs
├── Util.cs
└── benchmarks.csproj
├── create-nuget-debug.ps1
├── create-nuget-release.ps1
├── create-nuget-release.sh
├── docs
├── .gitignore
├── api
│ ├── .gitignore
│ └── index.md
├── docfx.json
├── index.md
└── toc.yml
├── harness
├── Program.cs
├── README.md
└── harness.csproj
├── nuget-readme.md
├── src
├── CSChaCha20.cs
└── ChaCha20-NetStandard.csproj
└── tests
├── ChaCha20Tests.cs
└── tests.csproj
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v4
18 | with:
19 | dotnet-version:
20 | 9.0.x
21 | - name: Test
22 | run: dotnet test tests/tests.csproj --verbosity normal --collect:"XPlat Code Coverage"
23 | - name: Upload coverage files
24 | env:
25 | CODACY_PROJECT_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}}
26 | run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l CSharp $(find . -name 'coverage.cobertura.xml' -printf '-r %p ')
27 | if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request'
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish-nuget_release.yml:
--------------------------------------------------------------------------------
1 | name: Publish-Nuget-Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v1.* # Push events to v1.0, v1.1, and v1.9 tags
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Setup .NET
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: 9.0.x
19 | - name: Add SHORT_SHA env property with commit short sha
20 | run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
21 | - name: Add current ISO time to env property
22 | run: echo "CURRENT_TIME=`date -Iseconds`" >> $GITHUB_ENV
23 | - name: Pack
24 | run: |
25 | cd src
26 | dotnet pack -o out --configuration Release --include-source --include-symbols /p:ContinuousIntegrationBuild=true /p:InformationalVersion="Build time: ${{env.CURRENT_TIME}} Short hash: ${{env.SHORT_SHA}}"
27 | - name: Push to Nuget
28 | run: dotnet nuget push ./src/out/*.nupkg --skip-duplicate -k ${{secrets.NUGET_TOKEN}} --source https://api.nuget.org/v3/index.json
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 |
--------------------------------------------------------------------------------
/CHANGE-LOG.md:
--------------------------------------------------------------------------------
1 | ## Version 1.1.0 (released 2024-12-06)
2 | - Drop support for .NET Standard and .NET 6 (**BREAKING**)
3 | - SIMD support for XOR operation (**PERFORMANCE**)
4 |
5 | ## Version 1.0.1 (released 2023-11-16)
6 | - Include nuget-readme
7 | - Support net8.0
8 | - Trimmable library
9 |
10 | ## Version 1.0.0 (released 2022-05-21)
11 | - Support net6.0
12 | - Deterministic build
13 |
14 | ## Version 0.9.5 (released 2021-01-09)
15 | - WorkStreams optimization, thanks to **Akintos** (**PERFORMANCE**)
16 |
17 | ## Version 0.9.4 (released 2020-05-16)
18 | - Add wanted bytes at time parameter for stream encryption/decryption (**FEATURE**)
19 | - Add async versions of stream processing (**FEATURE**)
20 |
21 | ## Version 0.9.3 (released 2020-05-09)
22 | - Support stream encryption/decryption (**FEATURE**)
23 |
24 | ## Version 0.9.2 (released 2020-01-12)
25 | - Generate documentation (**FEATURE**)
26 |
27 | ## Version 0.9.1 (released 2018-11-18)
28 | - More overloads for encrypt/decrypt (no need to pass lengths of arrays anymore)
29 | - One method to handle strings (as UTF-8 byte array)
30 |
31 | ## Version 0.9.0 (released 2018-11-14)
32 | - Initial Nuget release
33 | - Just one encrypt and decrypt method
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, 2018 Scott Bennett
2 | (c) 2018 Kaarlo Räihä
3 |
4 | Permission to use, copy, modify, and distribute this software for any
5 | purpose with or without fee is hereby granted, provided that the above
6 | copyright notice and this permission notice appear in all copies.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSharp-ChaCha20-NetStandard
2 |
3 | Managed .Net (.NET 8) compatible [ChaCha20](https://en.wikipedia.org/wiki/Salsa20#ChaCha_variant) cipher written in C#
4 |
5 | ## Build status
6 | 
7 | [](https://www.codacy.com/gh/mcraiha/CSharp-ChaCha20-NetStandard/dashboard?utm_source=github.com&utm_medium=referral&utm_content=mcraiha/CSharp-ChaCha20-NetStandard&utm_campaign=Badge_Coverage)
8 |
9 | ## Why?
10 |
11 | Because I needed this for my personal project
12 |
13 | ## Origin
14 |
15 | **Scott Bennett** wrote C# implementation called [ChaCha20-csharp](https://github.com/sbennett1990/ChaCha20-csharp), which works as base for my code. That is why the license is same for both projects
16 |
17 | ## Older versions
18 |
19 | YOu can find OLD .NET Standard and .NET 6 compatible version from [older branch](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard/tree/netstandard20andnet6)
20 |
21 | ## Documentation
22 |
23 | [Docs](https://mcraiha.github.io/CSharp-ChaCha20-NetStandard/api/index.html)
24 |
25 | ## How do I use this?
26 |
27 | Either copy the [CSChaCha20.cs](src/CSChaCha20.cs) to your project or use [LibChaCha20](https://www.nuget.org/packages/LibChaCha20/) nuget package
28 |
29 | Then do code like
30 | ```csharp
31 | using CSChaCha20;
32 |
33 | byte[] mySimpleTextAsBytes = Encoding.ASCII.GetBytes("Plain text I want to encrypt");
34 |
35 | // Do not use these key and nonce values in your own code!
36 | byte[] key = new byte[32] { 142, 26, 14, 68, 43, 188, 234, 12, 73, 246, 252, 111, 8, 227, 57, 22, 168, 140, 41, 18, 91, 76, 181, 239, 95, 182, 248, 44, 165, 98, 34, 12 };
37 | byte[] nonce = new byte[12] { 139, 164, 65, 213, 125, 108, 159, 118, 252, 180, 33, 88 };
38 | uint counter = 1;
39 |
40 | // Encrypt
41 | ChaCha20 forEncrypting = new ChaCha20(key, nonce, counter);
42 | byte[] encryptedContent = new byte[mySimpleTextAsBytes.Length];
43 | forEncrypting.EncryptBytes(encryptedContent, mySimpleTextAsBytes);
44 |
45 | // Decrypt
46 | ChaCha20 forDecrypting = new ChaCha20(key, nonce, counter);
47 | byte[] decryptedContent = new byte[encryptedContent.Length];
48 | forDecrypting.DecryptBytes(decryptedContent, encryptedContent);
49 |
50 | ```
51 |
52 | You can try out the code in [.NET Fiddle](https://dotnetfiddle.net/4D6E5Z)
53 |
54 | ## Test cases
55 |
56 | You can run test cases by moving to **tests** folder and running following command
57 | ```bash
58 | dotnet test
59 | ```
60 |
61 | ## Benchmarks
62 |
63 | You can run benchmarks (which compare this implementation to the original version) by moving to **benchmarks** folder and running following command
64 | ```bash
65 | dotnet run -c Release
66 | ```
67 |
68 | there are three different input sizes (64 bytes, 1024 bytes and 1 MiB) and comparisons are done between the original version (made by Scott Bennett) and this project
69 |
70 | ## License
71 |
72 | All the code is licensed under [ISC License](LICENSE)
--------------------------------------------------------------------------------
/benchmarks/ChaCha20Cipher.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, 2018 Scott Bennett
3 | *
4 | * Permission to use, copy, modify, and distribute this software for any
5 | * purpose with or without fee is hereby granted, provided that the above
6 | * copyright notice and this permission notice appear in all copies.
7 | *
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 | */
16 |
17 | using System;
18 | using System.Text;
19 |
20 | namespace ChaCha20Cipher {
21 | public sealed class ChaCha20Cipher : IDisposable {
22 | ///
23 | /// The ChaCha20 state (aka "context")
24 | ///
25 | private uint[] state;
26 |
27 | ///
28 | /// Determines if the objects in this class have been disposed of. Set to
29 | /// true by the Dispose() method.
30 | ///
31 | private bool isDisposed;
32 |
33 | ///
34 | /// Set up a new ChaCha20 state. The lengths of the given parameters are
35 | /// checked before encryption happens.
36 | ///
37 | ///
38 | /// See ChaCha20 Spec Section 2.4
39 | /// for a detailed description of the inputs.
40 | ///
41 | ///
42 | /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit
43 | /// little-endian integers
44 | ///
45 | ///
46 | /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit
47 | /// little-endian integers
48 | ///
49 | ///
50 | /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
51 | ///
52 | public ChaCha20Cipher(byte[] key, byte[] nonce, uint counter) {
53 | this.state = new uint[16];
54 | this.isDisposed = false;
55 |
56 | KeySetup(key);
57 | IvSetup(nonce, counter);
58 | }
59 |
60 | ///
61 | /// The ChaCha20 state (aka "context"). Read-Only.
62 | ///
63 | public uint[] State {
64 | get {
65 | return this.state;
66 | }
67 | }
68 |
69 | ///
70 | /// Set up the ChaCha state with the given key. A 32-byte key is required
71 | /// and enforced.
72 | ///
73 | ///
74 | /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit
75 | /// little-endian integers
76 | ///
77 | private void KeySetup(byte[] key) {
78 | if (key == null) {
79 | throw new ArgumentNullException("Key is null");
80 | }
81 | if (key.Length != 32) {
82 | throw new ArgumentException(
83 | $"Key length must be 32. Actual: {key.Length}"
84 | );
85 | }
86 |
87 | // These are the same constants defined in the reference implementation.
88 | // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c
89 | byte[] sigma = Encoding.ASCII.GetBytes("expand 32-byte k");
90 | byte[] tau = Encoding.ASCII.GetBytes("expand 16-byte k");
91 |
92 | state[4] = Util.U8To32Little(key, 0);
93 | state[5] = Util.U8To32Little(key, 4);
94 | state[6] = Util.U8To32Little(key, 8);
95 | state[7] = Util.U8To32Little(key, 12);
96 |
97 | byte[] constants = (key.Length == 32) ? sigma : tau;
98 | int keyIndex = key.Length - 16;
99 |
100 | state[8] = Util.U8To32Little(key, keyIndex + 0);
101 | state[9] = Util.U8To32Little(key, keyIndex + 4);
102 | state[10] = Util.U8To32Little(key, keyIndex + 8);
103 | state[11] = Util.U8To32Little(key, keyIndex + 12);
104 |
105 | state[0] = Util.U8To32Little(constants, 0);
106 | state[1] = Util.U8To32Little(constants, 4);
107 | state[2] = Util.U8To32Little(constants, 8);
108 | state[3] = Util.U8To32Little(constants, 12);
109 | }
110 |
111 | ///
112 | /// Set up the ChaCha state with the given nonce (aka Initialization Vector
113 | /// or IV) and block counter. A 12-byte nonce and a 4-byte counter are
114 | /// required.
115 | ///
116 | ///
117 | /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit
118 | /// little-endian integers
119 | ///
120 | ///
121 | /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
122 | ///
123 | private void IvSetup(byte[] nonce, uint counter) {
124 | if (nonce == null) {
125 | // There has already been some state set up. Clear it before exiting.
126 | Dispose();
127 | throw new ArgumentNullException("Nonce is null");
128 | }
129 | if (nonce.Length != 12) {
130 | // There has already been some state set up. Clear it before exiting.
131 | Dispose();
132 | throw new ArgumentException(
133 | $"Nonce length must be 12. Actual: {nonce.Length}"
134 | );
135 | }
136 |
137 | state[12] = counter;
138 | state[13] = Util.U8To32Little(nonce, 0);
139 | state[14] = Util.U8To32Little(nonce, 4);
140 | state[15] = Util.U8To32Little(nonce, 8);
141 | }
142 |
143 | ///
144 | /// Encrypt an arbitrary-length plaintext message (input), writing the
145 | /// resulting ciphertext to the output buffer. The number of bytes to read
146 | /// from the input buffer is determined by numBytes.
147 | ///
148 | ///
149 | ///
150 | ///
151 | public void EncryptBytes(byte[] output, byte[] input, int numBytes) {
152 | if (isDisposed) {
153 | throw new ObjectDisposedException("state",
154 | "The ChaCha state has been disposed");
155 | }
156 | if (numBytes < 0 || numBytes > input.Length) {
157 | throw new ArgumentOutOfRangeException("numBytes",
158 | "The number of bytes to read must be between [0..input.Length]");
159 | }
160 |
161 | uint[] x = new uint[16]; // Working buffer
162 | byte[] tmp = new byte[64]; // Temporary buffer
163 | int outputOffset = 0;
164 | int inputOffset = 0;
165 |
166 | while (numBytes > 0) {
167 | for (int i = 16; i-- > 0; ) {
168 | x[i] = this.state[i];
169 | }
170 |
171 | for (int i = 20; i > 0; i -= 2) {
172 | QuarterRound(x, 0, 4, 8, 12);
173 | QuarterRound(x, 1, 5, 9, 13);
174 | QuarterRound(x, 2, 6, 10, 14);
175 | QuarterRound(x, 3, 7, 11, 15);
176 |
177 | QuarterRound(x, 0, 5, 10, 15);
178 | QuarterRound(x, 1, 6, 11, 12);
179 | QuarterRound(x, 2, 7, 8, 13);
180 | QuarterRound(x, 3, 4, 9, 14);
181 | }
182 |
183 | for (int i = 16; i-- > 0; ) {
184 | Util.ToBytes(tmp, Util.Add(x[i], this.state[i]), 4 * i);
185 | }
186 |
187 | this.state[12] = Util.AddOne(state[12]);
188 | if (this.state[12] <= 0) {
189 | /* Stopping at 2^70 bytes per nonce is the user's responsibility */
190 | this.state[13] = Util.AddOne(state[13]);
191 | }
192 |
193 | if (numBytes <= 64) {
194 | for (int i = numBytes; i-- > 0; ) {
195 | output[i + outputOffset] = (byte) (input[i + inputOffset] ^ tmp[i]);
196 | }
197 |
198 | return;
199 | }
200 |
201 | for (int i = 64; i-- > 0; ) {
202 | output[i + outputOffset] = (byte) (input[i + inputOffset] ^ tmp[i]);
203 | }
204 |
205 | numBytes -= 64;
206 | outputOffset += 64;
207 | inputOffset += 64;
208 | }
209 | }
210 |
211 | ///
212 | /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned
213 | /// integers within the given buffer at indices a, b, c, and d.
214 | ///
215 | ///
216 | /// The ChaCha state does not have four integer numbers: it has 16. So
217 | /// the quarter-round operation works on only four of them -- hence the
218 | /// name. Each quarter round operates on four predetermined numbers in
219 | /// the ChaCha state.
220 | /// See ChaCha20 Spec Sections 2.1 - 2.2.
221 | ///
222 | /// A ChaCha state (vector). Must contain 16 elements.
223 | /// Index of the first number
224 | /// Index of the second number
225 | /// Index of the third number
226 | /// Index of the fourth number
227 | public static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d) {
228 | if (x == null) {
229 | throw new ArgumentNullException("Input buffer is null");
230 | }
231 | if (x.Length != 16) {
232 | throw new ArgumentException();
233 | }
234 |
235 | x[a] = Util.Add(x[a], x[b]); x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16);
236 | x[c] = Util.Add(x[c], x[d]); x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12);
237 | x[a] = Util.Add(x[a], x[b]); x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8);
238 | x[c] = Util.Add(x[c], x[d]); x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7);
239 | }
240 |
241 | ///
242 | /// Currently not used.
243 | ///
244 | ///
245 | ///
246 | public static void ChaCha20BlockFunction(byte[] output, uint[] input) {
247 | if (input == null || output == null) {
248 | throw new ArgumentNullException();
249 | }
250 | if (input.Length != 16 || output.Length != 64) {
251 | throw new ArgumentException();
252 | }
253 |
254 | uint[] x = new uint[16]; // Working buffer
255 |
256 | for (int i = 16; i-- > 0; ) {
257 | x[i] = input[i];
258 | }
259 |
260 | for (int i = 20; i > 0; i -= 2) {
261 | QuarterRound(x, 0, 4, 8, 12);
262 | QuarterRound(x, 1, 5, 9, 13);
263 | QuarterRound(x, 2, 6, 10, 14);
264 | QuarterRound(x, 3, 7, 11, 15);
265 |
266 | QuarterRound(x, 0, 5, 10, 15);
267 | QuarterRound(x, 1, 6, 11, 12);
268 | QuarterRound(x, 2, 7, 8, 13);
269 | QuarterRound(x, 3, 4, 9, 14);
270 | }
271 |
272 | for (int i = 16; i-- > 0; ) {
273 | Util.ToBytes(output, Util.Add(x[i], input[i]), 4 * i);
274 | }
275 | }
276 |
277 | #region Destructor and Disposer
278 |
279 | ///
280 | /// Clear and dispose of the internal state. The finalizer is only called
281 | /// if Dispose() was never called on this cipher.
282 | ///
283 | ~ChaCha20Cipher() {
284 | Dispose(false);
285 | }
286 |
287 | ///
288 | /// Clear and dispose of the internal state. Also request the GC not to
289 | /// call the finalizer, because all cleanup has been taken care of.
290 | ///
291 | public void Dispose() {
292 | Dispose(true);
293 | /*
294 | * The Garbage Collector does not need to invoke the finalizer because
295 | * Dispose(bool) has already done all the cleanup needed.
296 | */
297 | GC.SuppressFinalize(this);
298 | }
299 |
300 | ///
301 | /// This method should only be invoked from Dispose() or the finalizer.
302 | /// This handles the actual cleanup of the resources.
303 | ///
304 | ///
305 | /// Should be true if called by Dispose(); false if called by the finalizer
306 | ///
307 | private void Dispose(bool disposing) {
308 | if (!isDisposed) {
309 | if (disposing) {
310 | /* Cleanup managed objects by calling their Dispose() methods */
311 | }
312 |
313 | /* Cleanup any unmanaged objects here */
314 | if (state != null) {
315 | Array.Clear(state, 0, state.Length);
316 | }
317 |
318 | state = null;
319 | }
320 |
321 | isDisposed = true;
322 | }
323 |
324 | #endregion
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using BenchmarkDotNet.Attributes;
4 | using BenchmarkDotNet.Running;
5 |
6 | using CSChaCha20;
7 | using ChaCha20Cipher;
8 |
9 | namespace benchmarks
10 | {
11 | [MemoryDiagnoser]
12 | public class OriginalVsAdjusted
13 | {
14 | private const int dataLength1 = 64;
15 | private const int dataLength2 = 1024;
16 | private const int dataLength3 = 1024*1024;
17 |
18 | private readonly byte[] data1;
19 | private readonly byte[] data2;
20 | private readonly byte[] data3;
21 |
22 | private readonly ChaCha20Cipher.ChaCha20Cipher original1 = null;
23 | private readonly ChaCha20Cipher.ChaCha20Cipher original2 = null;
24 | private readonly ChaCha20Cipher.ChaCha20Cipher original3 = null;
25 |
26 | private readonly CSChaCha20.ChaCha20 adjustedNoSimd1 = null;
27 | private readonly CSChaCha20.ChaCha20 adjustedNoSimd2 = null;
28 | private readonly CSChaCha20.ChaCha20 adjustedNoSimd3 = null;
29 |
30 | private readonly CSChaCha20.ChaCha20 adjusted_V128_1 = null;
31 | private readonly CSChaCha20.ChaCha20 adjusted_V128_2 = null;
32 | private readonly CSChaCha20.ChaCha20 adjusted_V128_3 = null;
33 |
34 | private readonly CSChaCha20.ChaCha20 adjusted_V256_1 = null;
35 | private readonly CSChaCha20.ChaCha20 adjusted_V256_2 = null;
36 | private readonly CSChaCha20.ChaCha20 adjusted_V256_3 = null;
37 |
38 | private readonly CSChaCha20.ChaCha20 adjusted_V512_1 = null;
39 | private readonly CSChaCha20.ChaCha20 adjusted_V512_2 = null;
40 | private readonly CSChaCha20.ChaCha20 adjusted_V512_3 = null;
41 |
42 | private static readonly byte[] key = new byte[32] {
43 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
44 | 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
45 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
46 | 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
47 | };
48 |
49 | private static readonly byte[] nonce = new byte[12] { 0x00, 0x09, 0x00, 0x00, 0xFF, 0x20, 0x12, 0x00, 0x00, 0x8b, 0x00, 0x02 };
50 | private static readonly uint counter = 13;
51 |
52 | private byte[] outputForOriginal1;
53 | private byte[] outputForOriginal2;
54 | private byte[] outputForOriginal3;
55 |
56 | private byte[] output_For_Adjusted_NoSimd_1;
57 | private byte[] output_For_Adjusted_NoSimd_2;
58 | private byte[] output_For_Adjusted_NoSimd_3;
59 |
60 | private byte[] output_For_Adjusted_V128_1;
61 | private byte[] output_For_Adjusted_V128_2;
62 | private byte[] output_For_Adjusted_V128_3;
63 |
64 | private byte[] output_For_Adjusted_V256_1;
65 | private byte[] output_For_Adjusted_V256_2;
66 | private byte[] output_For_Adjusted_V256_3;
67 |
68 | private byte[] output_For_Adjusted_V512_1;
69 | private byte[] output_For_Adjusted_V512_2;
70 | private byte[] output_For_Adjusted_V512_3;
71 |
72 | public OriginalVsAdjusted()
73 | {
74 | // Arrays for outputs
75 | this.outputForOriginal1 = new byte[dataLength1];
76 | this.outputForOriginal2 = new byte[dataLength2];
77 | this.outputForOriginal3 = new byte[dataLength3];
78 |
79 | this.output_For_Adjusted_NoSimd_1 = new byte[dataLength1];
80 | this.output_For_Adjusted_NoSimd_2 = new byte[dataLength2];
81 | this.output_For_Adjusted_NoSimd_3 = new byte[dataLength3];
82 |
83 | this.output_For_Adjusted_V128_1 = new byte[dataLength1];
84 | this.output_For_Adjusted_V128_2 = new byte[dataLength2];
85 | this.output_For_Adjusted_V128_3 = new byte[dataLength3];
86 |
87 | this.output_For_Adjusted_V256_1 = new byte[dataLength1];
88 | this.output_For_Adjusted_V256_2 = new byte[dataLength2];
89 | this.output_For_Adjusted_V256_3 = new byte[dataLength3];
90 |
91 | this.output_For_Adjusted_V512_1 = new byte[dataLength1];
92 | this.output_For_Adjusted_V512_2 = new byte[dataLength2];
93 | this.output_For_Adjusted_V512_3 = new byte[dataLength3];
94 |
95 | // Generate inputs
96 | Random rng = new Random(Seed: 1337);
97 |
98 | this.data1 = new byte[dataLength1];
99 | rng.NextBytes(this.data1);
100 |
101 | this.data2 = new byte[dataLength2];
102 | rng.NextBytes(this.data2);
103 |
104 | this.data3 = new byte[dataLength3];
105 | rng.NextBytes(this.data3);
106 |
107 | // Set encrypters
108 | this.original1 = new ChaCha20Cipher.ChaCha20Cipher(key, nonce, counter);
109 | this.original2 = new ChaCha20Cipher.ChaCha20Cipher(key, nonce, counter);
110 | this.original3 = new ChaCha20Cipher.ChaCha20Cipher(key, nonce, counter);
111 |
112 | this.adjustedNoSimd1 = new CSChaCha20.ChaCha20(key, nonce, counter);
113 | this.adjustedNoSimd2 = new CSChaCha20.ChaCha20(key, nonce, counter);
114 | this.adjustedNoSimd3 = new CSChaCha20.ChaCha20(key, nonce, counter);
115 |
116 | this.adjusted_V128_1 = new CSChaCha20.ChaCha20(key, nonce, counter);
117 | this.adjusted_V128_2 = new CSChaCha20.ChaCha20(key, nonce, counter);
118 | this.adjusted_V128_3 = new CSChaCha20.ChaCha20(key, nonce, counter);
119 |
120 | this.adjusted_V256_1 = new CSChaCha20.ChaCha20(key, nonce, counter);
121 | this.adjusted_V256_2 = new CSChaCha20.ChaCha20(key, nonce, counter);
122 | this.adjusted_V256_3 = new CSChaCha20.ChaCha20(key, nonce, counter);
123 |
124 | this.adjusted_V512_1 = new CSChaCha20.ChaCha20(key, nonce, counter);
125 | this.adjusted_V512_2 = new CSChaCha20.ChaCha20(key, nonce, counter);
126 | this.adjusted_V512_3 = new CSChaCha20.ChaCha20(key, nonce, counter);
127 | }
128 |
129 | #region 64 bytes
130 | [Benchmark]
131 | public void Original_64_Bytes() => this.original1.EncryptBytes(this.outputForOriginal1, this.data1, dataLength1);
132 |
133 | [Benchmark]
134 | public void Adjusted_NoSimd_64Bytes() => this.adjustedNoSimd1.EncryptBytes(this.output_For_Adjusted_NoSimd_1, this.data1, dataLength1, SimdMode.None);
135 |
136 | [Benchmark]
137 | public void Adjusted_V128_64Bytes() => this.adjusted_V128_1.EncryptBytes(this.output_For_Adjusted_V128_1, this.data1, dataLength1, SimdMode.V128);
138 |
139 | [Benchmark]
140 | public void Adjusted_V256_64Bytes() => this.adjusted_V256_1.EncryptBytes(this.output_For_Adjusted_V256_1, this.data1, dataLength1, SimdMode.V256);
141 |
142 | [Benchmark]
143 | public void Adjusted_V512_64Bytes() => this.adjusted_V512_1.EncryptBytes(this.output_For_Adjusted_V512_1, this.data1, dataLength1, SimdMode.V512);
144 |
145 | #endregion // 64 bytes
146 |
147 | #region 1024 bytes
148 | [Benchmark]
149 | public void Original_1024_Bytes() => this.original2.EncryptBytes(this.outputForOriginal2, this.data2, dataLength2);
150 |
151 | [Benchmark]
152 | public void Adjusted_NoSimd_1024_Bytes() => this.adjustedNoSimd2.EncryptBytes(this.output_For_Adjusted_NoSimd_2, this.data2, dataLength2, SimdMode.None);
153 |
154 | [Benchmark]
155 | public void Adjusted_V128_1024_Bytes() => this.adjusted_V128_2.EncryptBytes(this.output_For_Adjusted_V128_2, this.data2, dataLength2, SimdMode.V128);
156 |
157 | [Benchmark]
158 | public void Adjusted_V256_1024_Bytes() => this.adjusted_V256_2.EncryptBytes(this.output_For_Adjusted_V256_2, this.data2, dataLength2, SimdMode.V256);
159 |
160 | [Benchmark]
161 | public void Adjusted_V512_1024_Bytes() => this.adjusted_V512_2.EncryptBytes(this.output_For_Adjusted_V512_2, this.data2, dataLength2, SimdMode.V512);
162 |
163 | #endregion // 1024 bytes
164 |
165 | #region 1 MiB
166 | [Benchmark]
167 | public void Original_1MiB_Bytes() => this.original3.EncryptBytes(this.outputForOriginal3, this.data3, dataLength3);
168 |
169 | [Benchmark]
170 | public void Adjusted_NoSimd_1Mib_Bytes() => this.adjustedNoSimd3.EncryptBytes(this.output_For_Adjusted_NoSimd_3, this.data3, dataLength3, SimdMode.None);
171 |
172 | [Benchmark]
173 | public void Adjusted_V128_1Mib_Bytes() => this.adjusted_V128_3.EncryptBytes(this.output_For_Adjusted_V128_3, this.data3, dataLength3, SimdMode.V128);
174 |
175 | [Benchmark]
176 | public void Adjusted_V256_1Mib_Bytes() => this.adjusted_V256_3.EncryptBytes(this.output_For_Adjusted_V256_3, this.data3, dataLength3, SimdMode.V256);
177 |
178 | [Benchmark]
179 | public void Adjusted_V512_1Mib_Bytes() => this.adjusted_V512_3.EncryptBytes(this.output_For_Adjusted_V512_3, this.data3, dataLength3, SimdMode.V512);
180 |
181 | #endregion // 1 MiB
182 | }
183 |
184 | class Program
185 | {
186 | static void Main(string[] args)
187 | {
188 | var summary = BenchmarkRunner.Run();
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/benchmarks/Util.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, 2018 Scott Bennett
3 | *
4 | * Permission to use, copy, modify, and distribute this software for any
5 | * purpose with or without fee is hereby granted, provided that the above
6 | * copyright notice and this permission notice appear in all copies.
7 | *
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 | */
16 |
17 | using System;
18 |
19 | namespace ChaCha20Cipher {
20 | public class Util {
21 | ///
22 | /// n-bit left rotation operation (towards the high bits) for 32-bit
23 | /// integers.
24 | ///
25 | ///
26 | ///
27 | /// The result of (v LEFTSHIFT c)
28 | public static uint Rotate(uint v, int c) {
29 | unchecked {
30 | return (v << c) | (v >> (32 - c));
31 | }
32 | }
33 |
34 | ///
35 | /// Unchecked integer exclusive or (XOR) operation.
36 | ///
37 | ///
38 | ///
39 | /// The result of (v XOR w)
40 | public static uint XOr(uint v, uint w) {
41 | return unchecked(v ^ w);
42 | }
43 |
44 | ///
45 | /// Unchecked integer addition. The ChaCha spec defines certain operations
46 | /// to use 32-bit unsigned integer addition modulo 2^32.
47 | ///
48 | ///
49 | ///
50 | /// See ChaCha20 Spec Section 2.1.
51 | ///
52 | ///
53 | ///
54 | ///
55 | /// The result of (v + w) modulo 2^32
56 | public static uint Add(uint v, uint w) {
57 | return unchecked(v + w);
58 | }
59 |
60 | ///
61 | /// Add 1 to the input parameter using unchecked integer addition. The
62 | /// ChaCha spec defines certain operations to use 32-bit unsigned integer
63 | /// addition modulo 2^32.
64 | ///
65 | ///
66 | /// See ChaCha20 Spec Section 2.1.
67 | ///
68 | ///
69 | /// The result of (v + 1) modulo 2^32
70 | public static uint AddOne(uint v) {
71 | return unchecked(v + 1);
72 | }
73 |
74 | ///
75 | /// Convert four bytes of the input buffer into an unsigned
76 | /// 32-bit integer, beginning at the inputOffset.
77 | ///
78 | ///
79 | ///
80 | /// An unsigned 32-bit integer
81 | public static uint U8To32Little(byte[] p, int inputOffset) {
82 | unchecked {
83 | return ((uint) p[inputOffset]
84 | | ((uint) p[inputOffset + 1] << 8)
85 | | ((uint) p[inputOffset + 2] << 16)
86 | | ((uint) p[inputOffset + 3] << 24));
87 | }
88 | }
89 |
90 | ///
91 | /// Serialize the input integer into the output buffer. The input integer
92 | /// will be split into 4 bytes and put into four sequential places in the
93 | /// output buffer, starting at the outputOffset.
94 | ///
95 | ///
96 | ///
97 | ///
98 | public static void ToBytes(byte[] output, uint input, int outputOffset) {
99 | if (outputOffset < 0) {
100 | throw new ArgumentOutOfRangeException("outputOffset",
101 | "The buffer offset cannot be negative");
102 | }
103 |
104 | unchecked {
105 | output[outputOffset] = (byte) input;
106 | output[outputOffset + 1] = (byte) (input >> 8);
107 | output[outputOffset + 2] = (byte) (input >> 16);
108 | output[outputOffset + 3] = (byte) (input >> 24);
109 | }
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/benchmarks/benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/create-nuget-debug.ps1:
--------------------------------------------------------------------------------
1 | # Git command used for getting latest commit
2 | $gitCmdName = "git"
3 | $gitCmdParameter = "rev-parse HEAD"
4 |
5 | $currentDate = Get-Date
6 | # Write-Host $currentDate.ToUniversalTime()
7 |
8 | $latestGitCommitHashFull = "Git is not installed"
9 | $latestGitCommitHashShort = "Git is not installed"
10 |
11 | if (Get-Command $gitCmdName -errorAction SilentlyContinue)
12 | {
13 | $latestGitCommitHashFull = &git rev-parse HEAD
14 | $latestGitCommitHashShort = &git rev-parse --short HEAD
15 | # Write-Host "$gitCmdName exists"
16 | }
17 |
18 | Write-Host $latestGitCommitHashFull $latestGitCommitHashShort
19 | $finalCommand = "dotnet pack" + " " + "--configuration Debug" + " " + "--include-source" + " " + "--include-symbols" + " " + "/p:InformationalVersion=""" + $currentDate.ToUniversalTime().ToString("yyyy-MM-dd HH.mm.ss") + " " + $latestGitCommitHashFull + """" + " " + "--version-suffix" + " git-" + $latestGitCommitHashShort
20 | Write-Host $finalCommand
--------------------------------------------------------------------------------
/create-nuget-release.ps1:
--------------------------------------------------------------------------------
1 | $currentDate = Get-Date
2 | $gitShortHash = ((git rev-parse --short HEAD) | Out-String).Trim()
3 | $finalCommand = "dotnet pack" + " " + "--configuration Release" + " " + "--include-source" + " " + "--include-symbols" + " " + "/p:InformationalVersion=""" + "Build time: " + $currentDate.ToUniversalTime().ToString("yyyy-MM-dd HH.mm.ss") + " Short hash: " + $gitShortHash + """"
4 | Write-Host $finalCommand
--------------------------------------------------------------------------------
/create-nuget-release.sh:
--------------------------------------------------------------------------------
1 | DATE=`date -Iseconds`
2 | SHORTHASH=`git rev-parse --short HEAD | xargs`
3 | echo "dotnet pack --configuration Release --include-source --include-symbols /p:InformationalVersion=\"Build time: $DATE Short hash: $SHORTHASH\""
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | ###############
2 | # folder #
3 | ###############
4 | /**/DROP/
5 | /**/TEMP/
6 | /**/packages/
7 | /**/bin/
8 | /**/obj/
9 | _site
10 |
--------------------------------------------------------------------------------
/docs/api/.gitignore:
--------------------------------------------------------------------------------
1 | ###############
2 | # temp file #
3 | ###############
4 | *.yml
5 | .manifest
6 |
--------------------------------------------------------------------------------
/docs/api/index.md:
--------------------------------------------------------------------------------
1 | # CSChaCha20
2 | Only namespace is [CSChaCha20](CSChaCha20.html)
3 |
4 | ## How do I use this?
5 | Either copy the [CSChaCha20.cs](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard/blob/master/src/CSChaCha20.cs) to your project or use [LibChaCha20](https://www.nuget.org/packages/LibChaCha20/) nuget package
6 |
7 | Then do code like
8 | ```csharp
9 | using CSChaCha20;
10 |
11 | byte[] mySimpleTextAsBytes = Encoding.ASCII.GetBytes("Plain text I want to encrypt");
12 |
13 | // Do not use these key and nonce values in your own code!
14 | byte[] key = new byte[32] { 142, 26, 14, 68, 43, 188, 234, 12, 73, 246, 252, 111, 8, 227, 57, 22, 168, 140, 41, 18, 91, 76, 181, 239, 95, 182, 248, 44, 165, 98, 34, 12 };
15 | byte[] nonce = new byte[12] { 139, 164, 65, 213, 125, 108, 159, 118, 252, 180, 33, 88 };
16 | uint counter = 1;
17 |
18 | // Encrypt
19 | ChaCha20 forEncrypting = new ChaCha20(key, nonce, counter);
20 | byte[] encryptedContent = new byte[mySimpleTextAsBytes.Length];
21 | forEncrypting.EncryptBytes(encryptedContent, mySimpleTextAsBytes);
22 |
23 | // Decrypt
24 | ChaCha20 forDecrypting = new ChaCha20(key, nonce, counter);
25 | byte[] decryptedContent = new byte[encryptedContent.Length];
26 | forDecrypting.DecryptBytes(decryptedContent, encryptedContent);
27 |
28 | ```
29 |
30 | You can try out the code in [.NET Fiddle](https://dotnetfiddle.net/4D6E5Z)
31 |
32 | ## License
33 |
34 | All the code is licensed under [ISC License](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard/blob/master/LICENSE)
35 |
--------------------------------------------------------------------------------
/docs/docfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": [
3 | {
4 | "src": [
5 | {
6 | "files": [
7 | "src/CSChaCha20.cs"
8 | ],
9 | "src": ".."
10 | }
11 | ],
12 | "dest": "api",
13 | "disableGitFeatures": false,
14 | "disableDefaultFilter": false
15 | }
16 | ],
17 | "build": {
18 | "content": [
19 | {
20 | "files": [
21 | "api/**.yml",
22 | "api/index.md"
23 | ]
24 | },
25 | {
26 | "files": [
27 | "toc.yml",
28 | "*.md"
29 | ]
30 | }
31 | ],
32 | "resource": [
33 | {
34 | "files": [
35 | "images/**"
36 | ]
37 | }
38 | ],
39 | "overwrite": [
40 | {
41 | "files": [
42 | "apidoc/**.md"
43 | ],
44 | "exclude": [
45 | "obj/**",
46 | "_site/**"
47 | ]
48 | }
49 | ],
50 | "dest": "_site",
51 | "globalMetadataFiles": [],
52 | "fileMetadataFiles": [],
53 | "template": [
54 | "statictoc"
55 | ],
56 | "postProcessors": [],
57 | "markdownEngineName": "markdig",
58 | "noLangKeyword": false,
59 | "keepFileLink": false,
60 | "cleanupCacheHistory": false,
61 | "disableGitFeatures": false
62 | }
63 | }
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # CSharp-ChaCha20-NetStandard
2 | Managed .Net (.NET 8) compatible [ChaCha20](https://en.wikipedia.org/wiki/Salsa20#ChaCha_variant) cipher written in C#
3 |
4 | ## GitHub
5 | [CSharp-ChaCha20-NetStandard](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard)
6 |
7 | ## Documentation
8 | [Documentation](api/)
9 |
10 | ## How do I use this?
11 | Either copy the [CSChaCha20.cs](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard/blob/master/src/CSChaCha20.cs) to your project or use [LibChaCha20](https://www.nuget.org/packages/LibChaCha20/) nuget package
12 |
13 | Then do code like
14 | ```csharp
15 | using CSChaCha20;
16 |
17 | byte[] mySimpleTextAsBytes = Encoding.ASCII.GetBytes("Plain text I want to encrypt");
18 |
19 | // Do not use these key and nonce values in your own code!
20 | byte[] key = new byte[32] { 142, 26, 14, 68, 43, 188, 234, 12, 73, 246, 252, 111, 8, 227, 57, 22, 168, 140, 41, 18, 91, 76, 181, 239, 95, 182, 248, 44, 165, 98, 34, 12 };
21 | byte[] nonce = new byte[12] { 139, 164, 65, 213, 125, 108, 159, 118, 252, 180, 33, 88 };
22 | uint counter = 1;
23 |
24 | // Encrypt
25 | ChaCha20 forEncrypting = new ChaCha20(key, nonce, counter);
26 | byte[] encryptedContent = new byte[mySimpleTextAsBytes.Length];
27 | forEncrypting.EncryptBytes(encryptedContent, mySimpleTextAsBytes);
28 |
29 | // Decrypt
30 | ChaCha20 forDecrypting = new ChaCha20(key, nonce, counter);
31 | byte[] decryptedContent = new byte[encryptedContent.Length];
32 | forDecrypting.DecryptBytes(decryptedContent, encryptedContent);
33 |
34 | ```
35 |
36 | You can try out the code in [.NET Fiddle](https://dotnetfiddle.net/4D6E5Z)
37 |
38 | ## License
39 |
40 | All the code is licensed under [ISC License](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard/blob/master/LICENSE)
--------------------------------------------------------------------------------
/docs/toc.yml:
--------------------------------------------------------------------------------
1 | - name: Api Documentation
2 | href: api/
3 | homepage: api/index.md
4 |
--------------------------------------------------------------------------------
/harness/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Diagnostics;
4 | using CSChaCha20;
5 |
6 | namespace harness
7 | {
8 | class Program
9 | {
10 | static void Main(string[] args)
11 | {
12 | TextWriter errorWriter = Console.Error;
13 |
14 | int limit = 0;
15 |
16 | if(args.Length > 1 && !int.TryParse(args[1], out limit))
17 | {
18 | errorWriter.WriteLine($"{args[1]} is not a valid integer");
19 | return;
20 | }
21 |
22 | errorWriter.WriteLine("Starting throughput harness...");
23 |
24 | if (limit > 0)
25 | {
26 | errorWriter.WriteLine($"Limit is {limit} bytes");
27 | }
28 | else
29 | {
30 | errorWriter.WriteLine($"No byte limit");
31 | }
32 |
33 | byte[] key = new byte[32] { 142, 26, 14, 68, 43, 188, 234, 12, 73, 246, 252, 111, 8, 227, 57, 22, 168, 140, 41, 18, 91, 76, 181, 239, 95, 182, 248, 44, 165, 98, 34, 12 };
34 | byte[] nonce = new byte[12] { 139, 164, 65, 213, 125, 108, 159, 118, 252, 180, 33, 88 };
35 | uint counter = 1;
36 |
37 | int bufferSize = 1024;
38 |
39 | int bytesProcessed = 0;
40 |
41 | byte[] buffer = new byte[bufferSize];
42 |
43 | Stopwatch stopwatch = new Stopwatch();
44 | stopwatch.Start();
45 |
46 | using (ChaCha20 forEncrypting = new ChaCha20(key, nonce, counter))
47 | {
48 | // Read from input stream as long as there is something
49 | using (Stream inputStream = Console.OpenStandardInput())
50 | {
51 | // Write to output stream
52 | using (Stream outputStream = Console.OpenStandardOutput())
53 | {
54 | int readAmount = inputStream.Read(buffer, 0, bufferSize);
55 | while (readAmount > 0 && limit > -1)
56 | {
57 | outputStream.Write(forEncrypting.EncryptBytes(buffer, readAmount));
58 |
59 | if (limit > 0)
60 | {
61 | limit -= readAmount;
62 | }
63 |
64 | bytesProcessed += readAmount;
65 |
66 | readAmount = inputStream.Read(buffer, 0, bufferSize);
67 | }
68 | }
69 | }
70 | }
71 |
72 | stopwatch.Stop();
73 | errorWriter.WriteLine($"Processed {bytesProcessed} bytes in {stopwatch.Elapsed.TotalSeconds} seconds");
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/harness/README.md:
--------------------------------------------------------------------------------
1 | # Harness
2 |
3 | This is a test harness one can use to benchmark the library
4 |
5 | ## How to use
6 |
7 | Run following command in Linux to test throughput of 1 000 000 000 bytes
8 |
9 | ```bash
10 | dotnet run -c release 1000000000 < /dev/zero > /dev/null
11 | ```
12 |
13 | or if you do not want any limit
14 |
15 | ```bash
16 | dotnet run -c release < /dev/zero > /dev/null
17 | ```
18 |
--------------------------------------------------------------------------------
/harness/harness.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/nuget-readme.md:
--------------------------------------------------------------------------------
1 | # CSharp-ChaCha20-NetStandard
2 |
3 | Managed .Net (.NET 8) compatible [ChaCha20](https://en.wikipedia.org/wiki/Salsa20#ChaCha_variant) cipher written in C#
4 |
5 | ## Documentation
6 |
7 | [Docs](https://mcraiha.github.io/CSharp-ChaCha20-NetStandard/api/index.html)
8 |
9 | ## How do I use this?
10 |
11 | ```csharp
12 | using CSChaCha20;
13 |
14 | byte[] mySimpleTextAsBytes = Encoding.ASCII.GetBytes("Plain text I want to encrypt");
15 |
16 | // Do NOT use these key and nonce values in your own code!
17 | byte[] key = new byte[32] { 142, 26, 14, 68, 43, 188, 234, 12, 73, 246, 252, 111, 8, 227, 57, 22, 168, 140, 41, 18, 91, 76, 181, 239, 95, 182, 248, 44, 165, 98, 34, 12 };
18 | byte[] nonce = new byte[12] { 139, 164, 65, 213, 125, 108, 159, 118, 252, 180, 33, 88 };
19 | uint counter = 1;
20 |
21 | // Encrypt
22 | ChaCha20 forEncrypting = new ChaCha20(key, nonce, counter);
23 | byte[] encryptedContent = new byte[mySimpleTextAsBytes.Length];
24 | forEncrypting.EncryptBytes(encryptedContent, mySimpleTextAsBytes);
25 |
26 | // Decrypt
27 | ChaCha20 forDecrypting = new ChaCha20(key, nonce, counter);
28 | byte[] decryptedContent = new byte[encryptedContent.Length];
29 | forDecrypting.DecryptBytes(decryptedContent, encryptedContent);
30 |
31 | ```
32 |
33 | You can try out the code in [.NET Fiddle](https://dotnetfiddle.net/4D6E5Z)
--------------------------------------------------------------------------------
/src/CSChaCha20.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, 2018 Scott Bennett
3 | * (c) 2018-2023 Kaarlo Räihä
4 | *
5 | * Permission to use, copy, modify, and distribute this software for any
6 | * purpose with or without fee is hereby granted, provided that the above
7 | * copyright notice and this permission notice appear in all copies.
8 | *
9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 | */
17 |
18 | using System;
19 | using System.IO;
20 | using System.Threading.Tasks;
21 | using System.Runtime.Intrinsics;
22 | using System.Runtime.CompilerServices; // For MethodImplOptions.AggressiveInlining
23 |
24 | namespace CSChaCha20;
25 |
26 | ///
27 | /// Chosen SIMD mode
28 | ///
29 | public enum SimdMode
30 | {
31 | ///
32 | /// Autodetect
33 | ///
34 | AutoDetect = 0,
35 |
36 | ///
37 | /// 128 bit SIMD
38 | ///
39 | V128,
40 |
41 | ///
42 | /// 256 bit SIMD
43 | ///
44 | V256,
45 |
46 | ///
47 | /// 512 bit SIMD
48 | ///
49 | V512,
50 |
51 | ///
52 | /// No SIMD
53 | ///
54 | None
55 | }
56 |
57 | ///
58 | /// Class for ChaCha20 encryption / decryption
59 | ///
60 | public sealed class ChaCha20 : IDisposable
61 | {
62 | ///
63 | /// Only allowed key lenght in bytes
64 | ///
65 | public const int allowedKeyLength = 32;
66 |
67 | ///
68 | /// Only allowed nonce lenght in bytes
69 | ///
70 | public const int allowedNonceLength = 12;
71 |
72 | ///
73 | /// How many bytes are processed per loop
74 | ///
75 | public const int processBytesAtTime = 64;
76 |
77 | private const int stateLength = 16;
78 |
79 | ///
80 | /// The ChaCha20 state (aka "context")
81 | ///
82 | private readonly uint[] state = new uint[stateLength];
83 |
84 | ///
85 | /// Determines if the objects in this class have been disposed of. Set to true by the Dispose() method.
86 | ///
87 | private bool isDisposed = false;
88 |
89 | ///
90 | /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens.
91 | ///
92 | ///
93 | /// See ChaCha20 Spec Section 2.4 for a detailed description of the inputs.
94 | ///
95 | ///
96 | /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers
97 | ///
98 | ///
99 | /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers
100 | ///
101 | ///
102 | /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
103 | ///
104 | public ChaCha20(byte[] key, byte[] nonce, uint counter)
105 | {
106 | this.KeySetup(key);
107 | this.IvSetup(nonce, counter);
108 | }
109 |
110 | ///
111 | /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens.
112 | ///
113 | ///
114 | /// See ChaCha20 Spec Section 2.4 for a detailed description of the inputs.
115 | ///
116 | /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers
117 | /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers
118 | /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian unsigned integer
119 | public ChaCha20(ReadOnlySpan key, ReadOnlySpan nonce, uint counter)
120 | {
121 | this.KeySetup(key.ToArray());
122 | this.IvSetup(nonce.ToArray(), counter);
123 | }
124 |
125 | ///
126 | /// The ChaCha20 state (aka "context"). Read-Only.
127 | ///
128 | public uint[] State
129 | {
130 | get
131 | {
132 | return this.state;
133 | }
134 | }
135 |
136 |
137 | // These are the same constants defined in the reference implementation.
138 | // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c
139 | private static readonly byte[] sigma = "expand 32-byte k"u8.ToArray();
140 | private static readonly byte[] tau = "expand 16-byte k"u8.ToArray();
141 |
142 | ///
143 | /// Set up the ChaCha state with the given key. A 32-byte key is required and enforced.
144 | ///
145 | ///
146 | /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers
147 | ///
148 | private void KeySetup(byte[] key)
149 | {
150 | if (key == null)
151 | {
152 | throw new ArgumentNullException("Key is null");
153 | }
154 |
155 | if (key.Length != allowedKeyLength)
156 | {
157 | throw new ArgumentException($"Key length must be {allowedKeyLength}. Actual: {key.Length}");
158 | }
159 |
160 | state[4] = Util.U8To32Little(key, 0);
161 | state[5] = Util.U8To32Little(key, 4);
162 | state[6] = Util.U8To32Little(key, 8);
163 | state[7] = Util.U8To32Little(key, 12);
164 |
165 | byte[] constants = (key.Length == allowedKeyLength) ? sigma : tau;
166 | int keyIndex = key.Length - 16;
167 |
168 | state[8] = Util.U8To32Little(key, keyIndex + 0);
169 | state[9] = Util.U8To32Little(key, keyIndex + 4);
170 | state[10] = Util.U8To32Little(key, keyIndex + 8);
171 | state[11] = Util.U8To32Little(key, keyIndex + 12);
172 |
173 | state[0] = Util.U8To32Little(constants, 0);
174 | state[1] = Util.U8To32Little(constants, 4);
175 | state[2] = Util.U8To32Little(constants, 8);
176 | state[3] = Util.U8To32Little(constants, 12);
177 | }
178 |
179 | ///
180 | /// Set up the ChaCha state with the given nonce (aka Initialization Vector or IV) and block counter. A 12-byte nonce and a 4-byte counter are required.
181 | ///
182 | ///
183 | /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers
184 | ///
185 | ///
186 | /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
187 | ///
188 | private void IvSetup(byte[] nonce, uint counter)
189 | {
190 | if (nonce == null)
191 | {
192 | // There has already been some state set up. Clear it before exiting.
193 | Dispose();
194 | throw new ArgumentNullException("Nonce is null");
195 | }
196 |
197 | if (nonce.Length != allowedNonceLength)
198 | {
199 | // There has already been some state set up. Clear it before exiting.
200 | Dispose();
201 | throw new ArgumentException($"Nonce length must be {allowedNonceLength}. Actual: {nonce.Length}");
202 | }
203 |
204 | state[12] = counter;
205 | state[13] = Util.U8To32Little(nonce, 0);
206 | state[14] = Util.U8To32Little(nonce, 4);
207 | state[15] = Util.U8To32Little(nonce, 8);
208 | }
209 |
210 | private static SimdMode DetectSimdMode()
211 | {
212 | if (Vector512.IsHardwareAccelerated)
213 | {
214 | return SimdMode.V512;
215 | }
216 | else if (Vector256.IsHardwareAccelerated)
217 | {
218 | return SimdMode.V256;
219 | }
220 | else if (Vector128.IsHardwareAccelerated)
221 | {
222 | return SimdMode.V128;
223 | }
224 |
225 | return SimdMode.None;
226 | }
227 |
228 | #region Encryption methods
229 |
230 | ///
231 | /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.
232 | ///
233 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
234 | /// Output byte array, must have enough bytes
235 | /// Input byte array
236 | /// Number of bytes to encrypt
237 | /// Chosen SIMD mode (default is auto-detect)
238 | public void EncryptBytes(byte[] output, byte[] input, int numBytes, SimdMode simdMode = SimdMode.AutoDetect)
239 | {
240 | if (output == null)
241 | {
242 | throw new ArgumentNullException("output", "Output cannot be null");
243 | }
244 |
245 | if (input == null)
246 | {
247 | throw new ArgumentNullException("input", "Input cannot be null");
248 | }
249 |
250 | if (numBytes < 0 || numBytes > input.Length)
251 | {
252 | throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]");
253 | }
254 |
255 | if (output.Length < numBytes)
256 | {
257 | throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}");
258 | }
259 |
260 | if (simdMode == SimdMode.AutoDetect)
261 | {
262 | simdMode = DetectSimdMode();
263 | }
264 |
265 | this.WorkBytes(output, input, numBytes, simdMode);
266 | }
267 |
268 | ///
269 | /// Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
270 | ///
271 | /// Output stream
272 | /// Input stream
273 | /// How many bytes to read and write at time, default is 1024
274 | /// Chosen SIMD mode (default is auto-detect)
275 | public void EncryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, SimdMode simdMode = SimdMode.AutoDetect)
276 | {
277 | if (simdMode == SimdMode.AutoDetect)
278 | {
279 | simdMode = DetectSimdMode();
280 | }
281 |
282 | this.WorkStreams(output, input, simdMode, howManyBytesToProcessAtTime);
283 | }
284 |
285 | ///
286 | /// Async encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
287 | ///
288 | /// Output stream
289 | /// Input stream
290 | /// How many bytes to read and write at time, default is 1024
291 | /// Chosen SIMD mode (default is auto-detect)
292 | public async Task EncryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, SimdMode simdMode = SimdMode.AutoDetect)
293 | {
294 | if (simdMode == SimdMode.AutoDetect)
295 | {
296 | simdMode = DetectSimdMode();
297 | }
298 |
299 | await this.WorkStreamsAsync(output, input, simdMode, howManyBytesToProcessAtTime);
300 | }
301 |
302 | ///
303 | /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.
304 | ///
305 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
306 | /// Output byte array, must have enough bytes
307 | /// Input byte array
308 | /// Chosen SIMD mode (default is auto-detect)
309 | public void EncryptBytes(byte[] output, byte[] input, SimdMode simdMode = SimdMode.AutoDetect)
310 | {
311 | if (output == null)
312 | {
313 | throw new ArgumentNullException("output", "Output cannot be null");
314 | }
315 |
316 | if (input == null)
317 | {
318 | throw new ArgumentNullException("input", "Input cannot be null");
319 | }
320 |
321 | if (simdMode == SimdMode.AutoDetect)
322 | {
323 | simdMode = DetectSimdMode();
324 | }
325 |
326 | this.WorkBytes(output, input, input.Length, simdMode);
327 | }
328 |
329 | ///
330 | /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
331 | ///
332 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
333 | /// Input byte array
334 | /// Number of bytes to encrypt
335 | /// Chosen SIMD mode (default is auto-detect)
336 | /// Byte array that contains encrypted bytes
337 | public byte[] EncryptBytes(byte[] input, int numBytes, SimdMode simdMode = SimdMode.AutoDetect)
338 | {
339 | if (input == null)
340 | {
341 | throw new ArgumentNullException("input", "Input cannot be null");
342 | }
343 |
344 | if (numBytes < 0 || numBytes > input.Length)
345 | {
346 | throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]");
347 | }
348 |
349 | if (simdMode == SimdMode.AutoDetect)
350 | {
351 | simdMode = DetectSimdMode();
352 | }
353 |
354 | byte[] returnArray = new byte[numBytes];
355 | this.WorkBytes(returnArray, input, numBytes, simdMode);
356 | return returnArray;
357 | }
358 |
359 | ///
360 | /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
361 | ///
362 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
363 | /// Input byte array
364 | /// Chosen SIMD mode (default is auto-detect)
365 | /// Byte array that contains encrypted bytes
366 | public byte[] EncryptBytes(byte[] input, SimdMode simdMode = SimdMode.AutoDetect)
367 | {
368 | if (input == null)
369 | {
370 | throw new ArgumentNullException("input", "Input cannot be null");
371 | }
372 |
373 | if (simdMode == SimdMode.AutoDetect)
374 | {
375 | simdMode = DetectSimdMode();
376 | }
377 |
378 | byte[] returnArray = new byte[input.Length];
379 | this.WorkBytes(returnArray, input, input.Length, simdMode);
380 | return returnArray;
381 | }
382 |
383 | ///
384 | /// Encrypt string as UTF8 byte array, returns byte array that is allocated by method.
385 | ///
386 | /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform
387 | /// Input string
388 | /// Chosen SIMD mode (default is auto-detect)
389 | /// Byte array that contains encrypted bytes
390 | public byte[] EncryptString(string input, SimdMode simdMode = SimdMode.AutoDetect)
391 | {
392 | if (input == null)
393 | {
394 | throw new ArgumentNullException("input", "Input cannot be null");
395 | }
396 |
397 | if (simdMode == SimdMode.AutoDetect)
398 | {
399 | simdMode = DetectSimdMode();
400 | }
401 |
402 | byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input);
403 | byte[] returnArray = new byte[utf8Bytes.Length];
404 |
405 | this.WorkBytes(returnArray, utf8Bytes, utf8Bytes.Length, simdMode);
406 | return returnArray;
407 | }
408 |
409 | #endregion // Encryption methods
410 |
411 |
412 | #region // Decryption methods
413 |
414 | ///
415 | /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to the output buffer.
416 | ///
417 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
418 | /// Output byte array
419 | /// Input byte array
420 | /// Number of bytes to decrypt
421 | /// Chosen SIMD mode (default is auto-detect)
422 | public void DecryptBytes(byte[] output, byte[] input, int numBytes, SimdMode simdMode = SimdMode.AutoDetect)
423 | {
424 | if (output == null)
425 | {
426 | throw new ArgumentNullException("output", "Output cannot be null");
427 | }
428 |
429 | if (input == null)
430 | {
431 | throw new ArgumentNullException("input", "Input cannot be null");
432 | }
433 |
434 | if (numBytes < 0 || numBytes > input.Length)
435 | {
436 | throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]");
437 | }
438 |
439 | if (output.Length < numBytes)
440 | {
441 | throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}");
442 | }
443 |
444 | if (simdMode == SimdMode.AutoDetect)
445 | {
446 | simdMode = DetectSimdMode();
447 | }
448 |
449 | this.WorkBytes(output, input, numBytes, simdMode);
450 | }
451 |
452 | ///
453 | /// Decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
454 | ///
455 | /// Output stream
456 | /// Input stream
457 | /// How many bytes to read and write at time, default is 1024
458 | /// Chosen SIMD mode (default is auto-detect)
459 | public void DecryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, SimdMode simdMode = SimdMode.AutoDetect)
460 | {
461 | if (simdMode == SimdMode.AutoDetect)
462 | {
463 | simdMode = DetectSimdMode();
464 | }
465 |
466 | this.WorkStreams(output, input, simdMode, howManyBytesToProcessAtTime);
467 | }
468 |
469 | ///
470 | /// Async decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
471 | ///
472 | /// Output stream
473 | /// Input stream
474 | /// How many bytes to read and write at time, default is 1024
475 | /// Chosen SIMD mode (default is auto-detect)
476 | public async Task DecryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, SimdMode simdMode = SimdMode.AutoDetect)
477 | {
478 | if (simdMode == SimdMode.AutoDetect)
479 | {
480 | simdMode = DetectSimdMode();
481 | }
482 |
483 | await this.WorkStreamsAsync(output, input, simdMode, howManyBytesToProcessAtTime);
484 | }
485 |
486 | ///
487 | /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.
488 | ///
489 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
490 | /// Output byte array, must have enough bytes
491 | /// Input byte array
492 | /// Chosen SIMD mode (default is auto-detect)
493 | public void DecryptBytes(byte[] output, byte[] input, SimdMode simdMode = SimdMode.AutoDetect)
494 | {
495 | if (output == null)
496 | {
497 | throw new ArgumentNullException("output", "Output cannot be null");
498 | }
499 |
500 | if (input == null)
501 | {
502 | throw new ArgumentNullException("input", "Input cannot be null");
503 | }
504 |
505 | if (simdMode == SimdMode.AutoDetect)
506 | {
507 | simdMode = DetectSimdMode();
508 | }
509 |
510 | WorkBytes(output, input, input.Length, simdMode);
511 | }
512 |
513 | ///
514 | /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
515 | ///
516 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
517 | /// Input byte array
518 | /// Number of bytes to encrypt
519 | /// Chosen SIMD mode (default is auto-detect)
520 | /// Byte array that contains decrypted bytes
521 | public byte[] DecryptBytes(byte[] input, int numBytes, SimdMode simdMode = SimdMode.AutoDetect)
522 | {
523 | if (input == null)
524 | {
525 | throw new ArgumentNullException("input", "Input cannot be null");
526 | }
527 |
528 | if (numBytes < 0 || numBytes > input.Length)
529 | {
530 | throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]");
531 | }
532 |
533 | if (simdMode == SimdMode.AutoDetect)
534 | {
535 | simdMode = DetectSimdMode();
536 | }
537 |
538 | byte[] returnArray = new byte[numBytes];
539 | WorkBytes(returnArray, input, numBytes, simdMode);
540 | return returnArray;
541 | }
542 |
543 | ///
544 | /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
545 | ///
546 | /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method
547 | /// Input byte array
548 | /// Chosen SIMD mode (default is auto-detect)
549 | /// Byte array that contains decrypted bytes
550 | public byte[] DecryptBytes(byte[] input, SimdMode simdMode = SimdMode.AutoDetect)
551 | {
552 | if (input == null)
553 | {
554 | throw new ArgumentNullException("input", "Input cannot be null");
555 | }
556 |
557 | if (simdMode == SimdMode.AutoDetect)
558 | {
559 | simdMode = DetectSimdMode();
560 | }
561 |
562 | byte[] returnArray = new byte[input.Length];
563 | WorkBytes(returnArray, input, input.Length, simdMode);
564 | return returnArray;
565 | }
566 |
567 | ///
568 | /// Decrypt UTF8 byte array to string.
569 | ///
570 | /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform
571 | /// Byte array
572 | /// Chosen SIMD mode (default is auto-detect)
573 | /// Byte array that contains encrypted bytes
574 | public string DecryptUTF8ByteArray(byte[] input, SimdMode simdMode = SimdMode.AutoDetect)
575 | {
576 | if (input == null)
577 | {
578 | throw new ArgumentNullException("input", "Input cannot be null");
579 | }
580 |
581 | if (simdMode == SimdMode.AutoDetect)
582 | {
583 | simdMode = DetectSimdMode();
584 | }
585 |
586 | byte[] tempArray = new byte[input.Length];
587 |
588 | WorkBytes(tempArray, input, input.Length, simdMode);
589 | return System.Text.Encoding.UTF8.GetString(tempArray);
590 | }
591 |
592 | #endregion // Decryption methods
593 |
594 | private void WorkStreams(Stream output, Stream input, SimdMode simdMode, int howManyBytesToProcessAtTime = 1024)
595 | {
596 | int readBytes;
597 |
598 | byte[] inputBuffer = new byte[howManyBytesToProcessAtTime];
599 | byte[] outputBuffer = new byte[howManyBytesToProcessAtTime];
600 |
601 | while ((readBytes = input.Read(inputBuffer, 0, howManyBytesToProcessAtTime)) > 0)
602 | {
603 | // Encrypt or decrypt
604 | WorkBytes(output: outputBuffer, input: inputBuffer, numBytes: readBytes, simdMode);
605 |
606 | // Write buffer
607 | output.Write(outputBuffer, 0, readBytes);
608 | }
609 | }
610 |
611 | private async Task WorkStreamsAsync(Stream output, Stream input, SimdMode simdMode, int howManyBytesToProcessAtTime = 1024)
612 | {
613 | byte[] readBytesBuffer = new byte[howManyBytesToProcessAtTime];
614 | byte[] writeBytesBuffer = new byte[howManyBytesToProcessAtTime];
615 | int howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime);
616 |
617 | while (howManyBytesWereRead > 0)
618 | {
619 | // Encrypt or decrypt
620 | WorkBytes(output: writeBytesBuffer, input: readBytesBuffer, numBytes: howManyBytesWereRead, simdMode);
621 |
622 | // Write
623 | await output.WriteAsync(writeBytesBuffer, 0, howManyBytesWereRead);
624 |
625 | // Read more
626 | howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime);
627 | }
628 | }
629 |
630 | ///
631 | /// Encrypt or decrypt an arbitrary-length byte array (input), writing the resulting byte array to the output buffer. The number of bytes to read from the input buffer is determined by numBytes.
632 | ///
633 | /// Output byte array
634 | /// Input byte array
635 | /// How many bytes to process
636 | /// Chosen SIMD mode (default is auto-detect)
637 | private void WorkBytes(byte[] output, byte[] input, int numBytes, SimdMode simdMode)
638 | {
639 | if (isDisposed)
640 | {
641 | throw new ObjectDisposedException("state", "The ChaCha state has been disposed");
642 | }
643 |
644 | uint[] x = new uint[stateLength]; // Working buffer
645 | byte[] tmp = new byte[processBytesAtTime]; // Temporary buffer
646 | int offset = 0;
647 |
648 | int howManyFullLoops = numBytes / processBytesAtTime;
649 | int tailByteCount = numBytes - (howManyFullLoops * processBytesAtTime);
650 |
651 | for (int loop = 0; loop < howManyFullLoops; loop++)
652 | {
653 | UpdateStateAndGenerateTemporaryBuffer(this.state, x, tmp);
654 |
655 | if (simdMode == SimdMode.V512)
656 | {
657 | // 1 x 64 bytes
658 | Vector512 inputV = Vector512.Create(input, offset);
659 | Vector512 tmpV = Vector512.Create(tmp, 0);
660 | Vector512 outputV = inputV ^ tmpV;
661 | outputV.CopyTo(output, offset);
662 | }
663 | else if (simdMode == SimdMode.V256)
664 | {
665 | // 2 x 32 bytes
666 | Vector256 inputV = Vector256.Create(input, offset);
667 | Vector256 tmpV = Vector256.Create(tmp, 0);
668 | Vector256 outputV = inputV ^ tmpV;
669 | outputV.CopyTo(output, offset);
670 |
671 | inputV = Vector256.Create(input, offset + 32);
672 | tmpV = Vector256.Create(tmp, 32);
673 | outputV = inputV ^ tmpV;
674 | outputV.CopyTo(output, offset + 32);
675 | }
676 | else if (simdMode == SimdMode.V128)
677 | {
678 | // 4 x 16 bytes
679 | Vector128 inputV = Vector128.Create(input, offset);
680 | Vector128 tmpV = Vector128.Create(tmp, 0);
681 | Vector128 outputV = inputV ^ tmpV;
682 | outputV.CopyTo(output, offset);
683 |
684 | inputV = Vector128.Create(input, offset + 16);
685 | tmpV = Vector128.Create(tmp, 16);
686 | outputV = inputV ^ tmpV;
687 | outputV.CopyTo(output, offset + 16);
688 |
689 | inputV = Vector128.Create(input, offset + 32);
690 | tmpV = Vector128.Create(tmp, 32);
691 | outputV = inputV ^ tmpV;
692 | outputV.CopyTo(output, offset + 32);
693 |
694 | inputV = Vector128.Create(input, offset + 48);
695 | tmpV = Vector128.Create(tmp, 48);
696 | outputV = inputV ^ tmpV;
697 | outputV.CopyTo(output, offset + 48);
698 | }
699 | else
700 | {
701 | for (int i = 0; i < processBytesAtTime; i+=4 )
702 | {
703 | // Small unroll
704 | int start = i + offset;
705 | output[start] = (byte) (input[start] ^ tmp[i]);
706 | output[start + 1] = (byte) (input[start + 1] ^ tmp[i + 1]);
707 | output[start + 2] = (byte) (input[start + 2] ^ tmp[i + 2]);
708 | output[start + 3] = (byte) (input[start + 3] ^ tmp[i + 3]);
709 | }
710 | }
711 |
712 | offset += processBytesAtTime;
713 | }
714 |
715 | // In case there are some bytes left
716 | if (tailByteCount > 0)
717 | {
718 | UpdateStateAndGenerateTemporaryBuffer(this.state, x, tmp);
719 |
720 | for (int i = 0; i < tailByteCount; i++)
721 | {
722 | output[i + offset] = (byte) (input[i + offset] ^ tmp[i]);
723 | }
724 | }
725 | }
726 |
727 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
728 | private static void UpdateStateAndGenerateTemporaryBuffer(uint[] stateToModify, uint[] workingBuffer, byte[] temporaryBuffer)
729 | {
730 | // Copy state to working buffer
731 | Buffer.BlockCopy(stateToModify, 0, workingBuffer, 0, stateLength * sizeof(uint));
732 |
733 | for (int i = 0; i < 10; i++)
734 | {
735 | QuarterRound(workingBuffer, 0, 4, 8, 12);
736 | QuarterRound(workingBuffer, 1, 5, 9, 13);
737 | QuarterRound(workingBuffer, 2, 6, 10, 14);
738 | QuarterRound(workingBuffer, 3, 7, 11, 15);
739 |
740 | QuarterRound(workingBuffer, 0, 5, 10, 15);
741 | QuarterRound(workingBuffer, 1, 6, 11, 12);
742 | QuarterRound(workingBuffer, 2, 7, 8, 13);
743 | QuarterRound(workingBuffer, 3, 4, 9, 14);
744 | }
745 |
746 | for (int i = 0; i < stateLength; i++)
747 | {
748 | Util.ToBytes(temporaryBuffer, Util.Add(workingBuffer[i], stateToModify[i]), 4 * i);
749 | }
750 |
751 | stateToModify[12] = Util.AddOne(stateToModify[12]);
752 | if (stateToModify[12] <= 0)
753 | {
754 | /* Stopping at 2^70 bytes per nonce is the user's responsibility */
755 | stateToModify[13] = Util.AddOne(stateToModify[13]);
756 | }
757 | }
758 |
759 | ///
760 | /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned integers within the given buffer at indices a, b, c, and d.
761 | ///
762 | ///
763 | /// The ChaCha state does not have four integer numbers: it has 16. So the quarter-round operation works on only four of them -- hence the name. Each quarter round operates on four predetermined numbers in the ChaCha state.
764 | /// See ChaCha20 Spec Sections 2.1 - 2.2.
765 | ///
766 | /// A ChaCha state (vector). Must contain 16 elements.
767 | /// Index of the first number
768 | /// Index of the second number
769 | /// Index of the third number
770 | /// Index of the fourth number
771 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
772 | private static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d)
773 | {
774 | x[a] = Util.Add(x[a], x[b]);
775 | x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16);
776 |
777 | x[c] = Util.Add(x[c], x[d]);
778 | x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12);
779 |
780 | x[a] = Util.Add(x[a], x[b]);
781 | x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8);
782 |
783 | x[c] = Util.Add(x[c], x[d]);
784 | x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7);
785 | }
786 |
787 | #region Destructor and Disposer
788 |
789 | ///
790 | /// Clear and dispose of the internal state. The finalizer is only called if Dispose() was never called on this cipher.
791 | ///
792 | ~ChaCha20()
793 | {
794 | Dispose(false);
795 | }
796 |
797 | ///
798 | /// Clear and dispose of the internal state. Also request the GC not to call the finalizer, because all cleanup has been taken care of.
799 | ///
800 | public void Dispose()
801 | {
802 | Dispose(true);
803 | /*
804 | * The Garbage Collector does not need to invoke the finalizer because Dispose(bool) has already done all the cleanup needed.
805 | */
806 | GC.SuppressFinalize(this);
807 | }
808 |
809 | ///
810 | /// This method should only be invoked from Dispose() or the finalizer. This handles the actual cleanup of the resources.
811 | ///
812 | ///
813 | /// Should be true if called by Dispose(); false if called by the finalizer
814 | ///
815 | private void Dispose(bool disposing)
816 | {
817 | if (!isDisposed)
818 | {
819 | if (disposing)
820 | {
821 | /* Cleanup managed objects by calling their Dispose() methods */
822 | }
823 |
824 | /* Cleanup any unmanaged objects here */
825 | Array.Clear(state, 0, stateLength);
826 | }
827 |
828 | isDisposed = true;
829 | }
830 |
831 | #endregion // Destructor and Disposer
832 | }
833 |
834 | ///
835 | /// Utilities that are used during compression
836 | ///
837 | public static class Util
838 | {
839 | ///
840 | /// n-bit left rotation operation (towards the high bits) for 32-bit integers.
841 | ///
842 | ///
843 | ///
844 | /// The result of (v LEFTSHIFT c)
845 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
846 | public static uint Rotate(uint v, int c)
847 | {
848 | unchecked
849 | {
850 | return (v << c) | (v >> (32 - c));
851 | }
852 | }
853 |
854 | ///
855 | /// Unchecked integer exclusive or (XOR) operation.
856 | ///
857 | ///
858 | ///
859 | /// The result of (v XOR w)
860 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
861 | public static uint XOr(uint v, uint w)
862 | {
863 | return unchecked(v ^ w);
864 | }
865 |
866 | ///
867 | /// Unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32.
868 | ///
869 | ///
870 | /// See ChaCha20 Spec Section 2.1.
871 | ///
872 | ///
873 | ///
874 | /// The result of (v + w) modulo 2^32
875 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
876 | public static uint Add(uint v, uint w)
877 | {
878 | return unchecked(v + w);
879 | }
880 |
881 | ///
882 | /// Add 1 to the input parameter using unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32.
883 | ///
884 | ///
885 | /// See ChaCha20 Spec Section 2.1.
886 | ///
887 | ///
888 | /// The result of (v + 1) modulo 2^32
889 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
890 | public static uint AddOne(uint v)
891 | {
892 | return unchecked(v + 1);
893 | }
894 |
895 | ///
896 | /// Convert four bytes of the input buffer into an unsigned 32-bit integer, beginning at the inputOffset.
897 | ///
898 | ///
899 | ///
900 | /// An unsigned 32-bit integer
901 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
902 | public static uint U8To32Little(byte[] p, int inputOffset)
903 | {
904 | unchecked
905 | {
906 | return ((uint) p[inputOffset]
907 | | ((uint) p[inputOffset + 1] << 8)
908 | | ((uint) p[inputOffset + 2] << 16)
909 | | ((uint) p[inputOffset + 3] << 24));
910 | }
911 | }
912 |
913 | ///
914 | /// Serialize the input integer into the output buffer. The input integer will be split into 4 bytes and put into four sequential places in the output buffer, starting at the outputOffset.
915 | ///
916 | ///
917 | ///
918 | ///
919 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
920 | public static void ToBytes(byte[] output, uint input, int outputOffset)
921 | {
922 | unchecked
923 | {
924 | output[outputOffset] = (byte) input;
925 | output[outputOffset + 1] = (byte) (input >> 8);
926 | output[outputOffset + 2] = (byte) (input >> 16);
927 | output[outputOffset + 3] = (byte) (input >> 24);
928 | }
929 | }
930 | }
931 |
--------------------------------------------------------------------------------
/src/ChaCha20-NetStandard.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | LibChaCha20
6 | 1.1.0
7 | $(VersionSuffix)
8 | Kaarlo Räihä
9 | Managed C# .NET (.NET 8) library for ChaCha20 encrypting and decrypting
10 | true
11 | https://github.com/mcraiha/CSharp-ChaCha20-NetStandard
12 | https://github.com/mcraiha/CSharp-ChaCha20-NetStandard.git
13 | true
14 | true
15 | ISC
16 | nuget-readme.md
17 | true
18 | true
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | all
28 | runtime; build; native; contentfiles; analyzers
29 |
30 |
31 |
32 |
33 | true
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/ChaCha20Tests.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 | using CSChaCha20;
3 | using System;
4 | using System.IO;
5 | using System.Threading.Tasks;
6 |
7 | namespace Tests
8 | {
9 | public class ChaCha20Tests
10 | {
11 | ///
12 | /// 32 bytes equals 256 bits
13 | ///
14 | private const int validKeyLength = 32;
15 |
16 | ///
17 | /// 12 bytes equals 96 bits
18 | ///
19 | private const int validNonceLength = 12;
20 |
21 | [SetUp]
22 | public void Setup()
23 | {
24 | }
25 |
26 | [Test]
27 | public void FailedKeyInits()
28 | {
29 | // Arrange
30 | byte[] invalidKey1 = null;
31 | byte[] invalidKey2 = new byte[0];
32 | byte[] invalidKey3 = new byte[15];
33 | byte[] invalidKey4 = new byte[31];
34 |
35 | byte[] validNonce = new byte[12];
36 |
37 | uint validCounter = 1;
38 |
39 | // Act
40 |
41 | // Assert
42 | Assert.That(() => new ChaCha20(invalidKey1, validNonce, validCounter), Throws.ArgumentNullException);
43 | Assert.That(() => new ChaCha20(invalidKey2, validNonce, validCounter), Throws.ArgumentException);
44 | Assert.That(() => new ChaCha20(invalidKey3, validNonce, validCounter), Throws.ArgumentException);
45 | Assert.That(() => new ChaCha20(invalidKey4, validNonce, validCounter), Throws.ArgumentException);
46 | }
47 |
48 | [Test]
49 | public void FailedNonceInits()
50 | {
51 | // Arrange
52 | byte[] validKey = new byte[32];
53 |
54 | byte[] invalidNonce1 = null;
55 | byte[] invalidNonce2 = new byte[0];
56 | byte[] invalidNonce3 = new byte[11];
57 |
58 | uint validCounter = 1;
59 |
60 | // Act
61 |
62 | // Assert
63 | Assert.That(() => new ChaCha20(validKey, invalidNonce1, validCounter), Throws.ArgumentNullException);
64 | Assert.That(() => new ChaCha20(validKey, invalidNonce2, validCounter), Throws.ArgumentException);
65 | Assert.That(() => new ChaCha20(validKey, invalidNonce3, validCounter), Throws.ArgumentException);
66 | }
67 |
68 | [Test]
69 | public void FailedInputOrOutput()
70 | {
71 | // Arrange
72 | byte[] key = new byte[validKeyLength];
73 | byte[] nonce = new byte[validNonceLength];
74 |
75 | uint counter = 1;
76 |
77 | const int lengthOfData = 128;
78 |
79 | byte[] validOutputArray = new byte[lengthOfData];
80 | byte[] validInputArray = new byte[lengthOfData];
81 |
82 | byte[] invalidInput1 = null;
83 | byte[] invalidOutput1 = null;
84 |
85 | ChaCha20 nullInput = new ChaCha20(key, nonce, counter);
86 | ChaCha20 nullOutput = new ChaCha20(key, nonce, counter);
87 |
88 | // Act
89 |
90 | // Assert
91 | Assert.That(() => nullInput.EncryptBytes(validOutputArray, invalidInput1, lengthOfData), Throws.ArgumentNullException);
92 | Assert.That(() => nullInput.EncryptBytes(invalidOutput1, validInputArray, lengthOfData), Throws.ArgumentNullException);
93 |
94 | Assert.Throws(() => nullInput.EncryptBytes(validOutputArray, validInputArray, -1));
95 | Assert.Throws(() => nullInput.EncryptBytes(validOutputArray, validInputArray, lengthOfData + 1));
96 | Assert.Throws(() => nullInput.EncryptBytes(new byte[lengthOfData/2], validInputArray, lengthOfData));
97 |
98 | }
99 |
100 | [Test]
101 | [TestCase(SimdMode.AutoDetect)]
102 | [TestCase(SimdMode.V128)]
103 | [TestCase(SimdMode.V256)]
104 | [TestCase(SimdMode.V512)]
105 | [TestCase(SimdMode.None)]
106 | public void BasicByteArrayEncryptDecryptWorkflow(SimdMode simdMode)
107 | {
108 | // Arrange
109 | Random rng = new Random(Seed: 1337);
110 |
111 | byte[] key = new byte[validKeyLength];
112 | byte[] nonce = new byte[validNonceLength];
113 |
114 | const int lengthOfData = 4096;
115 | byte[] randomContent = new byte[lengthOfData];
116 | byte[] encryptedContent = new byte[lengthOfData];
117 | byte[] decryptedContent = new byte[lengthOfData];
118 |
119 | uint counter = 1;
120 |
121 | ChaCha20 forEncrypting = null;
122 | ChaCha20 forDecrypting = null;
123 |
124 | // Act
125 | rng.NextBytes(key);
126 | rng.NextBytes(nonce);
127 | rng.NextBytes(randomContent);
128 |
129 | forEncrypting = new ChaCha20(key, nonce, counter);
130 | forDecrypting = new ChaCha20(key, nonce, counter);
131 |
132 | forEncrypting.EncryptBytes(encryptedContent, randomContent, lengthOfData, simdMode);
133 | forDecrypting.DecryptBytes(decryptedContent, encryptedContent, lengthOfData, simdMode);
134 |
135 | // Assert
136 | Assert.AreEqual(lengthOfData, encryptedContent.Length);
137 | Assert.AreEqual(lengthOfData, decryptedContent.Length);
138 |
139 | CollectionAssert.AreEqual(randomContent, decryptedContent);
140 | CollectionAssert.AreNotEqual(randomContent, encryptedContent);
141 | }
142 |
143 | [Test]
144 | [TestCase(SimdMode.AutoDetect)]
145 | [TestCase(SimdMode.V128)]
146 | [TestCase(SimdMode.V256)]
147 | [TestCase(SimdMode.V512)]
148 | [TestCase(SimdMode.None)]
149 | public void BasicByteArrayEncryptDecryptWorkflowNonPowerOfTwo(SimdMode simdMode)
150 | {
151 | // Arrange
152 | Random rng = new Random(Seed: 1339);
153 |
154 | byte[] key = new byte[validKeyLength];
155 | byte[] nonce = new byte[validNonceLength];
156 |
157 | const int lengthOfData = 13337;
158 | byte[] randomContent = new byte[lengthOfData];
159 | byte[] encryptedContent = new byte[lengthOfData];
160 | byte[] decryptedContent = new byte[lengthOfData];
161 |
162 | uint counter = 1;
163 |
164 | ChaCha20 forEncrypting = null;
165 | ChaCha20 forDecrypting = null;
166 |
167 | // Act
168 | rng.NextBytes(key);
169 | rng.NextBytes(nonce);
170 | rng.NextBytes(randomContent);
171 |
172 | forEncrypting = new ChaCha20(key, nonce, counter);
173 | forDecrypting = new ChaCha20(key, nonce, counter);
174 |
175 | forEncrypting.EncryptBytes(encryptedContent, randomContent, lengthOfData, simdMode);
176 | forDecrypting.DecryptBytes(decryptedContent, encryptedContent, lengthOfData, simdMode);
177 |
178 | // Assert
179 | Assert.AreEqual(lengthOfData, encryptedContent.Length);
180 | Assert.AreEqual(lengthOfData, decryptedContent.Length);
181 |
182 | CollectionAssert.AreEqual(randomContent, decryptedContent);
183 | CollectionAssert.AreNotEqual(randomContent, encryptedContent);
184 | }
185 |
186 | [Test]
187 | public void BasicStreamEncryptDecryptWorkflow()
188 | {
189 | // Arrange
190 | Random rng = new Random(Seed: 1338);
191 |
192 | byte[] key = new byte[validKeyLength];
193 | byte[] nonce = new byte[validNonceLength];
194 |
195 | const int lengthOfData = 4096;
196 | byte[] randomContent = new byte[lengthOfData];
197 | byte[] encryptedContent = new byte[lengthOfData];
198 | byte[] decryptedContent = new byte[lengthOfData];
199 |
200 | uint counter = 1;
201 |
202 | ChaCha20 forEncrypting = null;
203 | ChaCha20 forDecrypting = null;
204 |
205 | // Act
206 | rng.NextBytes(key);
207 | rng.NextBytes(nonce);
208 | rng.NextBytes(randomContent);
209 |
210 | forEncrypting = new ChaCha20(key, nonce, counter);
211 | forDecrypting = new ChaCha20(key, nonce, counter);
212 |
213 | forEncrypting.EncryptStream(new MemoryStream(encryptedContent), new MemoryStream(randomContent));
214 | forDecrypting.DecryptStream(new MemoryStream(decryptedContent), new MemoryStream(encryptedContent));
215 |
216 | // Assert
217 | Assert.AreEqual(lengthOfData, encryptedContent.Length);
218 | Assert.AreEqual(lengthOfData, decryptedContent.Length);
219 |
220 | CollectionAssert.AreEqual(randomContent, decryptedContent);
221 | CollectionAssert.AreNotEqual(randomContent, encryptedContent);
222 | }
223 |
224 | [Test]
225 | public async Task AsyncStreamEncryptDecryptWorkflow()
226 | {
227 | // Arrange
228 | Random rng = new Random(Seed: 1338);
229 |
230 | byte[] key = new byte[validKeyLength];
231 | byte[] nonce = new byte[validNonceLength];
232 |
233 | const int lengthOfData = 4096;
234 | byte[] randomContent = new byte[lengthOfData];
235 | byte[] encryptedContent = new byte[lengthOfData];
236 | byte[] decryptedContent = new byte[lengthOfData];
237 |
238 | uint counter = 1;
239 |
240 | ChaCha20 forEncrypting = null;
241 | ChaCha20 forDecrypting = null;
242 |
243 | // Act
244 | rng.NextBytes(key);
245 | rng.NextBytes(nonce);
246 | rng.NextBytes(randomContent);
247 |
248 | forEncrypting = new ChaCha20(key, nonce, counter);
249 | forDecrypting = new ChaCha20(key, nonce, counter);
250 |
251 | await forEncrypting.EncryptStreamAsync(new MemoryStream(encryptedContent), new MemoryStream(randomContent));
252 | await forDecrypting.DecryptStreamAsync(new MemoryStream(decryptedContent), new MemoryStream(encryptedContent));
253 |
254 | // Assert
255 | Assert.AreEqual(lengthOfData, encryptedContent.Length);
256 | Assert.AreEqual(lengthOfData, decryptedContent.Length);
257 |
258 | CollectionAssert.AreEqual(randomContent, decryptedContent);
259 | CollectionAssert.AreNotEqual(randomContent, encryptedContent);
260 | }
261 |
262 | [Test]
263 | public void BasicStreamEncryptDecryptWorkflowNonPowerOfTwo()
264 | {
265 | // Arrange
266 | Random rng = new Random(Seed: 138);
267 |
268 | byte[] key = new byte[validKeyLength];
269 | byte[] nonce = new byte[validNonceLength];
270 |
271 | const int lengthOfData = 13339;
272 | byte[] randomContent = new byte[lengthOfData];
273 | byte[] encryptedContent = new byte[lengthOfData];
274 | byte[] decryptedContent = new byte[lengthOfData];
275 |
276 | uint counter = 1;
277 |
278 | ChaCha20 forEncrypting = null;
279 | ChaCha20 forDecrypting = null;
280 |
281 | // Act
282 | rng.NextBytes(key);
283 | rng.NextBytes(nonce);
284 | rng.NextBytes(randomContent);
285 |
286 | forEncrypting = new ChaCha20(key, nonce, counter);
287 | forDecrypting = new ChaCha20(key, nonce, counter);
288 |
289 | forEncrypting.EncryptStream(new MemoryStream(encryptedContent), new MemoryStream(randomContent));
290 | forDecrypting.DecryptStream(new MemoryStream(decryptedContent), new MemoryStream(encryptedContent));
291 |
292 | // Assert
293 | Assert.AreEqual(lengthOfData, encryptedContent.Length);
294 | Assert.AreEqual(lengthOfData, decryptedContent.Length);
295 |
296 | CollectionAssert.AreEqual(randomContent, decryptedContent);
297 | CollectionAssert.AreNotEqual(randomContent, encryptedContent);
298 | }
299 |
300 | [Test]
301 | public async Task AsyncStreamEncryptDecryptWorkflowNonPowerOfTwo()
302 | {
303 | // Arrange
304 | Random rng = new Random(Seed: 139);
305 |
306 | byte[] key = new byte[validKeyLength];
307 | byte[] nonce = new byte[validNonceLength];
308 |
309 | const int lengthOfData = 13339;
310 | byte[] randomContent = new byte[lengthOfData];
311 | byte[] encryptedContent = new byte[lengthOfData];
312 | byte[] decryptedContent = new byte[lengthOfData];
313 |
314 | uint counter = 1;
315 |
316 | ChaCha20 forEncrypting = null;
317 | ChaCha20 forDecrypting = null;
318 |
319 | // Act
320 | rng.NextBytes(key);
321 | rng.NextBytes(nonce);
322 | rng.NextBytes(randomContent);
323 |
324 | forEncrypting = new ChaCha20(key, nonce, counter);
325 | forDecrypting = new ChaCha20(key, nonce, counter);
326 |
327 | await forEncrypting.EncryptStreamAsync(new MemoryStream(encryptedContent), new MemoryStream(randomContent));
328 | await forDecrypting.DecryptStreamAsync(new MemoryStream(decryptedContent), new MemoryStream(encryptedContent));
329 |
330 | // Assert
331 | Assert.AreEqual(lengthOfData, encryptedContent.Length);
332 | Assert.AreEqual(lengthOfData, decryptedContent.Length);
333 |
334 | CollectionAssert.AreEqual(randomContent, decryptedContent);
335 | CollectionAssert.AreNotEqual(randomContent, encryptedContent);
336 | }
337 |
338 | [Test]
339 | public void TestOverloads()
340 | {
341 | // Arrange
342 | Random rng = new Random(Seed: 1337);
343 |
344 | byte[] key = new byte[validKeyLength];
345 | byte[] nonce = new byte[validNonceLength];
346 |
347 | uint counter = 1;
348 |
349 | const int lengthOfData = 4096;
350 | byte[] randomContent = new byte[lengthOfData];
351 |
352 | byte[] encryptedContent1 = new byte[lengthOfData];
353 | byte[] decryptedContent1 = new byte[lengthOfData];
354 |
355 | byte[] encryptedContent2 = null;
356 | byte[] decryptedContent2 = null;
357 |
358 | byte[] encryptedContent3 = null;
359 | byte[] decryptedContent3 = null;
360 |
361 | ChaCha20 forEncrypting1 = null;
362 | ChaCha20 forDecrypting1 = null;
363 |
364 | ChaCha20 forEncrypting2 = null;
365 | ChaCha20 forDecrypting2 = null;
366 |
367 | ChaCha20 forEncrypting3 = null;
368 | ChaCha20 forDecrypting3 = null;
369 |
370 | // Act
371 | rng.NextBytes(key);
372 | rng.NextBytes(nonce);
373 | rng.NextBytes(randomContent);
374 |
375 | forEncrypting1 = new ChaCha20(key, nonce, counter);
376 | forDecrypting1 = new ChaCha20(key, nonce, counter);
377 |
378 | forEncrypting2 = new ChaCha20(key, nonce, counter);
379 | forDecrypting2 = new ChaCha20(key, nonce, counter);
380 |
381 | forEncrypting3 = new ChaCha20(key, nonce, counter);
382 | forDecrypting3 = new ChaCha20(key, nonce, counter);
383 |
384 | forEncrypting1.EncryptBytes(encryptedContent1, randomContent);
385 | forDecrypting1.DecryptBytes(decryptedContent1, encryptedContent1);
386 |
387 | encryptedContent2 = forEncrypting2.EncryptBytes(randomContent, randomContent.Length);
388 | decryptedContent2 = forDecrypting2.DecryptBytes(encryptedContent2, encryptedContent2.Length);
389 |
390 | encryptedContent3 = forEncrypting3.EncryptBytes(randomContent);
391 | decryptedContent3 = forDecrypting3.DecryptBytes(encryptedContent3);
392 |
393 | // Assert
394 | CollectionAssert.AreEqual(randomContent, decryptedContent1);
395 | CollectionAssert.AreNotEqual(randomContent, encryptedContent1);
396 |
397 | CollectionAssert.AreEqual(randomContent, decryptedContent2);
398 | CollectionAssert.AreNotEqual(randomContent, encryptedContent2);
399 |
400 | CollectionAssert.AreEqual(randomContent, decryptedContent3);
401 | CollectionAssert.AreNotEqual(randomContent, encryptedContent3);
402 | }
403 |
404 | [Test]
405 | public void TestStringToUTF8BytesAndBack()
406 | {
407 | // Arrange
408 | Random rng = new Random(Seed: 1337);
409 | byte[] key = new byte[validKeyLength];
410 | byte[] nonce = new byte[validNonceLength];
411 |
412 | uint counter = 1;
413 |
414 | string testContent = "this is test content 😊";
415 |
416 | ChaCha20 forEncrypting1 = null;
417 | ChaCha20 forDecrypting1 = null;
418 |
419 | // Act
420 | rng.NextBytes(key);
421 | rng.NextBytes(nonce);
422 |
423 | forEncrypting1 = new ChaCha20(key, nonce, counter);
424 | forDecrypting1 = new ChaCha20(key, nonce, counter);
425 |
426 | byte[] encryptedContent = forEncrypting1.EncryptString(testContent);
427 | string decryptedString = forDecrypting1.DecryptUTF8ByteArray(encryptedContent);
428 |
429 | // Assert
430 | Assert.AreEqual(testContent, decryptedString);
431 | }
432 |
433 | [Test]
434 | [TestCase(SimdMode.AutoDetect)]
435 | [TestCase(SimdMode.V128)]
436 | [TestCase(SimdMode.V256)]
437 | [TestCase(SimdMode.V512)]
438 | [TestCase(SimdMode.None)]
439 | public void ExistingTestVectors(SimdMode simdMode)
440 | {
441 | // Actual
442 |
443 | // These vectors are from https://github.com/quartzjer/chacha20/blob/master/test/chacha20.js
444 |
445 | byte[] key1 = new byte[validKeyLength] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
446 | byte[] nonce1 = new byte[validNonceLength] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
447 | uint counter1 = 0;
448 | const int lengthOfContent1 = 64;
449 | byte[] content1 = new byte[lengthOfContent1] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
450 | byte[] expected1 = new byte[lengthOfContent1] {
451 | 0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90,
452 | 0x40, 0x5d, 0x6a, 0xe5, 0x53, 0x86, 0xbd, 0x28,
453 | 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a,
454 | 0xa8, 0x36, 0xef, 0xcc, 0x8b, 0x77, 0x0d, 0xc7,
455 | 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, 0x8d,
456 | 0x77, 0x24, 0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37,
457 | 0x6a, 0x43, 0xb8, 0xf4, 0x15, 0x18, 0xa1, 0x1c,
458 | 0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86
459 | };
460 | byte[] output1 = new byte[lengthOfContent1];
461 |
462 |
463 | byte[] key2 = new byte[validKeyLength] {
464 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
465 | 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
466 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
467 | 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
468 | };
469 | byte[] nonce2 = new byte[validNonceLength] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00 };
470 | uint counter2 = 1;
471 | const int lengthOfContent2 = 114;
472 | byte[] content2 = new byte[lengthOfContent2] {
473 | 0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61,
474 | 0x6e, 0x64, 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c,
475 | 0x65, 0x6d, 0x65, 0x6e, 0x20, 0x6f, 0x66, 0x20,
476 | 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x61, 0x73,
477 | 0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39,
478 | 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63,
479 | 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66,
480 | 0x65, 0x72, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6f,
481 | 0x6e, 0x6c, 0x79, 0x20, 0x6f, 0x6e, 0x65, 0x20,
482 | 0x74, 0x69, 0x70, 0x20, 0x66, 0x6f, 0x72, 0x20,
483 | 0x74, 0x68, 0x65, 0x20, 0x66, 0x75, 0x74, 0x75,
484 | 0x72, 0x65, 0x2c, 0x20, 0x73, 0x75, 0x6e, 0x73,
485 | 0x63, 0x72, 0x65, 0x65, 0x6e, 0x20, 0x77, 0x6f,
486 | 0x75, 0x6c, 0x64, 0x20, 0x62, 0x65, 0x20, 0x69,
487 | 0x74, 0x2e
488 | };
489 | byte[] expected2 = new byte[lengthOfContent2] {
490 | 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81,
491 | 0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b,
492 | 0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57,
493 | 0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8,
494 | 0x07, 0xca, 0x0d, 0xbf, 0x50, 0x0d, 0x6a, 0x61, 0x56, 0xa3, 0x8e, 0x08, 0x8a, 0x22, 0xb6, 0x5e,
495 | 0x52, 0xbc, 0x51, 0x4d, 0x16, 0xcc, 0xf8, 0x06, 0x81, 0x8c, 0xe9, 0x1a, 0xb7, 0x79, 0x37, 0x36,
496 | 0x5a, 0xf9, 0x0b, 0xbf, 0x74, 0xa3, 0x5b, 0xe6, 0xb4, 0x0b, 0x8e, 0xed, 0xf2, 0x78, 0x5e, 0x42,
497 | 0x87, 0x4d
498 | };
499 | byte[] output2 = new byte[lengthOfContent2];
500 |
501 |
502 | byte[] key3 = new byte[validKeyLength] {
503 | 0x1c, 0x92, 0x40, 0xa5, 0xeb, 0x55, 0xd3, 0x8a,
504 | 0xf3, 0x33, 0x88, 0x86, 0x04, 0xf6, 0xb5, 0xf0,
505 | 0x47, 0x39, 0x17, 0xc1, 0x40, 0x2b, 0x80, 0x09,
506 | 0x9d, 0xca, 0x5c, 0xbc, 0x20, 0x70, 0x75, 0xc0
507 | };
508 | byte[] nonce3 = new byte[validNonceLength] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 };
509 | uint counter3 = 42;
510 | const int lengthOfContent3 = 127;
511 | byte[] content3 = new byte[lengthOfContent3] {
512 | 0x27, 0x54, 0x77, 0x61, 0x73, 0x20, 0x62, 0x72, 0x69, 0x6c, 0x6c, 0x69, 0x67, 0x2c, 0x20, 0x61,
513 | 0x6e, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x6c, 0x69, 0x74, 0x68, 0x79, 0x20, 0x74, 0x6f,
514 | 0x76, 0x65, 0x73, 0x0a, 0x44, 0x69, 0x64, 0x20, 0x67, 0x79, 0x72, 0x65, 0x20, 0x61, 0x6e, 0x64,
515 | 0x20, 0x67, 0x69, 0x6d, 0x62, 0x6c, 0x65, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77,
516 | 0x61, 0x62, 0x65, 0x3a, 0x0a, 0x41, 0x6c, 0x6c, 0x20, 0x6d, 0x69, 0x6d, 0x73, 0x79, 0x20, 0x77,
517 | 0x65, 0x72, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6f, 0x72, 0x6f, 0x67, 0x6f, 0x76, 0x65,
518 | 0x73, 0x2c, 0x0a, 0x41, 0x6e, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x6f, 0x6d, 0x65, 0x20,
519 | 0x72, 0x61, 0x74, 0x68, 0x73, 0x20, 0x6f, 0x75, 0x74, 0x67, 0x72, 0x61, 0x62, 0x65, 0x2e
520 | };
521 | byte[] expected3 = new byte[lengthOfContent3] {
522 | 0x62, 0xe6, 0x34, 0x7f, 0x95, 0xed, 0x87, 0xa4, 0x5f, 0xfa, 0xe7, 0x42, 0x6f, 0x27, 0xa1, 0xdf,
523 | 0x5f, 0xb6, 0x91, 0x10, 0x04, 0x4c, 0x0d, 0x73, 0x11, 0x8e, 0xff, 0xa9, 0x5b, 0x01, 0xe5, 0xcf,
524 | 0x16, 0x6d, 0x3d, 0xf2, 0xd7, 0x21, 0xca, 0xf9, 0xb2, 0x1e, 0x5f, 0xb1, 0x4c, 0x61, 0x68, 0x71,
525 | 0xfd, 0x84, 0xc5, 0x4f, 0x9d, 0x65, 0xb2, 0x83, 0x19, 0x6c, 0x7f, 0xe4, 0xf6, 0x05, 0x53, 0xeb,
526 | 0xf3, 0x9c, 0x64, 0x02, 0xc4, 0x22, 0x34, 0xe3, 0x2a, 0x35, 0x6b, 0x3e, 0x76, 0x43, 0x12, 0xa6,
527 | 0x1a, 0x55, 0x32, 0x05, 0x57, 0x16, 0xea, 0xd6, 0x96, 0x25, 0x68, 0xf8, 0x7d, 0x3f, 0x3f, 0x77,
528 | 0x04, 0xc6, 0xa8, 0xd1, 0xbc, 0xd1, 0xbf, 0x4d, 0x50, 0xd6, 0x15, 0x4b, 0x6d, 0xa7, 0x31, 0xb1,
529 | 0x87, 0xb5, 0x8d, 0xfd, 0x72, 0x8a, 0xfa, 0x36, 0x75, 0x7a, 0x79, 0x7a, 0xc1, 0x88, 0xd1
530 | };
531 | byte[] output3 = new byte[lengthOfContent3];
532 |
533 |
534 | ChaCha20 forEncrypting1 = new ChaCha20(key1, nonce1, counter1);
535 | ChaCha20 forEncrypting2 = new ChaCha20(key2, nonce2, counter2);
536 | ChaCha20 forEncrypting3 = new ChaCha20(key3, nonce3, counter3);
537 |
538 | // Act
539 | forEncrypting1.EncryptBytes(output1, content1, lengthOfContent1, simdMode);
540 | forEncrypting2.EncryptBytes(output2, content2, lengthOfContent2, simdMode);
541 | forEncrypting3.EncryptBytes(output3, content3, lengthOfContent3, simdMode);
542 |
543 | // Assert
544 | CollectionAssert.AreEqual(expected1, output1);
545 | CollectionAssert.AreEqual(expected2, output2);
546 | CollectionAssert.AreEqual(expected3, output3);
547 | }
548 |
549 | [Test, Description("Check that .State has something")]
550 | public void StateReadPossibleTest()
551 | {
552 | // Arrange
553 | byte[] key = new byte[validKeyLength];
554 | byte[] nonce = new byte[validNonceLength];
555 |
556 | uint counter = 1;
557 |
558 | ChaCha20 forEncrypting1 = new ChaCha20(key, nonce, counter);
559 |
560 | // Act
561 |
562 | // Assert
563 | Assert.NotNull(forEncrypting1.State);
564 | Assert.AreEqual(16, forEncrypting1.State.Length, "Valid state lenght should always be 16 bytes");
565 | }
566 |
567 | [Test, Description("Check that ReadOnlySpan constructor works as it should")]
568 | public void ReadOnlySpanConstructorTest()
569 | {
570 | // Arrange
571 | byte[] key1 = new byte[validKeyLength] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
572 | byte[] nonce1 = new byte[validNonceLength] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
573 | uint counter1 = 1;
574 |
575 | byte[] key2 = new byte[validKeyLength] { 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
576 | byte[] nonce2 = new byte[validNonceLength] { 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
577 | uint counter2 = UInt32.MaxValue;
578 |
579 | ChaCha20 cRegular1 = new ChaCha20(key1, nonce1, counter1);
580 | ChaCha20 cRegular2 = new ChaCha20(key2, nonce2, counter2);
581 |
582 | ChaCha20 cRreadOnlySpan1 = new ChaCha20(new ReadOnlySpan(key1), new ReadOnlySpan(nonce1), counter1);
583 | ChaCha20 cRreadOnlySpan2 = new ChaCha20(new ReadOnlySpan(key2), new ReadOnlySpan(nonce2), counter2);
584 |
585 | // Act
586 |
587 | // Assert
588 | CollectionAssert.AreEqual(cRegular1.State, cRreadOnlySpan1.State);
589 | CollectionAssert.AreEqual(cRegular2.State, cRreadOnlySpan2.State);
590 | }
591 | }
592 | }
--------------------------------------------------------------------------------
/tests/tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------