├── .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 | ![.NET](https://github.com/mcraiha/CSharp-ChaCha20-NetStandard/workflows/.NET/badge.svg) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/6affaeed425241a88304a5397005c789)](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 | --------------------------------------------------------------------------------