Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Build
run: dotnet build src/CipheredFileStream/CipheredFileStream.csproj -c Release

- name: Test with coverage
run: >
dotnet test tests/CipheredFileStream.Test/ -c Release
--filter "Category!=Performance&Category!=Benchmark&Category!=Profiling&Category!=Endurance"
--collect:"XPlat Code Coverage"
--results-directory ./coverage
--logger "console;verbosity=normal"

- name: Generate coverage report
uses: danielpalme/ReportGenerator-GitHub-Action@5
with:
reports: coverage/**/coverage.cobertura.xml
targetdir: coverage/report
reporttypes: MarkdownSummaryGithub;Cobertura

- name: Add coverage PR comment
uses: marocchino/sticky-pull-request-comment@v2
if: github.event_name == 'pull_request'
with:
path: coverage/report/SummaryGithub.md

- name: Write coverage to job summary
run: cat coverage/report/SummaryGithub.md >> $GITHUB_STEP_SUMMARY
41 changes: 41 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Release

on:
push:
tags: ['v*']

jobs:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

- name: Build
run: dotnet build src/CipheredFileStream/CipheredFileStream.csproj -c Release -p:Version=${{ steps.version.outputs.VERSION }}

- name: Test (functional)
run: dotnet test tests/CipheredFileStream.Test/ -c Release --filter "Category!=Performance&Category!=Benchmark&Category!=Profiling&Category!=Endurance"

- name: Pack
run: dotnet pack src/CipheredFileStream/CipheredFileStream.csproj -c Release --no-build -p:Version=${{ steps.version.outputs.VERSION }}

- name: Push to NuGet
run: dotnet nuget push src/CipheredFileStream/bin/Release/CipheredFileStream.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: src/CipheredFileStream/bin/Release/CipheredFileStream.*.nupkg
16 changes: 15 additions & 1 deletion src/CipheredFileStream/CipheredFileStream.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<PackageId>CipheredFileStream</PackageId>
<Authors>hooyao</Authors>
<Description>Block-based encrypted file stream for .NET with full random read/write support. AES-256-GCM, pluggable crypto, drop-in Stream replacement.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/hooyao/CipheredFileStream</PackageProjectUrl>
<RepositoryUrl>https://github.com/hooyao/CipheredFileStream</RepositoryUrl>
<PackageTags>encryption;aes;gcm;stream;filestream;crypto;block-cipher</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
Expand All @@ -19,4 +29,8 @@
<Protobuf Include="IO\Protos\EncryptedFileHeader.proto" GrpcServices="None" />
</ItemGroup>

<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="/" />
</ItemGroup>

</Project>
22 changes: 11 additions & 11 deletions src/CipheredFileStream/IO/CipheredFileStreamFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class CipheredFileStreamFactory : ICipheredStreamFactory
/// <exception cref="ArgumentException"><paramref name="key"/> is not 32 bytes.</exception>
public CipheredFileStreamFactory(byte[] key)
{
ArgumentNullException.ThrowIfNull(key);
ThrowHelper.ThrowIfNull(key, nameof(key));
if (key.Length != EncryptedFileFormat.KeySize)
throw new ArgumentException(
$"Key must be exactly {EncryptedFileFormat.KeySize} bytes.", nameof(key));
Expand All @@ -42,7 +42,7 @@ public CipheredFileStreamFactory(byte[] key)
/// <exception cref="ArgumentNullException"><paramref name="keyProvider"/> is null.</exception>
public CipheredFileStreamFactory(IKeyProvider keyProvider)
{
ArgumentNullException.ThrowIfNull(keyProvider);
ThrowHelper.ThrowIfNull(keyProvider, nameof(keyProvider));
_keyProvider = keyProvider;
_key = (byte[])keyProvider.GetKey().Clone();
}
Expand All @@ -58,12 +58,12 @@ public CipheredFileStreamFactory(IKeyProvider keyProvider)
/// <exception cref="EncryptedFileVersionException">The file version is not supported.</exception>
public static FileHeaderInfo ReadFileHeader(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
ThrowHelper.ThrowIfNullOrEmpty(path, nameof(path));

Span<byte> header = stackalloc byte[EncryptedFileFormat.CleartextHeaderSize];
var header = new byte[EncryptedFileFormat.CleartextHeaderSize];

using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
int bytesRead = fs.Read(header);
int bytesRead = fs.Read(header, 0, header.Length);
if (bytesRead < EncryptedFileFormat.CleartextHeaderSize)
throw EncryptedFileCorruptException.InvalidMagicBytes(path);

Expand All @@ -73,7 +73,7 @@ public static FileHeaderInfo ReadFileHeader(string path)
throw EncryptedFileCorruptException.InvalidMagicBytes(path);

// Validate version
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(header[2..]);
ushort version = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(2));
if (version > EncryptedFileFormat.MaxSupportedVersion)
throw new EncryptedFileVersionException(version, EncryptedFileFormat.MaxSupportedVersion, path);

Expand All @@ -86,9 +86,9 @@ public static FileHeaderInfo ReadFileHeader(string path)

if (kdfMethod == KdfMethod.Pbkdf2Sha256)
{
salt = header.Slice(EncryptedFileFormat.SaltOffset, EncryptedFileFormat.SaltSize).ToArray();
salt = header.AsSpan(EncryptedFileFormat.SaltOffset, EncryptedFileFormat.SaltSize).ToArray();
kdfIterations = BinaryPrimitives.ReadUInt32LittleEndian(
header[EncryptedFileFormat.KdfIterationsOffset..]);
header.AsSpan(EncryptedFileFormat.KdfIterationsOffset));
}

return new FileHeaderInfo
Expand All @@ -114,8 +114,8 @@ public Stream Create(string path, FileMode mode, FileAccess access, CipheredFile
public Stream Create(string path, FileMode mode, FileAccess access, FileShare share,
CipheredFileStreamOptions? options = null)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentException.ThrowIfNullOrEmpty(path);
ThrowHelper.ThrowIfDisposed(_disposed, this);
ThrowHelper.ThrowIfNullOrEmpty(path, nameof(path));

options ??= new CipheredFileStreamOptions();

Expand Down Expand Up @@ -201,7 +201,7 @@ public void Dispose()
{
if (_key is not null)
{
Array.Clear(_key);
Array.Clear(_key, 0, _key.Length);
_key = null;
}
_disposed = true;
Expand Down
4 changes: 2 additions & 2 deletions src/CipheredFileStream/IO/EphemeralKeyProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ public EphemeralKeyProvider()

public byte[] GetKey()
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowHelper.ThrowIfDisposed(_disposed, this);
return _key;
}

public void Dispose()
{
if (!_disposed)
{
Array.Clear(_key);
Array.Clear(_key, 0, _key.Length);
_disposed = true;
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/CipheredFileStream/IO/Internal/AesGcmBlockCrypto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ public int Encrypt(
_nonce.CopyTo(ciphertext.AsSpan(ciphertextOffset));

// 3. AES-GCM encrypt
#if NET7_0_OR_GREATER
using var aesGcm = new AesGcm(key, TagSize);
#else
using var aesGcm = new AesGcm(key);
#endif
aesGcm.Encrypt(
_nonce,
plaintext.AsSpan(plaintextOffset, plaintextCount),
Expand Down Expand Up @@ -88,7 +92,11 @@ public int Decrypt(
// 3. Decrypt and authenticate
try
{
#if NET7_0_OR_GREATER
using var aesGcm = new AesGcm(key, TagSize);
#else
using var aesGcm = new AesGcm(key);
#endif
aesGcm.Decrypt(
_nonce,
ciphertext.AsSpan(ciphertextOffset + NonceSize, payloadSize),
Expand Down Expand Up @@ -116,7 +124,7 @@ public int GetMaxPlaintextSize(int ciphertextSize)

public void Dispose()
{
Array.Clear(_nonce);
Array.Clear(_tag);
Array.Clear(_nonce, 0, _nonce.Length);
Array.Clear(_tag, 0, _tag.Length);
}
}
18 changes: 9 additions & 9 deletions src/CipheredFileStream/IO/Internal/BlockManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ private void LoadBlock(int blockIndex)
_underlyingStream.Position = physicalOffset + EncryptedFileFormat.CleartextHeaderSize;

// Read ciphertext length prefix
Span<byte> lengthBytes = stackalloc byte[_layout.CiphertextLengthPrefixSize];
int lengthRead = _underlyingStream.Read(lengthBytes);
var lengthArray = new byte[_layout.CiphertextLengthPrefixSize];
int lengthRead = _underlyingStream.Read(lengthArray, 0, lengthArray.Length);
if (lengthRead < _layout.CiphertextLengthPrefixSize)
throw new EndOfStreamException($"Failed to read ciphertext length for block {blockIndex}.");

int ciphertextLength = (int)BitConverter.ToUInt32(lengthBytes);
int ciphertextLength = (int)BitConverter.ToUInt32(lengthArray, 0);
int maxCiphertextSize = blockIndex == 0 ? _layout.Block0MaxCiphertextSize : _layout.BlockNMaxCiphertextSize;
if (ciphertextLength == 0 || ciphertextLength > maxCiphertextSize)
throw new EncryptedFileCorruptException($"Invalid ciphertext length {ciphertextLength} for block {blockIndex}.", blockIndex, _filePath);
Expand Down Expand Up @@ -151,9 +151,9 @@ public void FlushBlock()
_underlyingStream.Position = physicalOffset;

// Length prefix
Span<byte> lengthBytes = stackalloc byte[4];
var lengthBytes = new byte[4];
BitConverter.TryWriteBytes(lengthBytes, (uint)ciphertextLength);
_underlyingStream.Write(lengthBytes);
_underlyingStream.Write(lengthBytes, 0, lengthBytes.Length);

// Ciphertext
_underlyingStream.Write(_ciphertextBuffer, 0, ciphertextLength);
Expand Down Expand Up @@ -202,9 +202,9 @@ public void InvalidateCache()
public void Dispose()
{
_crypto.Dispose();
Array.Clear(_cachedPayload);
Array.Clear(_plaintextBuffer);
Array.Clear(_ciphertextBuffer);
Array.Clear(_integrityTagBuffer);
Array.Clear(_cachedPayload, 0, _cachedPayload.Length);
Array.Clear(_plaintextBuffer, 0, _plaintextBuffer.Length);
Array.Clear(_ciphertextBuffer, 0, _ciphertextBuffer.Length);
Array.Clear(_integrityTagBuffer, 0, _integrityTagBuffer.Length);
}
}
Loading
Loading