Skip to content

Commit 16152bd

Browse files
Copilotd2dyno1
andauthored
Merge branch 'f_vault4' into f_complementation
Co-authored-by: d2dyno1 <53011783+d2dyno1@users.noreply.github.com>
2 parents 2986d69 + 2105964 commit 16152bd

76 files changed

Lines changed: 1953 additions & 692 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ This ensures that your data will always remain secure.
5050

5151
> [!TIP]
5252
> The purpose of SecureFolderFS is to provide a professional, usable 'safe folder' experience that supports all major platforms with a consistent feature set.
53-
> SecureFolderFS can run on a huge range of devices (as supported by Uno Platform, MAUI and the .NET Runtime).
53+
> SecureFolderFS can run on a huge range of devices (as supported by Uno Platform, .NET MAUI, and the .NET Runtime).
5454
5555
### How to use SecureFolderFS
5656

@@ -69,8 +69,8 @@ From the app's UI, you can create new vaults to store items securely.
6969
</p>
7070

7171
> *Upon entering the correct password, the vault will then open.*
72-
> *You can press 'View vault' to open the vault's mounted file-system in your file manager of choice.*
73-
> *When you're done accessing your files, you can press the 'Lock vault' button and the vault file-system will close.*
72+
> *You can press 'View vault' to open the vault's mounted file system in your file manager of choice.*
73+
> *When you're done accessing your files, you can press the 'Lock vault' button, and the vault file system will close.*
7474
> *The vault remains on your disk and your data is encrypted in a way that cannot be accessed by any program, past, present or future.*
7575
7676
## Contributing
@@ -83,15 +83,17 @@ Take a look at our *[contributing guidelines](CONTRIBUTING.md)* to learn about b
8383

8484
You can update existing localization strings by heading to our *[Crowdin project page](https://crowdin.com/project/securefolderfs)*.
8585
To add a new language to the list, please request it to be added *[here](https://github.com/securefolderfs-community/SecureFolderFS/issues/50)*.
86-
New translations will be synchronized periodically to the source code, and new releases will always contain the latest translations.
86+
New translations will be synchronized periodically with the source code, and new releases will always contain the latest translations.
87+
88+
For help with translating custom-format strings with pluralization support, refer to the [Localization Playground](https://github.com/securefolderfs-community/LocalizationPlayground) repository.
8789

8890
---
8991

9092
## Building from source
9193

9294
> [!NOTE]
9395
> Below are the instructions for building SecureFolderFS for a cross-platform target.
94-
> For other projects, such as the SDK, libraries and CLI program, you can build as normal with the latest .NET SDK, without the prerequisites listed below.
96+
> For other projects, such as the SDK, libraries, and CLI program, you can build as normal with the latest .NET SDK, without the prerequisites listed below.
9597
9698
### 1. Prerequisites
9799

@@ -104,7 +106,7 @@ New translations will be synchronized periodically to the source code, and new r
104106
- For iOS builds:
105107
- Xcode 26.2 (on macOS)
106108

107-
### 2. Set up IDE
109+
### 2. Set up the IDE
108110

109111
> [!TIP]
110112
> *Using Visual Studio 2026 is recommended for SecureFolderFS development.*
@@ -130,7 +132,7 @@ New translations will be synchronized periodically to the source code, and new r
130132
### 3. Run `Uno.Check`
131133

132134
> [!TIP]
133-
> *This step is optional, but is good practice to check you installed all the necessary dependencies to build SecureFolderFS on your computer.*
135+
> *This step is optional, but it is good practice to check that you installed all the necessary dependencies to build SecureFolderFS on your computer.*
134136
135137
Run the following command and follow all of its instructions (you need to have `Uno.Check` installed!)
136138

@@ -154,13 +156,13 @@ cd SecureFolderFS
154156
### 5. Build the project
155157

156158
- Open the solution `SecureFolderFS.Public.slnx`
157-
- Set `SecureFolderFS.Uno` as the startup project if you are building for desktop targets (i.e. macOS, Linux or Windows)
158-
- Set `SecureFolderFS.Maui` as the startup project if you are building for mobile targets (i.e. Android, iOS or iPadOS)
159-
- Select the appropriate target device / platform
159+
- Set `SecureFolderFS.Uno` as the startup project if you are building for desktop targets (i.e., macOS, Linux, or Windows)
160+
- Set `SecureFolderFS.Maui` as the startup project if you are building for mobile targets (i.e., Android, iOS, or iPadOS)
161+
- Select the appropriate target device/platform
160162
- Run with debugger
161163

162164
---
163165

164166
<p align="center">
165167
<img src=".github/assets/SecureFolderFS_Hero.png" />
166-
</p>
168+
</p>
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Some parts of the following code were used from Secomba/Base4K on the MIT License basis.
2+
// See the associated license file for more information.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.IO;
7+
using System.Runtime.CompilerServices;
8+
using System.Text;
9+
10+
namespace SecureFolderFS.Core.Cryptography.Cipher
11+
{
12+
public enum Base4KVersion
13+
{
14+
V1,
15+
V2
16+
}
17+
18+
public static class SecombaBase4K
19+
{
20+
// Base addresses for mapping regions
21+
private const int BASE_FLAG_START = 0x04000;
22+
private const int BASE1_START = 0x06000;
23+
private const int BASE1_START_LEGACY = 0x05000;
24+
25+
// Sizes of each mapping region
26+
private const int BASE_FLAG_SIZE = 0x100;
27+
private const int BASE1_SIZE = 0x01000;
28+
29+
private static readonly UTF8Encoding Utf8Encoding = new UTF8Encoding(true, true);
30+
31+
/// <summary>
32+
/// Encodes the specified raw bytes as a Base4K string, mapping each group of bits
33+
/// to Unicode characters in a specific range, suitable for use as file names.
34+
/// </summary>
35+
/// <param name="raw">The raw bytes to encode.</param>
36+
/// <param name="version">The version of Base4K encoding to use. Defaults to <see cref="Base4KVersion.V2"/>.</param>
37+
/// <returns>A Base4K-encoded string representation of the input bytes.</returns>
38+
/// <exception cref="ArgumentException">Thrown when <paramref name="raw"/> is empty or too short to encode.</exception>
39+
[SkipLocalsInit]
40+
public static string Encode(ReadOnlySpan<byte> raw, Base4KVersion version = Base4KVersion.V2)
41+
{
42+
if (raw.Length <= 1)
43+
throw new ArgumentException("Input must be at least 2 bytes long.", nameof(raw));
44+
45+
var maxByteCount = (raw.Length + 1) * 3;
46+
var rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
47+
try
48+
{
49+
var buffer = rentedBuffer.AsSpan();
50+
var bufferPos = 0;
51+
Span<byte> utf8Buffer = stackalloc byte[4];
52+
int offset;
53+
54+
for (var i = 0; i < raw.Length * 2 - 2; i += 3)
55+
{
56+
offset = i % 2 == 0
57+
? ((raw[i / 2] << 4) | ((raw[i / 2 + 1] >> 4) & 0x0f)) & 0x0fff
58+
: ((raw[i / 2] << 8) | (raw[i / 2 + 1] & 0xff)) & 0x0fff;
59+
60+
offset += version == Base4KVersion.V1 ? BASE1_START_LEGACY : BASE1_START;
61+
62+
var written = ToUtf8(offset, utf8Buffer);
63+
utf8Buffer.Slice(0, written).CopyTo(buffer.Slice(bufferPos));
64+
bufferPos += written;
65+
}
66+
67+
if ((raw.Length * 2) % 3 == 2)
68+
{
69+
offset = (raw[^1] & 0xff) + BASE_FLAG_START;
70+
var written = ToUtf8(offset, utf8Buffer);
71+
utf8Buffer.Slice(0, written).CopyTo(buffer.Slice(bufferPos));
72+
bufferPos += written;
73+
}
74+
else if ((raw.Length * 2) % 3 == 1)
75+
{
76+
offset = (raw[^1] & 0x0f) + BASE_FLAG_START;
77+
var written = ToUtf8(offset, utf8Buffer);
78+
utf8Buffer.Slice(0, written).CopyTo(buffer.Slice(bufferPos));
79+
bufferPos += written;
80+
}
81+
82+
return Utf8Encoding.GetString(buffer.Slice(0, bufferPos));
83+
}
84+
finally
85+
{
86+
ArrayPool<byte>.Shared.Return(rentedBuffer);
87+
}
88+
}
89+
90+
/// <summary>
91+
/// Decodes a Base4K-encoded string back to the original raw bytes.
92+
/// Attempts decoding with both V2 and V1 (legacy) base addresses automatically.
93+
/// </summary>
94+
/// <param name="encoded">The Base4K-encoded string to decode.</param>
95+
/// <returns>The decoded bytes, or <see langword="null"/> if decoding failed due to invalid or malformed input.</returns>
96+
public static byte[]? Decode(ReadOnlySpan<char> encoded)
97+
{
98+
return DecodeInternal(encoded, BASE1_START) ?? DecodeInternal(encoded, BASE1_START_LEGACY);
99+
}
100+
101+
private static byte[]? DecodeInternal(ReadOnlySpan<char> encoded, int base1Start)
102+
{
103+
var byteCount = Utf8Encoding.GetByteCount(encoded);
104+
var encBytes = new byte[byteCount];
105+
var written = Utf8Encoding.GetBytes(encoded, encBytes);
106+
107+
using var memoryStream = new MemoryStream();
108+
var rentedCollector = ArrayPool<int>.Shared.Rent(written / 3 + 1);
109+
var collectorCount = 0;
110+
try
111+
{
112+
for (var i = 0; i < written;)
113+
{
114+
int nrOfBytes;
115+
if ((encBytes[i] & 0x80) == 0)
116+
{
117+
// 1 byte
118+
nrOfBytes = 1;
119+
}
120+
else if ((encBytes[i] & 0x40) == 0)
121+
{
122+
// Continuation byte — invalid as a leading byte
123+
return null;
124+
}
125+
else if ((encBytes[i] & 0x20) == 0)
126+
{
127+
// 2 bytes
128+
nrOfBytes = 2;
129+
}
130+
else if ((encBytes[i] & 0x10) == 0)
131+
{
132+
// 3 bytes
133+
nrOfBytes = 3;
134+
}
135+
else if ((encBytes[i] & 0x08) == 0)
136+
{
137+
// 4 bytes
138+
nrOfBytes = 4;
139+
}
140+
else
141+
{
142+
// Invalid leading byte
143+
return null;
144+
}
145+
146+
var code = ToCode(encBytes, i, nrOfBytes);
147+
i += nrOfBytes;
148+
149+
if (!(code >= base1Start && code < base1Start + BASE1_SIZE))
150+
{
151+
if (i < written || !(code >= BASE_FLAG_START && code < BASE_FLAG_START + BASE_FLAG_SIZE))
152+
return null;
153+
}
154+
155+
rentedCollector[collectorCount++] = code;
156+
}
157+
158+
for (var i = 0; i < collectorCount; i++)
159+
{
160+
if (rentedCollector[i] >= base1Start)
161+
rentedCollector[i] -= base1Start;
162+
else
163+
{
164+
rentedCollector[i] -= BASE_FLAG_START;
165+
if (i % 2 == 0)
166+
memoryStream.WriteByte((byte)rentedCollector[i]);
167+
else
168+
memoryStream.WriteByte((byte)(((rentedCollector[i - 1] << 4) | ((rentedCollector[i] & 0x0f)) & 0xff)));
169+
170+
break;
171+
}
172+
173+
if (i % 2 == 0)
174+
memoryStream.WriteByte((byte)(rentedCollector[i] >> 4));
175+
else
176+
{
177+
memoryStream.WriteByte((byte)(((rentedCollector[i - 1] << 4) | ((rentedCollector[i] & 0x0f00) >> 8)) & 0xff));
178+
memoryStream.WriteByte((byte)(rentedCollector[i] & 0xff));
179+
}
180+
}
181+
}
182+
finally
183+
{
184+
ArrayPool<int>.Shared.Return(rentedCollector);
185+
}
186+
187+
return memoryStream.ToArray();
188+
}
189+
190+
private static int ToUtf8(int code, Span<byte> destination)
191+
{
192+
switch (code)
193+
{
194+
case > 0xffff:
195+
{
196+
destination[0] = (byte)(0xf0 | ((code >> 18) & 0x07));
197+
destination[1] = (byte)(0x80 | ((code >> 12) & 0x3f));
198+
destination[2] = (byte)(0x80 | ((code >> 6) & 0x3f));
199+
destination[3] = (byte)(0x80 | (code & 0x3f));
200+
return 4;
201+
}
202+
203+
case > 0x7ff:
204+
{
205+
destination[0] = (byte)(0xe0 | ((code >> 12) & 0x0f));
206+
destination[1] = (byte)(0x80 | ((code >> 6) & 0x3f));
207+
destination[2] = (byte)(0x80 | (code & 0x3f));
208+
return 3;
209+
}
210+
211+
case > 0x7f:
212+
{
213+
destination[0] = (byte)(0xc0 | ((code >> 6) & 0x1f));
214+
destination[1] = (byte)(0x80 | (code & 0x3f));
215+
return 2;
216+
}
217+
218+
default:
219+
{
220+
destination[0] = (byte)(code & 0x7f);
221+
return 1;
222+
}
223+
}
224+
}
225+
226+
private static int ToCode(ReadOnlySpan<byte> utf8Char, int offset, int length)
227+
{
228+
var result = 0;
229+
switch (length)
230+
{
231+
case 1:
232+
{
233+
result |= utf8Char[offset];
234+
break;
235+
}
236+
237+
case 2:
238+
{
239+
result |= (utf8Char[offset + 0] & 0x1f) << 6;
240+
result |= (utf8Char[offset + 1] & 0x3f);
241+
break;
242+
}
243+
244+
case 3:
245+
{
246+
result |= (utf8Char[offset + 0] & 0x0f) << 12;
247+
result |= (utf8Char[offset + 1] & 0x3f) << 6;
248+
result |= (utf8Char[offset + 2] & 0x3f);
249+
break;
250+
}
251+
252+
case 4:
253+
{
254+
result |= (utf8Char[offset + 0] & 0x07) << 18;
255+
result |= (utf8Char[offset + 1] & 0x3f) << 12;
256+
result |= (utf8Char[offset + 2] & 0x3f) << 6;
257+
result |= (utf8Char[offset + 3] & 0x3f);
258+
break;
259+
}
260+
}
261+
262+
return result;
263+
}
264+
}
265+
}

src/Core/SecureFolderFS.Core.Cryptography/Constants.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ public static class KeyTraits
99
public const int DEK_KEY_LENGTH = 32;
1010
public const int MAC_KEY_LENGTH = 32;
1111
public const int ARGON2_KEK_LENGTH = 32;
12-
public const int CHALLENGE_KEY_PART_LENGTH_32 = 32;
13-
public const int CHALLENGE_KEY_PART_LENGTH_64 = 64;
14-
public const int CHALLENGE_KEY_PART_LENGTH_128 = 128;
15-
public const int ECIES_SHA256_AESGCM_STDX963_KEY_LENGTH = CHALLENGE_KEY_PART_LENGTH_32;
12+
public const int KEY_PART_LENGTH_32 = 32;
13+
public const int KEY_PART_LENGTH_64 = 64;
14+
public const int KEY_PART_LENGTH_128 = 128;
15+
public const int ECIES_SHA256_AESGCM_STDX963_KEY_LENGTH = KEY_PART_LENGTH_32;
1616
public const int HMAC_SHA1_HASH_LENGTH = 20;
1717
}
1818

src/Core/SecureFolderFS.Core.Cryptography/Helpers/CryptHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace SecureFolderFS.Core.Cryptography.Helpers
1010
{
1111
public static class CryptHelpers
1212
{
13-
public static IKeyBytes GenerateChallenge(string vaultId, int challengeSize = Constants.KeyTraits.CHALLENGE_KEY_PART_LENGTH_128)
13+
public static IKeyBytes GenerateChallenge(string vaultId, int challengeSize = Constants.KeyTraits.KEY_PART_LENGTH_128)
1414
{
1515
var encodedVaultIdLength = Encoding.ASCII.GetByteCount(vaultId);
1616
var challenge = new byte[challengeSize + encodedVaultIdLength];

0 commit comments

Comments
 (0)