An educational desktop digital-signature application that combines a Flutter UI with a C++23 native cryptographic core through Dart FFI.
![]() |
![]() |
| Signature creation | Signature verification |
pcsign is a desktop application for demonstrating the creation and verification of a digital signature with a custom RSA and MD5 implementation.
The project is split into two parts:
- a Flutter/Dart desktop interface that presents the signing and verification workflow;
- a C++23 shared library that implements hashing, arbitrary-precision decimal arithmetic, RSA key generation, signature creation, and verification.
The two parts communicate through Dart FFI. Flutter loads libpcsign.dll at runtime and calls exported C-compatible functions from the native library.
The application is intentionally small and explicit: intermediate values such as the message hash, RSA parameters, signature, and recovered hash are displayed directly in the UI so the complete signature flow can be inspected.
Warning
This is an educational cryptography project, not a production signing system. It uses MD5 and textbook-style RSA operations without a standardized signature padding scheme such as RSA-PSS or PKCS#1 v1.5. Do not use it to protect real data or secrets.
- Custom MD5 implementation in C++
- Custom arbitrary-precision unsigned integer operations using decimal strings
- RSA key generation
- Probabilistic prime testing with Miller-Rabin
- Parallel prime search using
std::async - Selection of a public exponent from common Fermat-number candidates
- Modular inverse calculation for the RSA private exponent
- Modular exponentiation for signing and verification
- Digital signature creation from a document hash
- Digital signature verification by comparing the recovered and recalculated hashes
- Flutter desktop UI with separate sender and recipient views
- Dart FFI integration with a native Windows DLL
- Option to generate a new RSA key pair or reuse existing key files
- Intermediate cryptographic values displayed in the UI
flowchart LR
DOC[Document] --> MD5[Custom MD5]
MD5 --> HASH[Message hash m]
KEYGEN[RSA key generation] --> PRIVATE[Private key D, N]
KEYGEN --> PUBLIC[Public key E, N]
HASH --> SIGN[Modular exponentiation]
PRIVATE --> SIGN
SIGN --> S[Signature S]
S --> VERIFY[Modular exponentiation]
PUBLIC --> VERIFY
VERIFY --> RECOVERED[Recovered hash m']
DOC --> MD5VERIFY[Recalculate hash]
MD5VERIFY --> HASHVERIFY[Hash m]
RECOVERED --> COMPARE{m' == m?}
HASHVERIFY --> COMPARE
COMPARE -->|yes| VALID[Signature confirmed]
COMPARE -->|no| INVALID[Verification failed]
The native core follows a direct RSA signature demonstration.
For a document M, the application first computes a hash:
The signature is created with the private exponent:
Verification recovers a value from the signature with the public exponent:
The application recalculates the document hash and compares the two values:
m' == m → signature confirmed
m' != m → verification failed
The sender view reads the document from:
pcsign/doc.txt
When Generate keys is enabled, the C++ library generates a new RSA key pair before creating the signature.
The result panel displays the current document and cryptographic values, including:
M document contents
m calculated hash
P generated prime
Q generated prime
N RSA modulus
D private exponent
E public exponent
S signature
When key generation is disabled, the existing key files are reused.
The recipient view:
- recalculates the hash of
pcsign/doc.txt; - reads the signature from
pcsign/sign.txt; - reads the public key from
pcsign/openkey.txt; - calculates
S^E mod N; - compares the recovered value with the newly calculated document hash.
The UI displays the signature, public exponent, modulus, recovered hash, and final verification result.
Changing the document, signature, or key material causes the compared values to differ and verification to fail.
flowchart LR
UI[Flutter widgets] --> FFI[Dart FFI]
FFI --> DLL[libpcsign.dll]
DLL --> API[Exported C API]
API --> MD5[MD5 implementation]
API --> RSA[RSA implementation]
RSA --> BBINT[Custom big-integer arithmetic]
API --> FILES[Working files]
The Dart side consists of three main widgets:
| File | Responsibility |
|---|---|
lib/main.dart |
Desktop window configuration and sender/recipient navigation |
lib/sign.dart |
Signature creation UI and FFI calls |
lib/verify.dart |
Signature verification UI and FFI calls |
NavigationRail switches between the sender and recipient views.
The desktop window is constrained to a compact fixed-height layout and uses Flutter Material 3 styling.
The C++ implementation is built as the pcsign shared library.
| Component | Responsibility |
|---|---|
library.cpp |
Exported FFI functions and file-based orchestration |
rsa.cpp / rsa.h |
RSA key generation, signing, and verification |
md5.cpp / md5.h |
Custom MD5 hashing implementation |
bbint.cpp / bbint.h |
Arbitrary-precision decimal arithmetic |
The public native API exposed to Dart contains four functions:
extern "C" {
__declspec(dllexport) char* gethash();
__declspec(dllexport) char* getkeys();
__declspec(dllexport) char* getsign();
__declspec(dllexport) char* getverify();
}Dart resolves these symbols from libpcsign.dll with DynamicLibrary.open() and lookupFunction().
The RSA implementation does not use OpenSSL, Crypto++, GMP, or another arbitrary-precision library.
Instead, large positive integers are represented as decimal std::string values:
using bbint = std::string;The project implements the required operations directly:
- addition;
- subtraction;
- multiplication;
- division;
- modulo;
- comparison;
- greatest common divisor;
- binary-to-decimal conversion;
- modular exponentiation;
- modular inverse.
Modular exponentiation uses the square-and-multiply approach and is shared by both signature creation and verification.
The current native configuration uses:
#define BITLEN 256
#define ITERLEN 5Prime candidates are generated from random binary strings and tested with Miller-Rabin.
Prime generation is parallelized across the number of hardware threads reported by:
std::thread::hardware_concurrency()Each worker searches independently until one task finds a probable prime.
After P and Q are selected:
N = P × Q
φ(N) = (P - 1) × (Q - 1)
The implementation selects the first exponent coprime with φ(N) from:
65537, 257, 17, 5, 3
The private exponent D is calculated as the modular inverse of E modulo φ(N).
The current implementation uses a small file-based exchange directory:
pcsign/
├── doc.txt
├── hashsign.txt
├── hashverify.txt
├── keys.txt
├── openkey.txt
├── secretkey.txt
└── sign.txt
| File | Purpose |
|---|---|
doc.txt |
Document currently being signed or verified |
hashsign.txt |
Hash calculated during signing |
hashverify.txt |
Hash calculated during verification |
keys.txt |
Human-readable generated key values |
openkey.txt |
Public exponent and modulus |
secretkey.txt |
Private exponent and modulus |
sign.txt |
Generated signature |
These files make the intermediate steps easy to inspect, but they also mean that the current application uses fixed relative paths instead of a file picker or a dedicated document format.
secretkey.txtcontains private key material. The bundled values are demo data; never use this workflow for real private keys.
.
├── lib/
│ ├── main.dart
│ ├── sign.dart
│ └── verify.dart
├── pcsign/
│ ├── doc.txt
│ ├── hashsign.txt
│ ├── hashverify.txt
│ ├── keys.txt
│ ├── openkey.txt
│ ├── secretkey.txt
│ └── sign.txt
├── signature/
│ ├── CMakeLists.txt
│ ├── bbint.cpp
│ ├── bbint.h
│ ├── library.cpp
│ ├── library.h
│ ├── md5.cpp
│ ├── md5.h
│ ├── rsa.cpp
│ └── rsa.h
├── libpcsign.dll
├── analysis_options.yaml
├── pubspec.lock
└── pubspec.yaml
- C++23
- CMake
- Dart
- Flutter
- Dart FFI
- Material 3
Flutter dependencies used by the source include:
ffiwindow_sizecupertino_icons
The project declares Dart SDK ^3.6.0 in pubspec.yaml.
- Windows
- Flutter SDK with Windows desktop support
- Dart compatible with the SDK constraint in
pubspec.yaml - CMake
3.29+ - A GCC-compatible Windows C++ toolchain with C++23 support, such as MinGW-w64 GCC
- Ninja or another CMake generator supported by the selected toolchain
The current C++ source uses GCC-compatible facilities including __uint128_t and <bits/...> headers, so the native core is not directly MSVC-compatible without source changes.
flutter pub getThe current source snapshot contains the application code but does not include a generated windows/ runner directory.
Generate it with:
flutter create --platforms=windows .Then restore dependencies again if Flutter modifies project metadata:
flutter pub getFor a maintained Flutter desktop repository, the generated windows/ directory should normally be committed.
Using Ninja and a MinGW-w64 GCC toolchain:
cmake -S signature -B signature/build \
-G Ninja \
-DCMAKE_CXX_COMPILER=g++Build the library:
cmake --build signature/buildThe produced DLL is expected to be named:
libpcsign.dll
Copy it to the repository root, replacing the bundled binary if necessary:
cp signature/build/libpcsign.dll .Run the Flutter desktop application from the repository root:
flutter run -d windowsThe current code opens the native library and data files through relative paths:
./libpcsign.dll
./pcsign/doc.txt
./pcsign/...
Running from the repository root keeps those paths consistent during development.
This project demonstrates the mechanics of digital-signature creation and verification and the integration of native C++ code with a Flutter desktop interface.
The implementation is intentionally educational and transparent rather than production-oriented:
- MD5 is used as the project hash function;
- RSA signing is implemented as direct modular exponentiation;
- no standard RSA signature padding or signature container format is used;
- key and signature data are stored in plain text files;
- the document path is currently fixed to
pcsign/doc.txt; - the FFI boundary exposes a small manually managed C interface.
The project is best treated as a cryptography and C++/Flutter FFI demonstration, not as a secure document-signing utility.

