Update example app with full tests

This commit is contained in:
Hugo Pointcheval 2020-12-20 22:20:22 +01:00
parent a77ef8df00
commit e32c54d685
9 changed files with 672 additions and 273 deletions

View File

@ -1,10 +1,11 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'dart:developer';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:native_crypto/native_crypto.dart';
import 'package:native_crypto_example/pages/kemPage.dart';
import 'pages/benchmarkPage.dart';
import 'pages/cipherPage.dart';
import 'pages/hashKeyDerivationPage.dart';
void main() => runApp(MyApp());
@ -14,163 +15,20 @@ class MyApp extends StatefulWidget {
}
class _MyAppState extends State<MyApp> {
final textController = TextEditingController();
final pwdController = TextEditingController();
String _output = 'none';
String _bench;
AESCipher aes;
CipherText cipherText;
Uint8List plainText;
SecretKey key;
void _generateKey() async {
var output;
try {
aes = await AESCipher.generate(
AESKeySize.bits256,
CipherParameters(
BlockCipherMode.CBC,
PlainTextPadding.PKCS5,
),
);
output = 'Key generated. Length: ${aes.secretKey.encoded.length}';
} catch (e) {
print(e);
output = e.message;
}
int _currentIndex = 0;
final List<Widget> _children = [
HashKeyDerivationPage(),
CipherPage(),
KemPage(),
BenchmarkPage(),
];
void onTabTapped(int index) {
setState(() {
_output = output;
_currentIndex = index;
});
}
void _pbkdf2() async {
final password = pwdController.text.trim();
var output;
if (password.isEmpty) {
output = 'Password is empty';
} else {
PBKDF2 _pbkdf2 =
PBKDF2(keyLength: 32, iteration: 1000, hash: HashAlgorithm.SHA512);
await _pbkdf2.derive(password: password, salt: 'salty');
key = _pbkdf2.key;
output = 'Key successfully derived.';
}
setState(() {
_output = output;
});
}
void _encrypt() async {
final plainText = textController.text.trim();
var output;
if (plainText.isEmpty) {
output = 'Entry is empty';
} else {
var stringToBytes = TypeHelper().stringToBytes(plainText);
cipherText = await aes.encrypt(stringToBytes);
output = 'String successfully encrypted.';
}
setState(() {
_output = output;
});
}
void _alter() async {
var output;
if (cipherText == null || cipherText.bytes.isEmpty) {
output = 'Encrypt before altering payload!';
} else {
// Add 1 to the first byte
Uint8List _altered = cipherText.bytes;
_altered[0] += 1;
// Recreate cipher text with altered data
cipherText = AESCipherText(_altered, cipherText.iv);
output = 'Payload altered.';
}
setState(() {
_output = output;
});
}
void _decrypt() async {
var output;
if (cipherText == null || cipherText.bytes.isEmpty) {
output = 'Encrypt before decrypting!';
} else {
try {
plainText = await aes.decrypt(cipherText);
var bytesToString = TypeHelper().bytesToString(plainText);
output = 'String successfully decrypted:\n\n$bytesToString';
} on DecryptionException catch (e) {
output = e.message;
}
}
setState(() {
_output = output;
});
}
Future<String> _benchmark(int megabytes) async {
String output;
var bigFile = Uint8List(megabytes * 1000000);
var before = DateTime.now();
var encryptedBigFile = await aes.encrypt(bigFile);
var after = DateTime.now();
var benchmark =
after.millisecondsSinceEpoch - before.millisecondsSinceEpoch;
output = '$megabytes MB\nAES Encryption: $benchmark ms\n';
before = DateTime.now();
await aes.decrypt(encryptedBigFile);
after = DateTime.now();
benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch;
output += 'AES Decryption: $benchmark ms\n\n';
return output;
}
void _testPerf({int megabytes}) async {
var output = '';
if (megabytes != null) {
output = await _benchmark(megabytes);
setState(() {
_output = output;
});
} else {
setState(() {
_bench = 'Open the logcat!';
});
for (int i = 1; i <= 50; i += 10) {
var benchmark = await _benchmark(i);
log(benchmark, name: 'fr.pointcheval.native_crypto');
}
}
}
@override
void initState() {
// Generate AES instance on init.
_generateKey();
super.initState();
}
void dispose() {
// Clean up the controller when the widget is disposed.
textController.dispose();
pwdController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
@ -179,126 +37,33 @@ class _MyAppState extends State<MyApp> {
centerTitle: true,
title: const Text('Native Crypto'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 10, 20),
child: Center(
child: Column(
children: <Widget>[
TextField(
controller: pwdController,
decoration: InputDecoration(
hintText: 'Test password',
),
),
SizedBox(height: 20),
FlatButton(
onPressed: _pbkdf2,
color: Colors.blue,
child: Text(
'Pbkdf2',
style: TextStyle(color: Colors.white),
)),
SizedBox(height: 30),
TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'Text to encrypt.',
),
),
SizedBox(height: 20),
FlatButton(
onPressed: _encrypt,
color: Colors.blue,
child: Text(
'Encrypt String',
style: TextStyle(color: Colors.white),
)),
FlatButton(
onPressed: _alter,
color: Colors.blue,
child: Text(
'Alter encrypted payload',
style: TextStyle(color: Colors.white),
)),
FlatButton(
onPressed: _decrypt,
color: Colors.blue,
child: Text(
'Decrypt String',
style: TextStyle(color: Colors.white),
)),
SizedBox(height: 20),
// Output
Text(
_output,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
FlatButton(
onPressed: () {
_testPerf(megabytes: 1);
},
color: Colors.blue,
child: Text(
'Benchmark 1 MB',
style: TextStyle(color: Colors.white),
)),
FlatButton(
onPressed: () {
_testPerf(megabytes: 10);
},
color: Colors.blue,
child: Text(
'Benchmark 10 MB',
style: TextStyle(color: Colors.white),
)),
FlatButton(
onPressed: () {
_testPerf(megabytes: 50);
},
color: Colors.blue,
child: Text(
'Benchmark 50 MB',
style: TextStyle(color: Colors.white),
)),
SizedBox(height: 20),
FlatButton(
onPressed: () {
_testPerf();
},
color: Colors.blue,
child: Text(
'Full benchmark',
style: TextStyle(color: Colors.white),
)),
(_bench != null && _bench.isNotEmpty)
? Text(_bench)
: Container(),
],
),
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.black,
showUnselectedLabels: true,
onTap: onTabTapped, // new
currentIndex: _currentIndex, // new
items: [
BottomNavigationBarItem(
icon: Icon(Icons.vpn_key),
label: 'Key',
),
),
BottomNavigationBarItem(
icon: Icon(Icons.lock),
label: 'Encryption',
),
BottomNavigationBarItem(
icon: Icon(Icons.connect_without_contact),
label: 'KEM',
),
BottomNavigationBarItem(
icon: Icon(Icons.timer),
label: 'Benchmark',
),
],
),
),
);
}
}
/// Contains some useful functions.
class TypeHelper {
/// Returns bytes `Uint8List` from a `String`.
Uint8List stringToBytes(String source) {
var list = source.runes.toList();
var bytes = Uint8List.fromList(list);
return bytes;
}
/// Returns a `String` from bytes `Uint8List`.
String bytesToString(Uint8List bytes) {
var string = String.fromCharCodes(bytes);
return string;
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:native_crypto/native_crypto.dart';
import '../session.dart';
import '../widgets/button.dart';
import '../widgets/output.dart';
class BenchmarkPage extends StatefulWidget {
const BenchmarkPage({key}) : super(key: key);
@override
_BenchmarkPageState createState() => _BenchmarkPageState();
}
class _BenchmarkPageState extends State<BenchmarkPage> {
final Output keyContent = Output(
textEditingController: TextEditingController(),
);
final Output benchmarkStatus = Output(
textEditingController: TextEditingController(),
large: true,
);
Future<void> _benchmark() async {
if (Session.secretKey == null || Session.secretKey.isEmpty) {
benchmarkStatus
.print('No SecretKey!\nGo in Key tab and generate or derive one.');
return;
} else if (!Session.aesCipher.isInitialized) {
benchmarkStatus.print(
'Cipher not initialized!\nGo in Key tab and generate or derive one.');
return;
}
benchmarkStatus.print("Benchmark 1/5/10/25/50MB\n");
List<int> testedSizes = [1, 5, 10, 25, 50];
var beforeBench = DateTime.now();
for (int size in testedSizes) {
var bigFile = Uint8List(size * 1000000);
var before = DateTime.now();
var encryptedBigFile = await Session.aesCipher.encrypt(bigFile);
var after = DateTime.now();
var benchmark =
after.millisecondsSinceEpoch - before.millisecondsSinceEpoch;
benchmarkStatus.append('[$size MB] Encryption took $benchmark ms\n');
before = DateTime.now();
await Session.aesCipher.decrypt(encryptedBigFile);
after = DateTime.now();
benchmark = after.millisecondsSinceEpoch - before.millisecondsSinceEpoch;
benchmarkStatus.append('[$size MB] Decryption took $benchmark ms\n\n');
}
var afterBench = DateTime.now();
var benchmark =
afterBench.millisecondsSinceEpoch - beforeBench.millisecondsSinceEpoch;
var sum = testedSizes.reduce((a, b) => a + b);
benchmarkStatus.append(
'Benchmark finished.\nGenerated, encrypted and decrypted $sum MB in $benchmark ms');
}
void _clear() {
benchmarkStatus.clear();
}
@override
void initState() {
super.initState();
if (Session.secretKey != null) {
keyContent.print(Session.secretKey.encoded.toString());
Session.aesCipher = AESCipher(
Session.secretKey,
CipherParameters(
BlockCipherMode.CBC,
PlainTextPadding.PKCS5,
),
);
}
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Align(
child: Text("Secret Key"),
alignment: Alignment.centerLeft,
),
keyContent,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Button(
onPressed: _benchmark,
label: "Launch benchmark",
),
Button(
onPressed: _clear,
label: "Clear",
),
],
),
benchmarkStatus,
],
),
),
);
}
}

View File

@ -0,0 +1,205 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:native_crypto/native_crypto.dart';
import '../session.dart';
import '../utils.dart';
import '../widgets/button.dart';
import '../widgets/output.dart';
class CipherPage extends StatefulWidget {
const CipherPage({key}) : super(key: key);
@override
_CipherPageState createState() => _CipherPageState();
}
class _CipherPageState extends State<CipherPage> {
final Output keyContent = Output(
textEditingController: TextEditingController(),
);
final Output encryptionStatus = Output(
textEditingController: TextEditingController(),
);
final Output decryptionStatus = Output(
textEditingController: TextEditingController(),
);
final Output cipherExport = Output(
textEditingController: TextEditingController(),
large: true,
editable: true,
);
final TextEditingController _plainTextController = TextEditingController();
CipherText cipherText;
void _encrypt() async {
final plainText = _plainTextController.text.trim();
if (Session.secretKey == null || Session.secretKey.isEmpty) {
encryptionStatus
.print('No SecretKey!\nGo in Key tab and generate or derive one.');
} else if (!Session.aesCipher.isInitialized) {
encryptionStatus.print(
'Cipher not initialized!\nGo in Key tab and generate or derive one.');
} else if (plainText.isEmpty) {
encryptionStatus.print('Entry is empty');
} else {
var stringToBytes = TypeHelper.stringToBytes(plainText);
cipherText = await Session.aesCipher.encrypt(stringToBytes);
encryptionStatus.print('String successfully encrypted.\n');
encryptionStatus.append("IV: " +
cipherText.iv.toString() +
"\nCipherText: " +
cipherText.bytes.toString());
}
}
void _alter() async {
if (cipherText == null || cipherText.bytes.isEmpty) {
decryptionStatus.print('Encrypt before altering CipherText!');
} else {
// Add 1 to the first byte
Uint8List _altered = cipherText.bytes;
_altered[0] += 1;
// Recreate cipher text with altered data
cipherText = AESCipherText(_altered, cipherText.iv);
encryptionStatus.print('String successfully encrypted.\n');
encryptionStatus.append("IV: " +
cipherText.iv.toString() +
"\nCipherText: " +
cipherText.bytes.toString());
decryptionStatus.print('CipherText altered!\nDecryption will fail.');
}
}
void _decrypt() async {
if (Session.secretKey == null || Session.secretKey.isEmpty) {
decryptionStatus
.print('No SecretKey!\nGo in Key tab and generate or derive one.');
} else if (!Session.aesCipher.isInitialized) {
decryptionStatus.print(
'Cipher not initialized!\nGo in Key tab and generate or derive one.');
} else if (cipherText == null || cipherText.bytes.isEmpty) {
decryptionStatus.print('Encrypt before decrypting!');
} else {
try {
Uint8List plainText = await Session.aesCipher.decrypt(cipherText);
var bytesToString = TypeHelper.bytesToString(plainText);
decryptionStatus
.print('String successfully decrypted:\n\n$bytesToString');
} on DecryptionException catch (e) {
decryptionStatus.print(e.message);
}
}
}
void _export() async {
if (cipherText == null) {
decryptionStatus.print('Encrypt data before export!');
} else {
Uint8List payload = Uint8List.fromList(cipherText.iv + cipherText.bytes);
String data = TypeHelper.bytesToBase64(payload);
decryptionStatus.print('CipherText successfully exported');
cipherExport.print(data);
}
}
void _import() async {
final String data = cipherExport.read();
if (data.isEmpty) {
encryptionStatus.print('CipherText import failed');
} else {
Uint8List payload = TypeHelper.base64ToBytes(data);
Uint8List iv = payload.sublist(0, 16);
Uint8List bytes = payload.sublist(16);
cipherText = AESCipherText(bytes, iv);
encryptionStatus.print('CipherText successfully imported\n');
encryptionStatus.append("IV: " +
cipherText.iv.toString() +
"\nCipherText: " +
cipherText.bytes.toString());
}
}
@override
void initState() {
super.initState();
if (Session.secretKey != null) {
keyContent.print(Session.secretKey.encoded.toString());
Session.aesCipher = AESCipher(
Session.secretKey,
CipherParameters(
BlockCipherMode.CBC,
PlainTextPadding.PKCS5,
),
);
}
}
@override
void dispose() {
super.dispose();
_plainTextController.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Align(
child: Text("Secret Key"),
alignment: Alignment.centerLeft,
),
keyContent,
TextField(
controller: _plainTextController,
decoration: InputDecoration(
hintText: 'Plain text',
),
),
Button(
onPressed: _encrypt,
label: "Encrypt",
),
encryptionStatus,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Button(
onPressed: _alter,
label: "Alter cipher",
),
Button(
onPressed: _decrypt,
label: "Decrypt",
),
],
),
decryptionStatus,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Button(
onPressed: _export,
label: "Export cipher",
),
Button(
onPressed: _import,
label: "Import cipher",
),
],
),
cipherExport
],
),
),
);
}
}

View File

@ -0,0 +1,180 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:native_crypto/native_crypto.dart';
import '../session.dart';
import '../utils.dart';
import '../widgets/button.dart';
import '../widgets/output.dart';
class HashKeyDerivationPage extends StatefulWidget {
const HashKeyDerivationPage({key}) : super(key: key);
@override
_HashKeyDerivationPageState createState() => _HashKeyDerivationPageState();
}
class _HashKeyDerivationPageState extends State<HashKeyDerivationPage> {
final Output keyContent = Output(
textEditingController: TextEditingController(),
);
final Output keyStatus = Output(
textEditingController: TextEditingController(),
);
final Output keyExport = Output(
textEditingController: TextEditingController(),
large: true,
editable: true,
);
final Output pbkdf2Status = Output(
textEditingController: TextEditingController(),
);
final Output hashStatus = Output(
textEditingController: TextEditingController(),
);
final TextEditingController _pwdTextController = TextEditingController();
final TextEditingController _messageTextController = TextEditingController();
final TextEditingController _keyTextController = TextEditingController();
void _generate() async {
try {
Session.secretKey = await SecretKey.generate(256, CipherAlgorithm.AES);
keyContent.print(Session.secretKey.encoded.toString());
keyStatus.print(
"Secret Key successfully generated.\nLength: ${Session.secretKey.encoded.length} bytes");
} catch (e) {
keyStatus.print(e.message);
}
}
void _pbkdf2() async {
final password = _pwdTextController.text.trim();
if (password.isEmpty) {
pbkdf2Status.print('Password is empty');
} else {
PBKDF2 _pbkdf2 =
PBKDF2(keyLength: 32, iteration: 1000, hash: HashAlgorithm.SHA512);
await _pbkdf2.derive(password: password, salt: 'salty');
SecretKey key = _pbkdf2.key;
pbkdf2Status.print('Key successfully derived.');
Session.secretKey = key;
keyContent.print(Session.secretKey.encoded.toString());
}
}
void _export() async {
if (Session.secretKey == null || Session.secretKey.isEmpty) {
keyStatus
.print('No SecretKey!\nGenerate or derive one before exporting!');
} else {
String key = TypeHelper.bytesToBase64(Session.secretKey.encoded);
keyStatus.print('Key successfully exported');
keyExport.print(key);
}
}
void _import() async {
final String key = keyExport.read();
if (key.isEmpty) {
keyStatus.print('Key import failed');
} else {
Uint8List keyBytes = TypeHelper.base64ToBytes(key);
Session.secretKey =
SecretKey.fromBytes(keyBytes, algorithm: CipherAlgorithm.AES);
keyStatus.print('Key successfully imported');
keyContent.print(Session.secretKey.encoded.toString());
}
}
void _hash() async {
final message = _messageTextController.text.trim();
if (message.isEmpty) {
hashStatus.print('Message is empty');
} else {
MessageDigest md = MessageDigest.getInstance("sha256");
Uint8List hash = await md.digest(TypeHelper.stringToBytes(message));
hashStatus.print('Message successfully hashed.\n' + hash.toString());
}
}
@override
void initState() {
super.initState();
if (Session.secretKey != null) {
keyContent.print(Session.secretKey.encoded.toString());
}
}
@override
void dispose() {
super.dispose();
_pwdTextController.dispose();
_messageTextController.dispose();
_keyTextController.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Align(
child: Text("Secret Key"),
alignment: Alignment.centerLeft,
),
keyContent,
Button(
onPressed: _generate,
label: "Generate key",
),
keyStatus,
TextField(
controller: _pwdTextController,
decoration: InputDecoration(
hintText: 'Password',
),
),
Button(
onPressed: _pbkdf2,
label: "Apply PBKDF2",
),
pbkdf2Status,
TextField(
controller: _messageTextController,
decoration: InputDecoration(
hintText: 'Message',
),
),
Button(
onPressed: _hash,
label: "Hash",
),
hashStatus,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Button(
onPressed: _export,
label: "Export key",
),
Button(
onPressed: _import,
label: "Import key",
),
],
),
keyExport
],
),
),
);
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'package:flutter/material.dart';
class KemPage extends StatefulWidget {
KemPage({Key key}) : super(key: key);
@override
_KemPageState createState() => _KemPageState();
}
class _KemPageState extends State<KemPage> {
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text("Not implemented."),
),
);
}
}

9
example/lib/session.dart Normal file
View File

@ -0,0 +1,9 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'package:native_crypto/native_crypto.dart';
class Session {
static SecretKey secretKey;
static AESCipher aesCipher;
}

30
example/lib/utils.dart Normal file
View File

@ -0,0 +1,30 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'dart:typed_data';
import 'dart:convert';
/// Contains some useful functions.
class TypeHelper {
/// Returns bytes [Uint8List] from a [String].
static Uint8List stringToBytes(String source) {
var list = source.runes.toList();
var bytes = Uint8List.fromList(list);
return bytes;
}
/// Returns a [String] from bytes [Uint8List].
static String bytesToString(Uint8List bytes) {
var string = String.fromCharCodes(bytes);
return string;
}
/// Returns a `base64` [String] from bytes [Uint8List].
static String bytesToBase64(Uint8List bytes) {
return base64.encode(bytes);
}
/// Returns a [Uint8List] from a `base64` [String].
static Uint8List base64ToBytes(String encoded) {
return base64.decode(encoded);
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'package:flutter/material.dart';
class Button extends StatelessWidget {
const Button({Key key, this.onPressed, this.label}) : super(key: key);
final void Function() onPressed;
final String label;
@override
Widget build(BuildContext context) {
return Container(
child: FlatButton(
onPressed: onPressed,
color: Colors.blue,
child: Text(
label,
style: TextStyle(color: Colors.white),
),
),
);
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2020
// Author: Hugo Pointcheval
import 'package:flutter/material.dart';
class Output extends StatelessWidget {
const Output(
{Key key,
this.textEditingController,
this.large: false,
this.editable: false})
: super(key: key);
final TextEditingController textEditingController;
final bool large;
final bool editable;
void print(String message) {
textEditingController.text = message;
}
void append(String message) {
textEditingController.text += message;
}
void appendln(String message) {
textEditingController.text += message + "\n";
}
void clear() {
textEditingController.clear();
}
String read() {
return textEditingController.text;
}
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
enableInteractiveSelection: true,
readOnly: editable ? false : true,
minLines: large ? 3 : 1,
maxLines: large ? 50 : 5,
decoration: InputDecoration(border: OutlineInputBorder()),
controller: textEditingController,
),
);
}
}