Skip to content

Commit dd53ee5

Browse files
committed
Initial: FF1 + FF3 with all 24 NIST vectors passing, pure C#
0 parents  commit dd53ee5

11 files changed

Lines changed: 505 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: CI
2+
on:
3+
push: { branches: [main] }
4+
pull_request: { branches: [main] }
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-dotnet@v4
11+
with: { dotnet-version: "8.0.x" }
12+
- run: dotnet test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bin/
2+
obj/
3+
*.user
4+
.vs/

Cyphera.sln

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4C36242D-3001-4E08-8763-69A7759CC61A}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cyphera", "src\Cyphera\Cyphera.csproj", "{79594759-2989-4C05-AD1E-300937A23CD1}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AA383D52-FE0A-4CCD-B090-FE35F3A2583D}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cyphera.Tests", "tests\Cyphera.Tests\Cyphera.Tests.csproj", "{3FDD6084-05E2-4081-BAC8-764D572ECDCB}"
13+
EndProject
14+
Global
15+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16+
Debug|Any CPU = Debug|Any CPU
17+
Release|Any CPU = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
23+
{79594759-2989-4C05-AD1E-300937A23CD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24+
{79594759-2989-4C05-AD1E-300937A23CD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
25+
{79594759-2989-4C05-AD1E-300937A23CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
26+
{79594759-2989-4C05-AD1E-300937A23CD1}.Release|Any CPU.Build.0 = Release|Any CPU
27+
{3FDD6084-05E2-4081-BAC8-764D572ECDCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28+
{3FDD6084-05E2-4081-BAC8-764D572ECDCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{3FDD6084-05E2-4081-BAC8-764D572ECDCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
30+
{3FDD6084-05E2-4081-BAC8-764D572ECDCB}.Release|Any CPU.Build.0 = Release|Any CPU
31+
EndGlobalSection
32+
GlobalSection(NestedProjects) = preSolution
33+
{79594759-2989-4C05-AD1E-300937A23CD1} = {4C36242D-3001-4E08-8763-69A7759CC61A}
34+
{3FDD6084-05E2-4081-BAC8-764D572ECDCB} = {AA383D52-FE0A-4CCD-B090-FE35F3A2583D}
35+
EndGlobalSection
36+
EndGlobal

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Cyphera
2+
3+
Data obfuscation SDK for .NET. FPE, AES, masking, hashing.
4+
5+
```
6+
dotnet add package Cyphera
7+
```
8+
9+
```csharp
10+
using Cyphera;
11+
12+
var cipher = FF1.Digits(key, tweak);
13+
var encrypted = cipher.Encrypt("0123456789");
14+
var decrypted = cipher.Decrypt(encrypted);
15+
```
16+
17+
## Status
18+
19+
Early development. FF1 and FF3 engines with all NIST test vectors. Pure C#, no native deps.
20+
21+
## License
22+
23+
Apache 2.0

src/Cyphera/Cyphera.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

src/Cyphera/FF1.cs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using System;
2+
using System.Numerics;
3+
using System.Security.Cryptography;
4+
5+
namespace Cyphera
6+
{
7+
public class FF1
8+
{
9+
private readonly int _radix;
10+
private readonly byte[] _key;
11+
private readonly byte[] _tweak;
12+
private readonly string _alphabet;
13+
private readonly Dictionary<char, int> _charMap;
14+
15+
public FF1(byte[] key, byte[] tweak, string alphabet = "0123456789abcdefghijklmnopqrstuvwxyz")
16+
{
17+
if (key.Length != 16 && key.Length != 24 && key.Length != 32)
18+
throw new ArgumentException("Key must be 16, 24, or 32 bytes");
19+
if (alphabet.Length < 2)
20+
throw new ArgumentException("Alphabet must have >= 2 chars");
21+
22+
_radix = alphabet.Length;
23+
_alphabet = alphabet;
24+
_key = (byte[])key.Clone();
25+
_tweak = (byte[])tweak.Clone();
26+
_charMap = new Dictionary<char, int>();
27+
for (int i = 0; i < alphabet.Length; i++) _charMap[alphabet[i]] = i;
28+
}
29+
30+
public string Encrypt(string plaintext) => FromDigits(FF1Encrypt(ToDigits(plaintext), _tweak));
31+
public string Decrypt(string ciphertext) => FromDigits(FF1Decrypt(ToDigits(ciphertext), _tweak));
32+
33+
private int[] ToDigits(string s) => s.Select(c => _charMap[c]).ToArray();
34+
private string FromDigits(int[] d) => new string(d.Select(i => _alphabet[i]).ToArray());
35+
36+
private int[] FF1Encrypt(int[] pt, byte[] T)
37+
{
38+
int n = pt.Length, u = n / 2, v = n - u;
39+
int[] A = pt[..u], B = pt[u..];
40+
int b = ComputeB(v);
41+
int d = 4 * ((b + 3) / 4) + 4;
42+
byte[] P = BuildP(u, n, T.Length);
43+
44+
for (int i = 0; i < 10; i++)
45+
{
46+
var numB = BigIntToBytes(Num(B), b);
47+
var Q = BuildQ(T, i, numB, b);
48+
var R = PRF(Concat(P, Q));
49+
var S = ExpandS(R, d);
50+
var y = new BigInteger(S, isUnsigned: true, isBigEndian: true);
51+
int m = i % 2 == 0 ? u : v;
52+
var c = (Num(A) + y) % BigInteger.Pow(_radix, m);
53+
A = B; B = Str(c, m);
54+
}
55+
return A.Concat(B).ToArray();
56+
}
57+
58+
private int[] FF1Decrypt(int[] ct, byte[] T)
59+
{
60+
int n = ct.Length, u = n / 2, v = n - u;
61+
int[] A = ct[..u], B = ct[u..];
62+
int b = ComputeB(v);
63+
int d = 4 * ((b + 3) / 4) + 4;
64+
byte[] P = BuildP(u, n, T.Length);
65+
66+
for (int i = 9; i >= 0; i--)
67+
{
68+
var numA = BigIntToBytes(Num(A), b);
69+
var Q = BuildQ(T, i, numA, b);
70+
var R = PRF(Concat(P, Q));
71+
var S = ExpandS(R, d);
72+
var y = new BigInteger(S, isUnsigned: true, isBigEndian: true);
73+
int m = i % 2 == 0 ? u : v;
74+
var mod = BigInteger.Pow(_radix, m);
75+
var c = ((Num(B) - y) % mod + mod) % mod;
76+
B = A; A = Str(c, m);
77+
}
78+
return A.Concat(B).ToArray();
79+
}
80+
81+
private int ComputeB(int v)
82+
{
83+
var pow = BigInteger.Pow(_radix, v) - 1;
84+
int bits = pow.IsZero ? 1 : (int)pow.GetBitLength();
85+
return (bits + 7) / 8;
86+
}
87+
88+
private byte[] BuildP(int u, int n, int t)
89+
{
90+
byte[] P = new byte[16];
91+
P[0] = 1; P[1] = 2; P[2] = 1;
92+
P[3] = (byte)(_radix >> 16); P[4] = (byte)(_radix >> 8); P[5] = (byte)_radix;
93+
P[6] = 10; P[7] = (byte)u;
94+
P[8] = (byte)(n >> 24); P[9] = (byte)(n >> 16); P[10] = (byte)(n >> 8); P[11] = (byte)n;
95+
P[12] = (byte)(t >> 24); P[13] = (byte)(t >> 16); P[14] = (byte)(t >> 8); P[15] = (byte)t;
96+
return P;
97+
}
98+
99+
private byte[] BuildQ(byte[] T, int i, byte[] numBytes, int b)
100+
{
101+
int pad = (16 - ((T.Length + 1 + b) % 16)) % 16;
102+
var Q = new byte[T.Length + pad + 1 + b];
103+
Array.Copy(T, 0, Q, 0, T.Length);
104+
Q[T.Length + pad] = (byte)i;
105+
int srcStart = Math.Max(0, numBytes.Length - b);
106+
int destStart = Q.Length - (numBytes.Length - srcStart);
107+
Array.Copy(numBytes, srcStart, Q, destStart, numBytes.Length - srcStart);
108+
return Q;
109+
}
110+
111+
private byte[] PRF(byte[] data)
112+
{
113+
byte[] y = new byte[16];
114+
for (int off = 0; off < data.Length; off += 16)
115+
{
116+
byte[] tmp = new byte[16];
117+
for (int j = 0; j < 16; j++) tmp[j] = (byte)(y[j] ^ data[off + j]);
118+
y = AesEcb(tmp);
119+
}
120+
return y;
121+
}
122+
123+
private byte[] ExpandS(byte[] R, int d)
124+
{
125+
int blocks = (d + 15) / 16;
126+
byte[] outBuf = new byte[blocks * 16];
127+
Array.Copy(R, 0, outBuf, 0, 16);
128+
byte[] prev = (byte[])R.Clone();
129+
for (int j = 1; j < blocks; j++)
130+
{
131+
byte[] x = new byte[16];
132+
x[12] = (byte)(j >> 24); x[13] = (byte)(j >> 16); x[14] = (byte)(j >> 8); x[15] = (byte)j;
133+
for (int k = 0; k < 16; k++) x[k] ^= prev[k];
134+
var enc = AesEcb(x);
135+
Array.Copy(enc, 0, outBuf, j * 16, 16);
136+
prev = enc;
137+
}
138+
return outBuf[..d];
139+
}
140+
141+
private byte[] AesEcb(byte[] block)
142+
{
143+
using var aes = Aes.Create();
144+
aes.Key = _key;
145+
aes.Mode = CipherMode.ECB;
146+
aes.Padding = PaddingMode.None;
147+
return aes.EncryptEcb(block, PaddingMode.None);
148+
}
149+
150+
private byte[] BigIntToBytes(BigInteger x, int b)
151+
{
152+
var bytes = x.ToByteArray(isUnsigned: true, isBigEndian: true);
153+
if (bytes.Length >= b) return bytes[^b..];
154+
var result = new byte[b];
155+
Array.Copy(bytes, 0, result, b - bytes.Length, bytes.Length);
156+
return result;
157+
}
158+
159+
private BigInteger Num(int[] digits)
160+
{
161+
BigInteger r = 0;
162+
foreach (var d in digits) r = r * _radix + d;
163+
return r;
164+
}
165+
166+
private int[] Str(BigInteger num, int len)
167+
{
168+
int[] r = new int[len];
169+
for (int i = len - 1; i >= 0; i--) { r[i] = (int)(num % _radix); num /= _radix; }
170+
return r;
171+
}
172+
173+
private static byte[] Concat(byte[] a, byte[] b)
174+
{
175+
var r = new byte[a.Length + b.Length];
176+
Array.Copy(a, 0, r, 0, a.Length);
177+
Array.Copy(b, 0, r, a.Length, b.Length);
178+
return r;
179+
}
180+
181+
public static FF1 Digits(byte[] key, byte[] tweak) => new FF1(key, tweak, "0123456789");
182+
public static FF1 Alphanumeric(byte[] key, byte[] tweak) => new FF1(key, tweak);
183+
public static byte[] HexToBytes(string hex)
184+
{
185+
byte[] r = new byte[hex.Length / 2];
186+
for (int i = 0; i < hex.Length; i += 2)
187+
r[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
188+
return r;
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)