Compare commits

...

5 Commits

Author SHA1 Message Date
Evgeny Poberezkin 4758c21afa Merge branch 'master' into v5-dart 2021-11-09 19:30:19 +00:00
Evgeny Poberezkin 800a4f90bf add version command (#123)
* Add /version and /v flags

* Move version info from C Chat.Help to Chat.View

* Move version string, use it in Main startup

* use "SimpleX Chat" consistently

Co-authored-by: TheTaoOfSu <TheTaoOfSu@protonmail.com>
2021-11-07 21:57:05 +00:00
Evgeny Poberezkin deaea44024 CLA workflow (#122)
* CLA: doc and workflow

* update CLA version and signatures path

* update workflow target

* update cla signatures and doc location to simplex-chat/cla repo

* remove CLA.md
2021-11-07 19:40:52 +00:00
Evgeny Poberezkin d79c9d7ef5 cryptographic primitives (#118)
* AES-GSM encryption with padding

* RSA-OAEP encryption and key generation

* SPKI encoding/decoding RSA public keys

* rename functions

* encode/decode RSA keys using asn1lib library

* remove poitycastle namespace

* remove unnecessary typecheck

* fix: ci

Co-authored-by: alex <alex@tekartik.com>
2021-10-16 14:02:06 +01:00
Efim Poberezkin 23468f0afd add section Troubleshooting on Unix to README (#113) 2021-10-05 01:44:36 +10:00
13 changed files with 337 additions and 13 deletions
+36
View File
@@ -0,0 +1,36 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request:
types: [opened, closed, synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request'
# Beta Release
uses: cla-assistant/github-action@v2.1.3-beta
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
path-to-signatures: 'signatures/v1.1/cla.json'
path-to-document: 'https://github.com/simplex-chat/cla/blob/master/CLA.md'
# branch should not be protected
remote-organization-name: simplex-chat
remote-repository-name: cla
branch: 'master'
# allowlist: user1,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA
+30 -3
View File
@@ -24,6 +24,9 @@ See [simplex.chat](https://simplex.chat) website for chat demo and the explanati
- [Terminal chat features](#terminal-chat-features)
- [Installation](#installation)
- [Download chat client](#download-chat-client)
- [Linux and MacOS](#linux-and-macos)
- [Troubleshooting on Unix](#troubleshooting-on-unix)
- [Windows](#windows)
- [Build from source](#build-from-source)
- [Using Docker](#using-docker)
- [Using Haskell stack](#using-haskell-stack)
@@ -83,10 +86,34 @@ chmod +x <binary>
mv <binary> ~/.local/bin/simplex-chat
```
(or any other preferred location on PATH).
(or any other preferred location on `PATH`).
On MacOS you also need to [allow Gatekeeper to run it](https://support.apple.com/en-us/HT202491).
##### Troubleshooting on Unix
If you get `simplex-chat: command not found` when executing the downloaded binary, you need to add the directory containing it to the [`PATH` variable](https://man7.org/linux/man-pages/man7/environ.7.html) (find "PATH" in page). To modify `PATH` for future sessions, put `PATH="$PATH:/path/to/dir"` in `~/.profile`, or in `~/.bash_profile` if that's what you have. See [this answer](https://unix.stackexchange.com/a/26059) for the detailed explanation on the appropriate place to define environment variables for `bash` and other shells.
For example, if you followed the previous instructions, open `~/.profile` for editing:
```sh
vi ~/.profile
```
And add the following line to the end:
```sh
PATH="$PATH:$HOME/.local/bin"
```
Note that this will not automatically update your `PATH` for the remainder of the session. To do this, you should run:
```sh
source ~/.profile
```
Or restart your terminal to start a new session.
#### Windows
```sh
@@ -105,7 +132,7 @@ $ cd simplex-chat
$ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
```
> **Please note:** If you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.8.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
> **Please note:** If you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.10.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
#### Using Haskell stack
@@ -127,7 +154,7 @@ $ stack install
### Running the chat client
To start the chat client, run `simplex-chat` from the terminal.
To start the chat client, run `simplex-chat` from the terminal. If you get `simplex-chat: command not found`, see [Troubleshooting on Unix](#troubleshooting-on-unix).
By default, app data directory is created in the home directory (`~/.simplex`, or `%APPDATA%/simplex` on Windows), and two SQLite database files `simplex.chat.db` and `simplex.agent.db` are initialized in it.
+2 -1
View File
@@ -6,6 +6,7 @@
module Main where
import Simplex.Chat
import Simplex.Chat.Controller (versionNumber)
import Simplex.Chat.Options
import System.Directory (getAppUserDataDirectory)
import System.Terminal (withTerminal)
@@ -20,7 +21,7 @@ welcomeGetOpts :: IO ChatOpts
welcomeGetOpts = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {dbFile} <- getChatOpts appDir
putStrLn "SimpleX chat prototype v0.4.0"
putStrLn $ "SimpleX Chat v" ++ versionNumber
putStrLn $ "db: " <> dbFile <> ".chat.db, " <> dbFile <> ".agent.db"
putStrLn "type \"/help\" or \"/h\" for usage info"
pure opts
+3
View File
@@ -89,6 +89,7 @@ data ChatCommand
| UpdateProfile Profile
| ShowProfile
| QuitChat
| ShowVersion
deriving (Show)
defaultChatConfig :: ChatConfig
@@ -317,6 +318,7 @@ processChatCommand user@User {userId, profile} = \case
showUserProfileUpdated user user'
ShowProfile -> showUserProfile profile
QuitChat -> liftIO exitSuccess
ShowVersion -> printToView clientVersionInfo
where
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
contactMember Contact {contactId} =
@@ -1110,6 +1112,7 @@ chatCommandP =
<|> ("/profile " <|> "/p ") *> (UpdateProfile <$> userProfile)
<|> ("/profile" <|> "/p") $> ShowProfile
<|> ("/quit" <|> "/q") $> QuitChat
<|> ("/version" <|> "/v") $> ShowVersion
where
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
refChar c = c > ' ' && c /= '#' && c /= '@'
+3
View File
@@ -25,6 +25,9 @@ import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
import System.IO (Handle)
import UnliftIO.STM
versionNumber :: String
versionNumber = "0.4.2"
data ChatConfig = ChatConfig
{ agentConfig :: AgentConfig,
dbPoolSize :: Int,
+2 -1
View File
@@ -30,7 +30,7 @@ chatHelpInfo :: [StyledString]
chatHelpInfo =
map
styleMarkdown
[ highlight "Using SimpleX chat prototype",
[ highlight "Using SimpleX Chat",
"Follow these steps to set up a connection:",
"",
green "Step 1: " <> highlight "/connect" <> " - Alice adds a contact.",
@@ -58,6 +58,7 @@ chatHelpInfo =
indent <> highlight "/profile <name> [<full_name>]" <> " - update user profile",
indent <> highlight "/delete <contact>" <> " - delete contact and all messages with them",
indent <> highlight "/markdown " <> " - show supported markdown syntax",
indent <> highlight "/version " <> " - show SimpleX Chat version",
indent <> highlight "/quit " <> " - quit chat",
"",
"The commands may be abbreviated to a single letter: " <> listHighlight ["/c", "/f", "/g", "/p", "/h"] <> ", etc."
+4
View File
@@ -62,6 +62,7 @@ module Simplex.Chat.View
showMessageError,
safeDecodeUtf8,
msgPlain,
clientVersionInfo,
)
where
@@ -685,3 +686,6 @@ highlight' = highlight
styleTime :: String -> StyledString
styleTime = Styled [SetColor Foreground Vivid Black]
clientVersionInfo :: [StyledString]
clientVersionInfo = [plain $ "SimpleX Chat v" <> versionNumber]
@@ -1,6 +1 @@
// import 'package:simplexmq/simplexmq.dart';
// void main() {
// var awesome = Awesome();
// print('awesome: ${awesome.isAwesome}');
// }
+93
View File
@@ -0,0 +1,93 @@
import 'dart:math' show Random;
import 'dart:typed_data';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/asymmetric/oaep.dart';
import 'package:pointycastle/asymmetric/rsa.dart';
import 'package:pointycastle/block/aes_fast.dart';
import 'package:pointycastle/block/modes/gcm.dart';
import 'package:pointycastle/key_generators/api.dart';
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
import 'package:pointycastle/random/fortuna_random.dart';
class AESKey {
final Uint8List _key;
AESKey._make(this._key);
static AESKey random([bool secure = false]) =>
AESKey._make((secure ? secureRandomBytes : pseudoRandomBytes)(32));
static AESKey decode(Uint8List rawKey) => AESKey._make(rawKey);
Uint8List encode() => _key;
}
Uint8List randomIV() {
return pseudoRandomBytes(16);
}
Uint8List secureRandomBytes(int len) {
return _randomBytes(len, Random.secure());
}
final sessionSeed = Random.secure();
Uint8List pseudoRandomBytes(int len) {
return _randomBytes(len, sessionSeed);
}
// len should be divisible by 4
Uint8List _randomBytes(int len, Random seedSource) {
final bytes = Uint8List(len);
for (int i = 0; i < len; i++) {
bytes[i] = seedSource.nextInt(256);
}
return bytes;
}
final empty = Uint8List(0);
final paddingByte = '#'.codeUnitAt(0);
Uint8List encryptAES(AESKey key, Uint8List iv, int padTo, Uint8List data) {
if (data.length >= padTo) throw ArgumentError('large message');
final padded = Uint8List(padTo);
padded.setAll(0, data);
padded.fillRange(data.length, padTo, paddingByte);
return _makeGCMCipher(key, iv, true).process(padded);
}
Uint8List decryptAES(AESKey key, Uint8List iv, Uint8List encryptedAndTag) {
return _makeGCMCipher(key, iv, false).process(encryptedAndTag);
}
GCMBlockCipher _makeGCMCipher(AESKey key, Uint8List iv, bool encrypt) {
return GCMBlockCipher(AESFastEngine())
..init(encrypt, AEADParameters(KeyParameter(key._key), 128, iv, empty));
}
FortunaRandom _secureFortunaRandom() {
return FortunaRandom()..seed(KeyParameter(secureRandomBytes(32)));
}
AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> generateRSAkeyPair(
[int bitLength = 2048]) {
final keyGen = RSAKeyGenerator()
..init(ParametersWithRandom(
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
_secureFortunaRandom()));
final pair = keyGen.generateKeyPair();
return AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(
pair.publicKey as RSAPublicKey, pair.privateKey as RSAPrivateKey);
}
Uint8List encryptOAEP(RSAPublicKey key, Uint8List data) {
final oaep = OAEPEncoding(RSAEngine())
..init(true, PublicKeyParameter<RSAPublicKey>(key));
return oaep.process(data);
}
Uint8List decryptOAEP(RSAPrivateKey key, Uint8List data) {
final oaep = OAEPEncoding(RSAEngine())
..init(false, PrivateKeyParameter<RSAPrivateKey>(key));
return oaep.process(data);
}
+109
View File
@@ -0,0 +1,109 @@
import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart';
import 'package:pointycastle/asymmetric/api.dart'
show RSAPublicKey, RSAPrivateKey;
const _rsaOid = '1.2.840.113549.1.1.1';
final _asnRsaOid = ASN1ObjectIdentifier.fromComponentString(_rsaOid);
final _asn1Null = ASN1Null();
ASN1Sequence _asn1Sequence(List<ASN1Object> elements) {
final seq = ASN1Sequence()..elements = elements;
return seq;
}
void _assertRsaAlgorithm(ASN1Sequence seq) {
if (seq.elements.isEmpty ||
seq.elements[0] is! ASN1ObjectIdentifier ||
(seq.elements[0] as ASN1ObjectIdentifier).identifier != _rsaOid) {
throw Exception('Invalid key algorithm identifier');
}
}
/// Decodes binary PKCS1 to [RSAPublicKey]
RSAPublicKey decodeRsaPubKeyPKCS1(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 2 || els[0] is! ASN1Integer || els[1] is! ASN1Integer) {
throw Exception('Invalid PKCS1 encoding');
}
return RSAPublicKey(
(els[0] as ASN1Integer).valueAsBigInteger!,
(els[1] as ASN1Integer).valueAsBigInteger!,
);
}
/// Decodes binary SPKI to [RSAPublicKey]
RSAPublicKey decodeRsaPubKey(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 2 || els[1] is! ASN1BitString || els[0] is! ASN1Sequence) {
throw Exception('Invalid SPKI structure');
}
_assertRsaAlgorithm(els[0] as ASN1Sequence);
return decodeRsaPubKeyPKCS1(els[1].valueBytes().sublist(1));
}
/// Encodes [key] as binary PKCS1
Uint8List encodeRsaPubKeyPKCS1(RSAPublicKey key) =>
_asn1Sequence([ASN1Integer(key.modulus!), ASN1Integer(key.publicExponent!)])
.encodedBytes;
/// Encodes [key] as binary SPKI
Uint8List encodeRsaPubKey(RSAPublicKey key) => _asn1Sequence([
_asn1Sequence([_asnRsaOid, _asn1Null]),
ASN1BitString(encodeRsaPubKeyPKCS1(key))
]).encodedBytes;
/// Decodes binary PKCS1 to [RSAPrivateKey]
RSAPrivateKey decodeRsaPrivKeyPKCS1(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 9 || els.any((el) => el is! ASN1Integer)) {
throw Exception('Invalid PKCS1 encoding');
}
return RSAPrivateKey(
(els[1] as ASN1Integer).valueAsBigInteger!,
(els[3] as ASN1Integer).valueAsBigInteger!,
(els[4] as ASN1Integer).valueAsBigInteger!,
(els[5] as ASN1Integer).valueAsBigInteger!);
}
/// Decodes binary PKCS8 to [RSAPrivateKey]
RSAPrivateKey decodeRsaPrivKey(Uint8List bytes) {
final els = ASN1Sequence.fromBytes(bytes).elements;
if (els.length != 3 ||
els[1] is! ASN1Sequence ||
els[2] is! ASN1OctetString) {
throw Exception('Invalid PKCS8 structure');
}
_assertRsaAlgorithm(els[1] as ASN1Sequence);
return decodeRsaPrivKeyPKCS1(els[2].valueBytes());
}
final _asnZero = ASN1Integer(BigInt.from(0));
/// Encodes [key] as PKCS1 binary
Uint8List encodeRsaPrivKeyPKCS1(RSAPrivateKey key) {
final d = key.privateExponent!;
final p = key.p!;
final q = key.q!;
final dModP = d % (p - BigInt.from(1));
final dModQ = d % (q - BigInt.from(1));
final coefficient = q.modInverse(p);
return _asn1Sequence([
_asnZero,
ASN1Integer(key.modulus!),
ASN1Integer(key.publicExponent!),
ASN1Integer(d),
ASN1Integer(p),
ASN1Integer(q),
ASN1Integer(dModP),
ASN1Integer(dModQ),
ASN1Integer(coefficient),
]).encodedBytes;
}
/// Encodes [key] as PKCS8 binary
Uint8List encodeRsaPrivKey(RSAPrivateKey key) => _asn1Sequence([
_asnZero,
_asn1Sequence([_asnRsaOid, _asn1Null]),
ASN1OctetString(encodeRsaPrivKeyPKCS1(key))
]).encodedBytes;
+3 -3
View File
@@ -6,9 +6,9 @@ publish_to: none
environment:
sdk: '>=2.14.0 <3.0.0'
# dependencies:
# path: ^1.8.0
dependencies:
asn1lib: ^1.0.2
pointycastle: ^3.3.4
dev_dependencies:
lints: ^1.0.0
+29
View File
@@ -0,0 +1,29 @@
import 'package:simplexmq/src/buffer.dart';
import 'package:simplexmq/src/crypto.dart';
import 'package:test/test.dart';
void main() {
group('AES-GCM encryption with padding', () {
test('encrypt and decrypt', () {
final key = AESKey.random();
final iv = pseudoRandomBytes(16);
final data = encodeAscii('hello');
final cipherText = encryptAES(key, iv, 32, data);
expect(cipherText.length, 32 + 16);
final decrypted = decryptAES(key, iv, cipherText);
expect(decodeAscii(decrypted),
'hello' + List.filled(32 - 'hello'.length, '#').join());
});
});
group('RSA-OAEP encryption', () {
test('encrypt and decrypt', () {
final keyPair = generateRSAkeyPair();
final data = encodeAscii('hello there');
final cipherText = encryptOAEP(keyPair.publicKey, data);
expect(cipherText.length, 2048 ~/ 8);
final decrypted = decryptOAEP(keyPair.privateKey, cipherText);
expect(decodeAscii(decrypted), 'hello there');
});
});
}
@@ -0,0 +1,23 @@
import 'package:simplexmq/src/buffer.dart';
import 'package:simplexmq/src/rsa_keys.dart';
import 'package:test/test.dart';
void main() {
group('RSA keys encoding', () {
test('SPKI encode/decode RSA public key', () {
final keyStr =
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fdE2lndTnPi7QHS2OqP1YE6ZH6sGdf7Boji6jPgQVJB289aQAPZRSJlg6s+xHC52sa2isFiuZN2uFENNWznuZsOWXkHMthbo9Qkp7ZjOhomZURtsIsaRny9GTcaFOrd19rqbsrCRLyb3xtwbQjv/2HEGNZyP9YsGsZijTJaV0yQNEp/5Gt3jHebJ8mqLdBr/aDQBf3oSsmUDDvocGU4kL14GOuVYCKNlEUrFe1X1poSXLH0uu485GVfHB72XjKP/flS2rL91fguqMil1nkelL1K4WOyx1Z87LyyXT2Vh4GRLVHG/a9LyPpw7ovQlO5RIr6suODkXwbAUHq/8j5IDwIDAQAB';
final key = decodeRsaPubKey(decode64(encodeAscii(keyStr))!);
final encKey = decodeAscii(encode64(encodeRsaPubKey(key)));
expect(encKey, keyStr);
});
test('PKCS8 encode/decode RSA private key', () {
final keyStr =
'MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZ90TaWd1Oc+LtAdLY6o/VgTpkfqwZ1/sGiOLqM+BBUkHbz1pAA9lFImWDqz7EcLnaxraKwWK5k3a4UQ01bOe5mw5ZeQcy2Fuj1CSntmM6GiZlRG2wixpGfL0ZNxoU6t3X2upuysJEvJvfG3BtCO//YcQY1nI/1iwaxmKNMlpXTJA0Sn/ka3eMd5snyaot0Gv9oNAF/ehKyZQMO+hwZTiQvXgY65VgIo2URSsV7VfWmhJcsfS67jzkZV8cHvZeMo/9+VLasv3V+C6oyKXWeR6UvUrhY7LHVnzsvLJdPZWHgZEtUcb9r0vI+nDui9CU7lEivqy44ORfBsBQer/yPkgPAgMBAAECggEAezFPgB4EgB/tpUk/k4xXiTPF/iC+QskYvyPFJNv3JtRIFuWGO+Iw/esn9xhlnH+d+/IOIDSXCQ44ropY7dZEzlm97YIDOJCikuEHaqciRCedheT8Hikwy6Aa/NJw8lug0SyRDdeZn2H+s0X98BJ6Gxx1yhgCcOQq/2MbNnS8LNQ0yNNHu9Ds2K5Weiwhb9nrLLuMvrF/k1z0QNi5mCzDZK8iDMr87UZycmKKue13/xppI1pddJm4Ta13/OmZtYe2d5UgK9FrLStFkl7yqWnIcDueCOZvqo4nIfxPlPVolQ9B9RXL2tctkYRVy6FBkZIJkSk4O1Vz5BuPgBy9McoRMQKBgQD5xmVKAnvBQ08BUGXw5HVy4qR1Oj/EmxXKNypZsTdaKpiMfjNB8GbegO0na9Ry7sRo5g15vXpjCS6LDwGx4fZ/5u4N5CP3845DscrZjebs0tS7u5USPMoMzZ/KYfddRsGdm7y9HMp3Z6O3ZGGqZ15VFHGYjRjzK8BYE91rvv1WlwKBgQDfZe0c8/aa25KHW2zLRtSOR8ze+pz79hxNmpCeOoyzfaVRzwDh3KUl78PEWqMbJ7EdEdokCisFU0yEpuuc29JD6l9YuQmYH2VGdyg52iPPCXJOP1PBO0VQ/D/cAmZd/75cmoiyC7kELHfiAWBqO/7xpWkNiEpZZcI33DbzReYBSQKBgBKGuaqUppM+J9UEHpuQhnmf/+zGBkbR7frSvqxqbZ2dfTUmgyzH5Qlp7K043UgtF5pkPemiuToxSyd7VHfaN8ti2JNlMZnJkerJfC9IzDESrj7CehshMSdj9Q8w1wUvI1tKWuR4Bzh2Enme03OtORz8aDSVep1GyHx/9LNyNh4/AoGAcpdk/nIB8ENrMTVrZAYsJ+OaqlIhTnla4U/EmPVtkPCFaaZmTHUS3ZfUcpcPjXFZv5CVteDlWnD1EiJRP3/epmnFiMw5qKeKGpAquSo1LhEpagu/2aGel8EcvK0ad2Mk8XlvXuz2dbads/eCzluCFdAESCW+BYdWDbNPGJClP8kCgYBoT+0res0efi1cn6H0fPx/q33Wmgf47txVrzQN0ZEWDFOOhnErvGpRan9AG+LGvp7TvWWHnW13qjFCXGocWcbaoqsLabkov961R8ij2MTeToz6V+7YfK0KBt/h2HHJ5t/CybNxE5iYFyUMI7GTlC2GzFrnvxH/UYwUma1AplkpEw==';
final key = decodeRsaPrivKey(decode64(encodeAscii(keyStr))!);
final encKey = decodeAscii(encode64(encodeRsaPrivKey(key)));
expect(encKey, keyStr);
});
});
}