Compare commits

...
23 Commits
Author SHA1 Message Date
Evgeny @ SimpleX Chat d49e3a303b Merge branch 'master' into names 2026-10-12 05:52:46 +00:00
Evgeny Poberezkin cba1382356 7.1.0.11 2026-10-11 22:33:06 +01:00
EvgenyandEvgeny @ SimpleX Chat b5e8a4b8cc fix tests on ghc 8.10.7 (#1939)
* fix tests on ghc 8.10.7

* fix test

* fix schema dump test

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-11 20:36:51 +01:00
EvgenyandEvgeny @ SimpleX Chat 3b760e8e7b database field conversion instance (#1940)
Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-11 18:48:52 +01:00
Evgeny @ SimpleX Chat f7ca2f956c Merge branch 'master' into names 2026-10-11 17:05:46 +00:00
EvgenyandEvgeny @ SimpleX Chat 3db4029bb9 ci: build cache (#1938)
* ci: build cache

* update build job

* rename steps, cache all builds, patch timestamps for ghc 8.10.7

* refactor, save O0 caches in master and stable builds

* update build to include toolchain fingerprint

* do not save cache on exact match

* remove caching docker from prs

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-11 11:52:24 +01:00
EvgenyandEvgeny @ SimpleX Chat 8a80727fa5 smp server: delayed AUTH responses (#1937)
* smp server: delayed AUTH responses

* plan

* update

* simplify

* simplify

* stat when sent without delay

* update schema

* round up response time to the nearest delay multiple

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-11 07:37:33 +01:00
Evgeny @ SimpleX Chat d4b2e9e8fc Merge branch 'master' into names 2026-10-10 22:18:35 +00:00
EvgenyandEvgeny @ SimpleX Chat a2f985dc68 agent: prepare invitation link, create ratchet when connection is prepared (#1880)
* agent: prepare invitation link

* simplify

* refactor

* fix

* change min SMP version to 15

* remove dead code for SMP version <= 15

* update

* diff

* create ratchet when connection is prepared

* createRatchetIfNotExists

* clean up, only check client notices for contacts, fix orphan record

* simplify

* refactor

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-10 23:15:19 +01:00
Evgeny Poberezkin 58b47e2a86 Merge branch 'master' into names 2026-10-10 17:36:53 +01:00
EvgenyandEvgeny @ SimpleX Chat c18e0297c4 agent: migration to add index to read queues (#1934)
* agent: migration to add index to read queues

* update postgres schema

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-08 17:07:55 +01:00
EvgenyandEvgeny @ SimpleX Chat e24d537eaa agent: fix race when switching reception to the new queue (#1933)
* agent: fix race when switching reception to the new queue

* return bool

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-08 16:03:06 +01:00
sh 7754f8624e smp-server: copy entity IDs out of received block (#1906) 2026-10-08 15:28:38 +01:00
EvgenyandEvgeny @ SimpleX Chat 8d81f775b1 smp protocol: require fixed size corr ID when used as nonce, and min size msg ID to use as nonce (#1925)
* smp protocol: require fixed size corr ID when used as nonce, and min size msg ID to use as nonce

* simpler

* fixes

* randomMsgId

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-08 09:32:11 +01:00
EvgenyandEvgeny @ SimpleX Chat c0a1b7bd2d agent: fail database transaction instantly when database is closed, prevent race conditions in command processing, fix race when uploading files (#1885)
* agent: fail database transaction instantly when database is closed

* update direct-sqlcipher

* update direct-sqlcipher, fixes

* autoincrement command ID

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-06 13:01:55 +01:00
6770e732b7 [names] eth transaction signing for name registration (#1887)
* implement eth transaction signing

* Update src/Simplex/Messaging/Eth/RLP.hs

Co-authored-by: Evgeny <[email protected]>

* fix

Co-authored-by: Evgeny <[email protected]>

* address review comments

* self-review

* $$, puns

---------

Co-authored-by: Evgeny Poberezkin <[email protected]>
Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-05 12:17:14 +01:00
EvgenyandEvgeny @ SimpleX Chat 4ab5376a6e smp: reduce min supported version to 15 (short links support, 3/30/2025) (#1931)
Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-05 11:31:57 +01:00
brenzi 9f907a2740 simplify wallet tooling (#1904) 2026-10-05 08:17:42 +01:00
Evgeny aaffb5b07d Merge pull request #1922 from simplex-chat/ratchet
Improve double ratchet
2026-10-05 08:07:04 +01:00
EvgenyandEvgeny @ SimpleX Chat 65893a359a agent: resolve potential race conditions in double ratchet (#1923)
* agent: resolve potential race conditions in double ratchet

* update

* improve

* diff

* diff

* diff

* simpler

* more races

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-04 22:28:50 +01:00
EvgenyandEvgeny @ SimpleX Chat fcc5670ae2 agent: keep ratchet hashes for connection lifetime, up to some cap, to prevent replay of expired ratchets (#1893)
* agent: keep ratchet hashes for connection lifetime, to prevent replay of expired ratchets

* cap

* simpler

* simpler2

* split

* one query

* simplify

* performance

* migration

* fix schema

* comment

* improve

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-04 14:43:21 +01:00
EvgenyandEvgeny @ SimpleX Chat 64c5d19fc4 agent: aggregate cap on skipped message keys (#1903)
* agent: aggregate cap on skipped message keys

* migration

* update migration

---------

Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
2026-10-04 10:18:41 +01:00
26e70edfbf crypto: primitives for SimpleX names (#1843)
* crypto: Ethereum primitives for SimpleX names

secp256k1 (vendored libsecp256k1 v0.8.0, recoverable ECDSA), BIP-39
mnemonics, BIP-32 derivation, Keccak-256, EIP-55 addresses and EIP-712
typed data hashing.

Client-side signing only: no RLP, no transaction construction, no chain
writes. The resolver path stays read-only.

Verified against published vectors — the 24 official BIP-39 English
vectors, BIP-32 spec vectors 1 and 2, the EIP-55 spec addresses and the
EIP-712 Mail example.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* tiny fixes

* plan update

* expose what's necessary for stealth addresses too"

* add string encoding

* streamline hex encoding

* core: ethereumPath takes the address index

The BIP-44 path is the same for every address under an account, so the
caller that wants one names it, rather than rebuilding the path around the
one this hard-coded.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* core: embed the BIP-39 wordlist as the upstream file

The list was transcoded into Haskell, which can only be audited by reading
2048 words against the source. The upstream english.txt is vendored instead
and embedded at compile time, so auditing it is one sha256sum, and a test
pins that checksum so the copy cannot drift.

file-embed moves into the library, where it was already a dependency of the
executables and the test suite.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX

* review fixes

* fix review findings iterate self-reviews with /goal

* next review round

* cut to the min

* cut comments

* streamline

* Update src/Simplex/Messaging/Crypto/BIP32.hs

Co-authored-by: Evgeny <[email protected]>

* Update CHANGELOG.md

Co-authored-by: Evgeny <[email protected]>

* Update src/Simplex/Messaging/Crypto/BIP32.hs

Co-authored-by: Evgeny <[email protected]>

* Update src/Simplex/Messaging/Crypto/Secp256k1.hs

Co-authored-by: Evgeny <[email protected]>

* Update src/Simplex/Messaging/Crypto/Secp256k1.hs

Co-authored-by: Evgeny <[email protected]>

* opus 5.5 /goal iterations

* self-review rounds

* self-review iteration 2

* self-review iteration 3

* self-review iteration 3ff

* self-review iteration 3fff

* self-review iteration 5

* more doc tweaks

* self-review iteration 9

* self-review iteration 10

* wip

* rng preserve

* finish wip and factor in test vectors

* make wallet creation and account derivation infallible like trezor does

* review iterations

* limit derivation retries to 3

* remove unnecessary export

* random

* security, simplify, imports

* use static context

* fix test

* restore public api to use rng

* back to IO

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
Co-authored-by: Evgeny <[email protected]>
2026-09-29 10:18:21 +01:00
70 changed files with 7262 additions and 590 deletions
+155 -19
View File
@@ -66,8 +66,19 @@ jobs:
build:
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }}, GHC: ${{ matrix.ghc }}"
needs: maybe-release
permissions:
contents: write
packages: write
env:
apps: "smp-server xftp-server ntf-server xftp"
opt: ${{ github.event_name == 'pull_request' && 'O0' || 'O2' }}
ghc_options: ${{ github.event_name == 'pull_request' && '--ghc-options=-O0' || '' }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
postgres_dist: ${{ startsWith(github.ref, 'refs/tags/v') && 'dist-newstyle' || 'dist-postgres' }}
branch: ${{ github.base_ref || github.ref_name }}
cache_o0: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable') }}
image_cache: ghcr.io/simplex-chat/simplexmq-build-cache:${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}
source_hashes: dist-newstyle/sources.sha256
runs-on: ${{ matrix.runner }}
services:
postgres:
@@ -146,6 +157,18 @@ jobs:
sudo apt update
sudo apt -y install postgresql-client-15
- name: Log in to GitHub Container Registry
if: matrix.should_run == true && env.release != 'true'
shell: bash
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "$GHCR_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Compute image cache week
id: week
if: matrix.should_run == true && env.release != 'true'
run: echo "week=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT"
- name: Build and cache Docker image
if: matrix.should_run == true
uses: simplex-chat/docker-build-push-action@v6
@@ -157,20 +180,14 @@ jobs:
build-args: |
TAG=${{ matrix.os }}
GHC=${{ matrix.ghc }}
- name: Cache dependencies
if: matrix.should_run == true
uses: actions/cache@v4
with:
path: |
~/.cabal/store
dist-newstyle
key: ubuntu-${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplexmq.cabal') }}
cache-from: ${{ env.release != 'true' && format('type=registry,ref={0}-{1}', env.image_cache, steps.week.outputs.week) || '' }}
cache-to: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && format('type=registry,ref={0}-{1},mode=max,ignore-error=true', env.image_cache, steps.week.outputs.week) || '' }}
- name: Start container
if: matrix.should_run == true
shell: bash
run: |
mkdir -p ~/.cabal
docker run -t -d \
--device /dev/fuse \
--cap-add SYS_ADMIN \
@@ -181,21 +198,66 @@ jobs:
-v ${{ github.workspace }}:/project \
build/${{ matrix.os }}:latest
- name: Compute cache key prefixes
id: keys
if: matrix.should_run == true && env.release != 'true'
run: |
toolchain=$(docker exec builder dpkg-query -W -f '${db:Status-Status} ${binary:Package} ${Version}\n' 'binutils*' 'cpp*' 'gcc*' '*-dev' | grep '^installed ' | sha256sum | cut -c1-16)
target=${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}-$toolchain
echo "deps=deps-$target" >> "$GITHUB_OUTPUT"
echo "dist=dist-$target" >> "$GITHUB_OUTPUT"
- name: Restore dependencies from cache
id: restore-deps
if: matrix.should_run == true && env.release != 'true'
uses: actions/cache/restore@v4
with:
path: ~/.cabal/store
key: ${{ steps.keys.outputs.deps }}-${{ env.branch }}-${{ github.sha }}
restore-keys: |
${{ steps.keys.outputs.deps }}-${{ env.branch }}-
${{ steps.keys.outputs.deps }}-
- name: Warn if dependencies not in cache
if: matrix.should_run == true && env.release != 'true' && steps.restore-deps.outputs.cache-matched-key == ''
run: echo "::warning title=Cache miss::${{ steps.keys.outputs.deps }}-${{ env.branch }}"
- name: Restore build directories from cache
id: restore-build
if: matrix.should_run == true && env.release != 'true'
uses: actions/cache/restore@v4
with:
path: |
dist-newstyle
dist-postgres
key: ${{ steps.keys.outputs.dist }}-${{ env.opt }}-${{ env.branch }}-${{ github.sha }}
restore-keys: |
${{ steps.keys.outputs.dist }}-${{ env.opt }}-${{ env.branch }}-
${{ steps.keys.outputs.dist }}-${{ env.opt }}-
- name: Warn if build directories not in cache
if: matrix.should_run == true && env.release != 'true' && steps.restore-build.outputs.cache-matched-key == ''
run: echo "::warning title=Cache miss::${{ steps.keys.outputs.dist }}-${{ env.opt }}-${{ env.branch }}"
- name: Set old timestamps on unchanged sources (GHC 8.10.7)
if: matrix.ghc == '8.10.7' && steps.restore-build.outputs.cache-matched-key != '' && hashFiles(env.source_hashes) != ''
run: scripts/ci/source-timestamps.sh restore ${{ env.source_hashes }}
- name: Build smp-server, xftp-server (postgresql) and tests
if: matrix.should_run == true
shell: docker exec -t builder sh -eu {0}
run: |
chmod -fR 777 ~/.cabal ./dist-newstyle || :; git config --global --add safe.directory '*'
cabal clean
chmod -fR 777 ~/.cabal ./dist-newstyle ./dist-postgres || :; git config --global --add safe.directory '*'
scripts/ci/prune-old-builds.sh dist-newstyle dist-postgres
cabal update
cabal build --jobs=$(nproc) --enable-tests -fserver_postgres
cabal build --builddir=${{ env.postgres_dist }} --jobs=$(nproc) --enable-tests -fserver_postgres ${{ env.ghc_options }}
mkdir -p /out
for i in smp-server xftp-server simplexmq-test; do
bin=$(find /project/dist-newstyle -name "$i" -type f -executable)
for i in smp-server xftp-server ntf-server simplexmq-test; do
bin=$(find /project/${{ env.postgres_dist }} -name "$i" -type f -executable)
chmod +x "$bin"
mv "$bin" /out/
cp "$bin" /out/
done
strip /out/smp-server /out/xftp-server
strip /out/smp-server /out/xftp-server /out/ntf-server
- name: Copy simplexmq-test from container
if: matrix.should_run == true
@@ -228,16 +290,17 @@ jobs:
cat hashes.output >> "$GITHUB_OUTPUT"
- name: Build everything else (standard)
id: build-standard
if: matrix.should_run == true
shell: docker exec -t builder sh -eu {0}
run: |
cabal build --jobs=$(nproc)
cabal build --jobs=$(nproc) ${{ env.ghc_options }}
mkdir -p /out
for i in ${{ env.apps }}; do
for i in smp-server xftp-server xftp; do
bin=$(find /project/dist-newstyle -name "$i" -type f -executable)
strip "$bin"
chmod +x "$bin"
mv "$bin" /out/
cp "$bin" /out/
done
- name: Copy binaries from container and prepare them
@@ -282,6 +345,33 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Record source hashes (GHC 8.10.7)
if: matrix.should_run == true && matrix.ghc == '8.10.7'
shell: docker exec -t builder sh -eu {0}
run: scripts/ci/source-timestamps.sh record ${{ env.source_hashes }}
- name: Compute dependency cache key
id: hash-deps
if: matrix.should_run == true && env.release != 'true'
shell: bash
run: echo "hash=$(ls ~/.cabal/store/ghc-*/ | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
- name: Save dependencies to cache
if: matrix.should_run == true && env.release != 'true' && steps.restore-deps.outputs.cache-matched-key != format('{0}-{1}-{2}', steps.keys.outputs.deps, env.branch, steps.hash-deps.outputs.hash)
uses: actions/cache/save@v4
with:
path: ~/.cabal/store
key: ${{ steps.keys.outputs.deps }}-${{ env.branch }}-${{ steps.hash-deps.outputs.hash }}
- name: Save build directories to cache
if: matrix.should_run == true && env.release != 'true' && steps.restore-build.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
dist-newstyle
dist-postgres
key: ${{ steps.keys.outputs.dist }}-${{ env.opt }}-${{ env.branch }}-${{ github.sha }}
- name: Test
if: matrix.should_run == true && matrix.arch == 'x86-64'
timeout-minutes: 120
@@ -306,6 +396,52 @@ jobs:
exit 1
fi
- name: Remove -O2 build directories
id: remove-o2
if: env.cache_o0 == 'true' && !cancelled() && steps.build-standard.outcome == 'success'
shell: docker exec -t builder sh -eu {0}
run: rm -rf dist-newstyle dist-postgres
- name: Restore -O0 build directories from cache
id: restore-build-o0
if: ${{ !cancelled() && steps.remove-o2.outcome == 'success' }}
uses: actions/cache/restore@v4
with:
path: |
dist-newstyle
dist-postgres
key: ${{ steps.keys.outputs.dist }}-O0-${{ env.branch }}-${{ github.sha }}
restore-keys: |
${{ steps.keys.outputs.dist }}-O0-${{ env.branch }}-
${{ steps.keys.outputs.dist }}-O0-
- name: Set old timestamps on unchanged sources for -O0 build (GHC 8.10.7)
if: ${{ !cancelled() && matrix.ghc == '8.10.7' && steps.restore-build-o0.outputs.cache-matched-key != '' && hashFiles(env.source_hashes) != '' }}
run: scripts/ci/source-timestamps.sh restore ${{ env.source_hashes }}
- name: Build at -O0 for pull request caches
id: build-o0
if: ${{ !cancelled() && steps.remove-o2.outcome == 'success' }}
shell: docker exec -t builder sh -eu {0}
run: |
scripts/ci/prune-old-builds.sh dist-newstyle dist-postgres
cabal build --builddir=dist-postgres --jobs=$(nproc) --enable-tests -fserver_postgres --ghc-options=-O0
cabal build --jobs=$(nproc) --ghc-options=-O0
- name: Record source hashes for -O0 build (GHC 8.10.7)
if: ${{ !cancelled() && matrix.ghc == '8.10.7' && steps.build-o0.outcome == 'success' }}
shell: docker exec -t builder sh -eu {0}
run: scripts/ci/source-timestamps.sh record ${{ env.source_hashes }}
- name: Save -O0 build directories to cache
if: ${{ !cancelled() && steps.build-o0.outcome == 'success' && steps.restore-build-o0.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
with:
path: |
dist-newstyle
dist-postgres
key: ${{ steps.keys.outputs.dist }}-O0-${{ env.branch }}-${{ github.sha }}
# =============================
# Resolver test job
# =============================
+3
View File
@@ -4,3 +4,6 @@
[submodule "cbits/blst"]
path = cbits/blst
url = https://github.com/supranational/blst.git
[submodule "cbits/libsecp256k1"]
path = cbits/libsecp256k1
url = https://github.com/bitcoin-core/secp256k1.git
+1 -1
View File
@@ -31,7 +31,7 @@ source-repository-package
source-repository-package
type: git
location: https://github.com/simplex-chat/direct-sqlcipher.git
tag: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9
tag: 2330df3dc8c4b660674d3c02d76bb86f66c2abee
source-repository-package
type: git
Submodule cbits/libsecp256k1 added at 6e2c8bc4ec
+301
View File
@@ -0,0 +1,301 @@
# Ethereum crypto primitives for simplexmq
Client-side crypto for SimpleX names: enough to derive an Ethereum key and its
address from a recovery phrase. General-purpose: these modules contain nothing
specific to names, registrars or relayers.
This is the first part of Workstream B of the SimpleX names v2 plan. The design
it serves: names are owned by a plain EOA derived per name from one BIP-39 seed
held by the device.
## What is deliberately absent
- **No EIP-7702 authorizations.** RLP is implemented only as far as EIP-1559
transactions need it, for registering names; the `RSLV` resolver path in
this repo stays strictly read-only.
- **No stealth addresses and no EIP-712.** Signing covers recoverable ECDSA
over a 32-byte digest and EIP-1559 transactions, which name registration
requires. EIP-712 typed-data hashing and stealth addresses are added with
the first code that uses them.
- **No BIP-32 public derivation.** We always hold the seed, so CKDpub, xpub
serialization and fingerprints are not implemented. Non-hardened *private*
derivation is, because BIP-44 paths end in non-hardened components.
- **English wordlist only, and no full NFKD.** Every English BIP-39 word is
ASCII, so NFKD normalization, which BIP-39 mandates, does not change a valid
phrase's words. NFKD maps Unicode space characters such as U+00A0 and U+3000
to a space; `parsePhrase` splits on any character `Data.Char.isSpace`
accepts and lower-cases with `Data.Text.toLower`, and a word that is then not
in the ASCII wordlist, such as one in fullwidth letters, is rejected. So no
normalization dependency is needed. A passphrase is bytes the caller
normalizes.
## Modules
```
Simplex.Messaging.Crypto.Secp256k1 FFI to libsecp256k1
Simplex.Messaging.Crypto.BIP39 mnemonics
Simplex.Messaging.Crypto.BIP39.English embedded upstream 2048-word list
Simplex.Messaging.Crypto.BIP32 HD derivation, WalletMaster
Simplex.Messaging.Crypto.BIP44 BIP-44 paths, AccountIndex
Simplex.Messaging.Eth.Address addresses, EIP-55
Simplex.Messaging.Eth.RLP Recursive Length Prefix encoding
Simplex.Messaging.Eth.Transaction signed EIP-1559 transactions
```
`keccak256` is added to `Simplex.Messaging.Crypto`, next to `sha3_256`.
## Types
```haskell
newtype Secp256k1PrivateKey -- 32 bytes, validated in [1, n-1]
newtype Secp256k1PublicKey -- libsecp256k1's opaque 64-byte form
newtype Secp256k1Context -- randomised libsecp256k1 context, internal to simplexmq
data PubKeyFormat = Compressed | Uncompressed
newtype WalletEntropy -- 16, 20, 24, 28 or 32 bytes
data EntropyStrength = ES128 | ES160 | ES192 | ES224 | ES256
data ExtendedKey = ExtendedKey {xkKey :: Secp256k1PrivateKey, xkChainCode :: ScrubbedBytes}
data WalletMaster -- entropy with the master key it derives
data CoinType = Ethereum -- SLIP-44 coin types
newtype AccountIndex -- below 2^31
newtype Address -- 20 bytes; StrEncoding is EIP-55, JSON via it
```
Private keys, chain codes, BIP-39 entropy and seeds are `ScrubbedBytes`:
constant-time `Eq`, no readable `Show`, and zeroed when freed. A chain code is
secret too: with the parent public key and one non-hardened child private key,
it yields the parent private key.
## Functions
```haskell
-- Secp256k1
mkPrivateKey :: ScrubbedBytes -> Either String Secp256k1PrivateKey -- 32 bytes in [1, n-1]
unPrivateKey :: Secp256k1PrivateKey -> ScrubbedBytes
secp256k1PublicKey :: TVar ChaChaDRG -> Secp256k1PrivateKey -> IO Secp256k1PublicKey -- total: key is validated
serializePublicKey :: PubKeyFormat -> Secp256k1PublicKey -> IO ByteString
privateKeyTweakAdd :: Secp256k1PrivateKey -> ScrubbedBytes -> IO (Maybe Secp256k1PrivateKey)
-- BIP39
mkEntropy :: ScrubbedBytes -> Either String WalletEntropy
unEntropy :: WalletEntropy -> ScrubbedBytes
randomEntropy :: EntropyStrength -> TVar ChaChaDRG -> STM WalletEntropy
parsePhrase :: Text -> Either String WalletEntropy -- word count, wordlist, checksum
entropyPhrase :: WalletEntropy -> ByteString -- canonical lowercase phrase
entropySeed :: WalletEntropy -> ByteString -> ScrubbedBytes -- PBKDF2 with the passphrase
-- BIP32
masterKey :: ScrubbedBytes -> Either String ExtendedKey
derivePath :: TVar ChaChaDRG -> ExtendedKey -> [Word32] -> IO (Either String ExtendedKey)
renderPath :: [Word32] -> ByteString
hardened :: Word32 -> Word32
isHardened :: Word32 -> Bool
mkWalletMaster :: WalletEntropy -> Either String WalletMaster -- empty passphrase
parseWalletMaster :: ScrubbedBytes -> ScrubbedBytes -> Either String WalletMaster -- entropy and stored master
masterEntropy :: WalletMaster -> WalletEntropy
walletMasterKey :: WalletMaster -> ExtendedKey
masterBytes :: WalletMaster -> ScrubbedBytes -- key then chain code, the storage form
-- BIP44
mkAccountIndex :: Word32 -> Either String AccountIndex
unAccountIndex :: AccountIndex -> Word32
bip44Path :: CoinType -> AccountIndex -> [Word32] -- m/44'/coin'/account'/0/0
-- Crypto
keccak256 :: ByteString -> ByteString
-- Eth
addressFromPublicKey :: Secp256k1PublicKey -> IO Address
addressFromPrivateKey :: TVar ChaChaDRG -> Secp256k1PrivateKey -> IO Address
deriveAddress :: TVar ChaChaDRG -> ExtendedKey -> [Word32] -> IO (Either String (ExtendedKey, Address))
```
Internal to simplexmq, for sharing one context:
```haskell
withContext :: TVar ChaChaDRG -> (Secp256k1Context -> IO a) -> IO a
secp256k1PublicKey_ :: Secp256k1Context -> Secp256k1PrivateKey -> IO Secp256k1PublicKey
derivePath_ :: Secp256k1Context -> ExtendedKey -> [Word32] -> IO (Either String ExtendedKey)
```
`Address` has a `StrEncoding` instance: `strEncode` is the EIP-55 checksummed
form and `strP` accepts bare or `0x`-prefixed hex, rejecting a bad mixed-case
checksum.
Like `Simplex.Messaging.Crypto.randomBytes`, `randomEntropy` takes a
`TVar ChaChaDRG` and runs in `STM`. Every exported function that computes a
public key takes the same generator for the context blinding seed, so this code
never reads system entropy itself.
A value of one of these types is valid by construction: `WalletEntropy` has one
of the five sizes, `AccountIndex` is below 2^31, and `WalletMaster` holds a
master key derived from its entropy, so every function from them is total. The
fallible steps are the boundaries: `parsePhrase` for typed text,
`parseWalletMaster` for a stored row and `mkAccountIndex` for a number.
BIP-32 declares a master or child key invalid when its IL is 0 or at least n,
one case in 2^128, and says to use the next index. This code instead applies
SLIP-0010's rule, which trezor-crypto also implements: recompute the HMAC over
`I` for a master key, or over `0x01 || IR || ser32(i)` for a child. Every key
BIP-32 produces is produced unchanged; only an index BIP-32 would skip gets a
key. The recomputation is bounded to three attempts, after which `masterKey`,
`mkWalletMaster`, `derivePath` and `deriveAddress` return
`Left "derivation failed after 3 attempts"`, an error no input is expected to
produce, which consumers report as an internal error rather than a wallet
condition.
Because `parsePhrase` lower-cases each word, a recovery phrase with a
capitalised word is accepted. This does not change the derived seed:
`entropyPhrase` always rebuilds the canonical lowercase sentence from the
wordlist, and that is what `entropySeed` hashes.
## How applications use it
An application defines the derivation path. For SimpleX names, one seed per
device and one account per name, as in `Simplex.Chat.Wallet` in simplex-chat:
```haskell
ent <- either fail pure $ parsePhrase phrase -- phrase :: Text
master <- either fail pure $ mkWalletMaster ent
n <- either fail pure $ mkAccountIndex account
(xk, addr) <- either fail pure =<< deriveAddress g (walletMasterKey master) (bip44Path Ethereum n)
```
## libsecp256k1 C API mapping
```c
secp256k1_context_create(SECP256K1_CONTEXT_NONE) /* per withContext, then _randomize */
secp256k1_context_destroy(ctx)
secp256k1_ec_pubkey_create(ctx, pubkey, seckey)
secp256k1_ec_pubkey_serialize(secp256k1_context_static, output, outputlen, pubkey, flags)
secp256k1_ec_seckey_tweak_add(secp256k1_context_static, seckey, tweak)
```
Context randomisation blinds only the multiplication of a secret scalar by the
generator, which only `secp256k1_ec_pubkey_create` performs here. `withContext`
creates a context, randomises it with 32 bytes from the caller's
`TVar ChaChaDRG`, passes it to the action and destroys it after. Each exported
function that computes a public key creates one context: `deriveAddress` uses
it for the derivation and the address of its result. Functions that end in `_`
take the context and are defined for sharing it inside simplexmq.
`secp256k1_ec_pubkey_serialize` and `secp256k1_ec_seckey_tweak_add` run on
`secp256k1_context_static`, so `serializePublicKey`, `addressFromPublicKey`
and `privateKeyTweakAdd` take no generator. Every function that calls
libsecp256k1 runs in `IO`; `unsafePerformIO` is used only to read
`secp256k1_context_static` once. The functions that take a `Secp256k1Context`
are therefore not evaluated after the context is destroyed; the context is not
retained past `withContext`.
`Secp256k1PublicKey` is produced only by libsecp256k1 and has no encoding: its
64-byte form is not portable between libsecp256k1 versions, and
`serializePublicKey` returns the SEC1 bytes.
`secp256k1_ec_seckey_tweak_add` returns 0 exactly when BIP-32 declares the
child invalid (tweak out of range, or a zero result), which is why
`privateKeyTweakAdd` returns `Maybe`; `deriveChild` then recomputes as above.
libsecp256k1 never reads OS entropy: the caller supplies the context blinding
seed. So unlike libbbs it causes no `getentropy` / ITMS-90338 issue on iOS, and
requires no equivalent of the `commoncrypto` flag.
## Build
Submodule in `cbits/`, same pattern as blst and libbbs:
`cbits/libsecp256k1`, https://github.com/bitcoin-core/secp256k1, pinned to
**v0.8.0**.
```
c-sources: cbits/libsecp256k1/src/{secp256k1,precomputed_ecmult,precomputed_ecmult_gen}.c
```
No `include-dirs` are needed: every libsecp256k1 include is quoted and relative
to the including file. The embedded wordlist makes `file-embed` a library
dependency; before this change only the `smp-server` and `xftp-server`
executables and the test suite used it. `wide-word` provides `Word128` and
`Word256` for transaction fields, each as narrow as both geth and reth accept.
Built with one `-D` of our own, `ENABLE_MODULE_RECOVERY=1`, for recoverable
signatures. The table-size settings (`ECMULT_WINDOW_SIZE`, `COMB_BLOCKS`,
`COMB_TEETH`) have `#ifndef` defaults in the headers, and the checked-in
precomputed tables are generated for those defaults; every other option, such
as the other optional modules and x86-64 assembly, is off unless a `-D`
enables it. `secp256k1.c` defines `SECP256K1_BUILD` itself.
32-bit targets (armv7a-android, i686 musl) are covered by libsecp256k1's own
fallback: `src/util.h` selects `SECP256K1_WIDEMUL_INT64`, and with it the 10x26
field and 8x32 scalar backends, when the target has neither a native 128-bit
integer nor 64-bit pointers.
No `flake.nix` change is needed in simplex-chat: the per-platform overrides
there only force `packages.simplexmq.components.library.libs` (external
libraries, i.e. openssl for `extra-libraries: crypto`) and flags. Vendored
`c-sources` require no nix entry, which is why blst and libbbs have none either.
### Cross-compilation status
Verified by building simplex-chat through its flake, before the wordlist in
`Simplex.Messaging.Crypto.BIP39.English` was embedded with Template Haskell
(`embedFile`), the construct the armv7a build fails on below. These builds have
to be rerun:
| Target | Result |
|---|---|
| `x86_64-linux` (native, nix) | compiles and links |
| `aarch64-android` | **compiles and links** into the final shared object |
| `armv7a-android` | libsecp256k1 compiles; final link not reached (see below) |
| `x86_64-windows` (mingw) | blocked before our code, see below |
| `aarch64-darwin-ios` | not yet run (requires a darwin host) |
`aarch64-android` is the meaningful pass: it proves the C both cross-compiles
and links into the shared object included in the app.
The `armv7a-android` build compiles libsecp256k1, which proves that its
`SECP256K1_WIDEMUL_INT64` fallback builds under the NDK, but the build then
fails in simplex-chat's own `Simplex.Chat.Operators`, on the
`$(embedFile "PRIVACY.md")` splice. Cross-compiled Template Haskell runs the
splice on the target via `iserv-proxy` under `qemu-arm`, and that interpreter
fails to resolve `realpath` from `libHSdirectory` and segfaults. So 32-bit
*linking* remains unproven.
`x86_64-windows` fails while bootstrapping the mingw cross-GHC, before any
simplexmq code is compiled: haskell.nix applies
`ghc-9.6-fix-code-symbol-jumps.patch` to `rts/linker/PEi386.c` twice from the
same store path, and the second application aborts. That is a duplicate entry in
the patch list of the pinned haskell.nix branch
(`github:input-output-hk/haskell.nix/armv7a`), not something this change can
influence.
Both gaps can be closed without GHC by compiling the three C files with the
cross toolchain directly and linking a test program against them, which verifies
the C code independently of the Haskell build.
## Tests
`tests/CoreTests/EthCryptoTests.hs`, 104 examples, with published vectors read
from vendored upstream files in `tests/fixtures`, each pinned by a sha256 test:
- **BIP-39**: all 24 English vectors of `trezor/python-mnemonic/vectors.json`:
entropy to phrase, phrase to entropy, seed with the `TREZOR` passphrase, and
the master key against the vector's `xprv`.
- **BIP-32**: vectors 1 to 4 of `bip-0032.mediawiki`, every chain, against its
`xprv`; the two vector 5 keys outside `[1, n-1]` drive the `mkPrivateKey`
rejections. The test decodes base58check itself; the library does not parse
`xprv`, so vector 5's malformed serializations do not apply.
- **EIP-55**: the eight addresses from the EIP-55 spec, round-tripped.
- **BIP-44**: the well-known `0x9858EfFD232B4033E47d90003D41EC34EcaEda94` for
the `abandon ... about` mnemonic at `m/44'/60'/0'/0/0`, plus accounts 1 and 2,
all through `deriveAddress`, whose key is checked against `derivePath`.
- Keccak-256 of the empty string and of `abc`.
- Recovery phrases with capitalised words, extra whitespace, and non-breaking or
ideographic spaces between words.
- Negative cases: zero, short and out-of-range private keys, a tweak that makes
the key zero or is not 32 bytes, bad BIP-39 checksums and word counts,
out-of-range seeds, a stored master that does not match its entropy, account
indexes at or above 2^31, and bad EIP-55 checksums.
The BIP-44 expectations were additionally reproduced by an independent
pure-Python secp256k1 reference written for the purpose, so they were not
computed only by this implementation.
@@ -0,0 +1,62 @@
# Fast rotation: draining the old receive queue
Recipient only. The sender is unchanged.
## Decisions
- New `RcvSwitchStatus` value:
| constructor | encoding |
|------------------|-----------------|
| `RSReceivedQEND` | `received_qend` |
- Stored in `rcv_queues.switch_status`.
- `canAbortRcvSwitch` returns `False` for it.
- `qEndMsg`, by the queue that delivered `QEND`:
- the queue named in `QEND`: the named queue is marked deleted, `ICDeleteRcvQueue` is enqueued.
- the new queue: the named queue is set to `RSReceivedQEND` and stays subscribed.
- `NSCCreate` and `SWITCH QDRcv SPCompleted`: sent when the named queue had no `RSReceivedQEND`.
- Deletion of an `RSReceivedQEND` queue:
- trigger: `OK` response to an `ACK` on that queue, without a `GET` lock on it.
- action: the queue is marked deleted, `ICDeleteRcvQueue` is enqueued.
- Bound, a `cleanupManager` step, for each connection with an `RSReceivedQEND` queue:
- condition: another receive queue of the connection has an active subscription.
- `delete_errors + 1 < deleteErrorCount`: `delete_errors` is incremented.
- otherwise: the queue is deleted as above; the first `DEL` error of `ICDeleteRcvQueue` removes it locally.
- `ackSMPMessage` result, `type QueueDrained = Bool`:
- `OK`: `True`.
- `MSG`: `False`; the message is written to `msgQ`.
- Apps: `received_qend` is added to `RcvSwitchStatus`.
- Migration `20261008_rcv_switch_status`, SQLite and Postgres:
- up: index `idx_rcv_queues_switch_status` on `rcv_queues(switch_status)`.
- down: `received_qend` is set to `NULL`, the index is dropped.
## Mechanics
- `Agent/Protocol.hs`: `RSReceivedQEND`.
- `Agent/Store.hs`: `canAbortRcvSwitch`.
- `Client.hs`: `QueueDrained`; `ackSMPMessage` returns it.
- `Agent/Client.hs`: `sendAck` returns `QueueDrained`.
- `Agent/Store/AgentStore.hs`: `getEndedRcvQueueConnIds`:
```sql
SELECT conn_id FROM rcv_queues WHERE switch_status = ? AND deleted = 0
```
- `Agent.hs`:
- `deleteRcvQueueAsync`: `setRcvQueueDeleted`, then `ICDeleteRcvQueue`.
- `qEndMsg`.
- `ackQueueMessage`.
- `cleanupManager`: `expireEndedRcvQueues`.
- Apps:
- `apps/ios/SimpleXChat/APITypes.swift`
- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt`
## Tests
- `QEND` on the new queue while a message waits on the old queue:
- the message is delivered;
- the old queue is deleted after the `ACK` that returns `OK`.
- The same with a recipient restart before the `ACK`.
- Old server stopped: the old queue is deleted by the bound.
- Downgrade: `received_qend` is cleared, other switch statuses are kept.
+52
View File
@@ -0,0 +1,52 @@
# Delayed AUTH error responses
## Decisions
- Responses that include `ERR AUTH` are sent at the next multiple of the delay after the start time.
- Delayed response sets:
| Source | Responses sent together | Start time |
|---|---|---|
| `receive`, failed verification | verification and parsing errors of one received block | block receipt |
| `client`, command processing | responses and messages of one batch | batch read from `rcvQ` |
| `RFWD`, forwarded command | the `RRES` of that command | start of forwarded command processing |
- A thread created by `forkClient` writes the responses to the send queue.
- Delay 0: the responses are written to the send queue directly.
- `authDelayExceeded`: incremented when processing took longer than the delay.
- Prometheus counter `simplex_smp_auth_delay_exceeded`.
- Not saved in the stats backup.
- Not in the daily stats log.
- Configuration:
| Item | Value |
|---|---|
| INI section | `TRANSPORT` |
| INI key | `auth_error_delay_ms` |
| default | 50 |
| `ServerConfig` field | `authErrorDelay`, microseconds |
| test servers | 20 ms |
- AUTH timing test: elapsed time of 5 requests, test servers' delay.
- Out of scope: NTF and XFTP servers.
## Mechanics
- `Server.hs`:
- `isAuthError`: response is `ERR AUTH`.
- `sendResponses`: responses with `ERR AUTH` to `sendAfterDelay`, others to the send queue.
- `sendAfterDelay`: writes responses to the send queue at the next multiple of the delay.
- `receive`: block receipt time from the `rcvActiveAt` reading.
- `client`: batch time.
- `processForwardedCommand`: start time.
- `Server/Env/STM.hs`: `authErrorDelay` field, `defaultAuthErrorDelayMs`.
- `Server/Stats.hs`: `authDelayExceeded` in `ServerStats` and `ServerStatsData`.
- `Server/Prometheus.hs`: `simplex_smp_auth_delay_exceeded`.
- `Server/Main.hs`: INI value.
- `Server/Main/Init.hs`: INI template line.
- `simplexmq.cabal`: `timeit` removed from the test suite.
- `tests/SMPClient.hs`: `authErrorDelay = 20000`.
- `tests/ServerTests.hs`:
- delayed AUTH for wrong key, absent queue, absent link and suspended queue; immediate `SOK`.
- AUTH timing test measures elapsed time.
- `tests/SMPProxyTests.hs`: delayed AUTH to forwarded `SEND` and `LGET`.
+3 -2
View File
@@ -208,7 +208,7 @@ This syntax of decrypted SMP client message body is defined by `decryptedAgentMe
Decrypted SMP message client body can be one of 4 types:
- `agentConnInfo` - used by the initiating party when confirming reply queue - sent in `agentConfirmation` envelope.
- `agentConnInfoReply` - used by accepting party, includes reply queue(s) in the initial confirmation - sent in `agentConfirmation` envelope.
- `agentRatchetInfo` - used to pass additional information when renegotiating double ratchet encryption - sent in `agentRatchetKey` envelope.
- `agentRatchetInfo` - used to pass additional information when renegotiating double ratchet encryption - sent in `agentRatchetKey` envelope. A key sent in reply to another key includes the hash of that key; agents do not reply to such keys.
- `agentMessage` - all other agent messages.
`agentMessage` contains these parts:
@@ -233,7 +233,8 @@ connInfo = *OCTET
agentConnInfoReply = %s"D" smpQueues connInfo
smpQueues = length 1*newQueueInfo ; NonEmpty list of reply queues
agentRatchetInfo = %s"R" ratchetInfo
ratchetInfo = *OCTET
ratchetInfo = [answeredKeyHash *OCTET] ; bytes after answeredKeyHash are ignored
answeredKeyHash = %s"0" / (%s"1" shortString) ; "0" in a key that starts renegotiation, otherwise SHA-256 of the two raw public keys of the answered key
agentMessage = %s"M" agentMsgHeader aMessage
agentMsgHeader = agentMsgId prevMsgHash
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
version=$(awk '/^version:/ {print $2}' simplexmq.cabal)
for dir in "$@"; do
if [ -d "$dir/build" ]; then
find "$dir/build" -mindepth 3 -maxdepth 3 -type d -name 'simplexmq-*' ! -name "simplexmq-$version" -exec rm -rf {} +
fi
done
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
set -eu
command=$1
hashes=$2
sources() {
git ls-files -z '*.hs' '*.hsc'
}
case "$command" in
record)
sources | xargs -0 sha256sum > "$hashes"
;;
restore)
sources | xargs -0 touch
while read -r hash file; do
if [ -f "$file" ] && [ "$(sha256sum "$file" | cut -d' ' -f1)" = "$hash" ]; then
touch -d 2000-01-01 "$file"
fi
done < "$hashes"
;;
*)
echo "usage: $0 record|restore <hashes file>" >&2
exit 1
;;
esac
+28 -4
View File
@@ -1,7 +1,7 @@
cabal-version: 3.0
name: simplexmq
version: 7.1.0.10
version: 7.1.0.11
synopsis: SimpleXMQ message broker
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and
@@ -31,6 +31,11 @@ extra-source-files:
cbits/blst/**/*.asm
cbits/libbbs/**/*.c
cbits/libbbs/**/*.h
cbits/libsecp256k1/**/*.c
cbits/libsecp256k1/**/*.h
src/Simplex/Messaging/Crypto/BIP39/english.txt
tests/fixtures/bip39-vectors.json
tests/fixtures/bip-0032.mediawiki
apps/common/Web/static/index.html
apps/common/Web/static/link.html
apps/common/Web/static/media/apk_icon.png
@@ -135,7 +140,12 @@ library
Simplex.Messaging.Crypto.Lazy
Simplex.Messaging.Crypto.Ratchet
Simplex.Messaging.Crypto.BBS
Simplex.Messaging.Crypto.BIP32
Simplex.Messaging.Crypto.BIP39
Simplex.Messaging.Crypto.BIP39.English
Simplex.Messaging.Crypto.BIP44
Simplex.Messaging.Crypto.Entitlement
Simplex.Messaging.Crypto.Secp256k1
Simplex.Messaging.Crypto.SNTRUP761
Simplex.Messaging.Crypto.SNTRUP761.Bindings
Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
@@ -144,6 +154,9 @@ library
Simplex.Messaging.Crypto.ShortLink
Simplex.Messaging.Encoding
Simplex.Messaging.Encoding.String
Simplex.Messaging.Eth.Address
Simplex.Messaging.Eth.RLP
Simplex.Messaging.Eth.Transaction
Simplex.Messaging.Names.Record
Simplex.Messaging.Notifications.Client
Simplex.Messaging.Notifications.Protocol
@@ -195,6 +208,8 @@ library
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260712_address_dr_rpc
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260823_snd_files_entitlement
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260919_ratchet_verify_codes
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260929_ratchet_indexes
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20261008_rcv_switch_status
else
exposed-modules:
Simplex.Messaging.Agent.Store.SQLite
@@ -250,6 +265,9 @@ library
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260712_address_dr_rpc
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260823_snd_files_entitlement
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260919_ratchet_verify_codes
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260929_ratchet_indexes
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20261005_commands_autoincrement
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20261008_rcv_switch_status
Simplex.Messaging.Agent.Store.SQLite.Util
if flag(client_postgres) || flag(server_postgres)
exposed-modules:
@@ -328,7 +346,8 @@ library
cbits/blst/src
cbits/libbbs/include
cbits/libbbs/src
cc-options: -D__BLST_PORTABLE__
-- libsecp256k1's only -D flag enables its recovery module: every other setting has an #ifndef default that matches the checked-in tables
cc-options: -D__BLST_PORTABLE__ -DENABLE_MODULE_RECOVERY=1
if flag(commoncrypto)
cc-options: -DBBS_CRYPTO_CC
frameworks: Security
@@ -342,6 +361,9 @@ library
cbits/libbbs/src/compat-string.c
cbits/libbbs/src/sha256.c
cbits/libbbs/src/shake256.c
cbits/libsecp256k1/src/secp256k1.c
cbits/libsecp256k1/src/precomputed_ecmult.c
cbits/libsecp256k1/src/precomputed_ecmult_gen.c
asm-sources:
cbits/blst/build/assembly.S
extra-libraries:
@@ -369,6 +391,7 @@ library
, cryptostore ==0.3.*
, data-default ==0.7.*
, directory ==1.3.*
, file-embed >=0.0.10 && <0.1
, filepath ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
@@ -392,6 +415,7 @@ library
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, wide-word ==0.1.6.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
@@ -546,6 +570,7 @@ test-suite simplexmq-test
CoreTests.CryptoFileTests
CoreTests.CryptoTests
CoreTests.EncodingTests
CoreTests.EthCryptoTests
CoreTests.MsgStoreTests
CoreTests.RetryIntervalTests
CoreTests.SOCKSSettings
@@ -597,7 +622,7 @@ test-suite simplexmq-test
default-extensions:
StrictData
-- add -fhpc to ghc-options to run tests with coverage
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts "-with-rtsopts=-A64M -N1"
build-depends:
base
, aeson
@@ -635,7 +660,6 @@ test-suite simplexmq-test
, stm
, text
, time
, timeit ==2.0.*
, tls >=1.9.0 && <1.10
, transformers
, unliftio
+3 -2
View File
@@ -568,9 +568,10 @@ runXFTPSndWorker c srv Worker {doWork} = do
notify c sndFileEntityId $ SFPROG uploaded total
when complete $ do
(sndDescr, rcvDescrs) <- sndFileToDescrs sf
flip catchAllErrors (\e -> logError $ "XFTP snd worker error: " <> tshow e) $ do
lift . forM_ prefixPath $ removePath <=< toFSFilePath
withStore' c $ \db -> updateSndFileComplete db sndFileId
notify c sndFileEntityId $ SFDONE sndDescr rcvDescrs (sndFileExpiresAt chunks)
lift . forM_ prefixPath $ removePath <=< toFSFilePath
withStore' c $ \db -> updateSndFileComplete db sndFileId
where
addRecipients :: SndFileChunk -> SndFileChunkReplica -> AM SndFileChunkReplica
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {sndChunkReplicaId, rcvIdsKeys}
+2 -2
View File
@@ -80,7 +80,7 @@ import Simplex.Messaging.Protocol
tParse,
)
import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..))
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
xftpBlockSize :: Int
xftpBlockSize = 16384
@@ -383,7 +383,7 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of
xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion XFTPErrorType c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString
xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey t@(corrId, _, _) = do
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams t
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth False (Just pKey) (C.cbNonce $ bs corrId) tForAuth
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth False (Just pKey) (eitherToMaybe $ C.cbNonce $ bs corrId) tForAuth
xftpEncodeTransmission :: ProtocolEncoding XFTPVersion XFTPErrorType c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString
xftpEncodeTransmission thParams t = xftpEncodeBatch1 (Nothing, encodeTransmission thParams t)
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -312,7 +312,7 @@ import Simplex.Messaging.Session
import Simplex.Messaging.SystemTime
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (HandshakeError (..), SMPServiceRole (..), SMPVersion, ServiceCredentials (..), SessionId, THClientService' (..), THandleAuth (..), THandleParams (sessionId, thAuth, thVersion, serverInfo), TransportError (..), TransportPeer (..), shortLinksSMPVersion, newNtfCredsSMPVersion)
import Simplex.Messaging.Transport (HandshakeError (..), SMPServiceRole (..), SMPVersion, ServiceCredentials (..), SessionId, THClientService' (..), THandleAuth (..), THandleParams (sessionId, thAuth, thVersion, serverInfo), TransportError (..), newNtfCredsSMPVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Credentials
import Simplex.Messaging.Util
@@ -1499,7 +1499,7 @@ newRcvQueue_ c nm userId connId (ProtoServerWithAuth srv auth) vRange cqrd enabl
let sessServiceId = (\THClientService {serviceId = sId} -> sId) <$> (clientService =<< thAuth thParams')
when (isJust serviceId && serviceId /= sessServiceId) $ logError "incorrect service ID in NEW response"
liftIO . logServer "<--" c srv NoEntity $ B.unwords ["IDS", logSecret rcvId, logSecret sndId]
shortLink <- mkShortLinkCreds thParams' qik
shortLink <- mkShortLinkCreds qik
let rq =
RcvQueue
{ userId,
@@ -1540,8 +1540,8 @@ newRcvQueue_ c nm userId connId (ProtoServerWithAuth srv auth) vRange cqrd enabl
(Just ((ntfPublicKey, ntfPrivateKey), dhpk), Just (ServerNtfCreds notifierId dhk')) ->
Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret = C.dh' dhk' dhpk}
_ -> Nothing
mkShortLinkCreds :: THandleParams SMPVersion 'TClient -> QueueIdsKeys -> AM (Maybe ShortLinkCreds)
mkShortLinkCreds thParams' QIK {sndId, queueMode, linkId} = case (cqrd, queueMode) of
mkShortLinkCreds :: QueueIdsKeys -> AM (Maybe ShortLinkCreds)
mkShortLinkCreds QIK {sndId, queueMode, linkId} = case (cqrd, queueMode) of
(CQRMessaging ld, Just QMMessaging) ->
withLinkData ld $ \lnkId CQRData {linkKey, privSigKey, srvReq = (sndId', d)} ->
if sndId == sndId'
@@ -1555,11 +1555,9 @@ newRcvQueue_ c nm userId connId (ProtoServerWithAuth srv auth) vRange cqrd enabl
(_, Nothing) -> newErr "unexpected link ID"
_ -> newErr "unexpected queue mode"
where
v = thVersion thParams'
withLinkData :: Maybe d -> (SMP.LinkId -> d -> AM (Maybe ShortLinkCreds)) -> AM (Maybe ShortLinkCreds)
withLinkData ld_ mkLink = case (ld_, linkId) of
(Just ld, Just lnkId) -> mkLink lnkId ld
(Just _, Nothing) | v < shortLinksSMPVersion -> pure Nothing
(Nothing, Nothing) -> pure Nothing
_ -> newErr "unexpected or absent link ID"
newErr :: String -> AM (Maybe ShortLinkCreds)
@@ -1984,7 +1982,7 @@ decryptSMPMessage :: RcvQueue -> SMP.RcvMessage -> AM SMP.ClientRcvMsgBody
decryptSMPMessage rq SMP.RcvMessage {msgId, msgBody = SMP.EncRcvMsgBody body} =
liftEither $ parse SMP.clientRcvMsgBodyP (AGENT $ A_MESSAGE "decrypt message") =<< decrypt body
where
decrypt = agentCbDecrypt (rcvDhSecret rq) (C.cbNonce msgId)
decrypt = agentCbDecrypt (rcvDhSecret rq) msgId
secureQueue :: AgentClient -> NetworkRequestMode -> RcvQueue -> SndPublicAuthKey -> AM ()
secureQueue c nm rq@RcvQueue {rcvId, rcvPrivateKey} senderKey =
@@ -2082,9 +2080,9 @@ disableQueuesNtfs c = sendTSessionBatches "NDEL" (mkSMPTSession . snd) disableQu
queueCreds :: DisableQueueNtfReq -> (SMP.RecipientId, SMP.RcvPrivateAuthKey)
queueCreds (_, RcvQueue {rcvPrivateKey, rcvId}) = (rcvId, rcvPrivateKey)
sendAck :: AgentClient -> RcvQueue -> MsgId -> AM ()
sendAck :: AgentClient -> RcvQueue -> MsgId -> AM QueueDrained
sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId =
withSMPClient c NRMBackground rq ("ACK:" <> logSecret' msgId) $ \smp ->
withSMPClient c NRMBackground rq ("ACK:" <> logSecret' (SMP.unMsgId msgId)) $ \smp ->
ackSMPMessage smp rcvPrivateKey rcvId msgId
hasGetLock :: SomeRcvQueue q => AgentClient -> q -> IO Bool
@@ -2279,7 +2277,7 @@ agentCbEncryptOnce clientVersion dhRcvPubKey msg = do
-- | NaCl crypto-box decrypt - both for messages received from the server
-- and per-queue E2E encrypted messages from the sender that were inside.
agentCbDecrypt :: C.DhSecretX25519 -> C.CbNonce -> ByteString -> Either AgentErrorType ByteString
agentCbDecrypt :: C.CbNonceI n => C.DhSecretX25519 -> n -> ByteString -> Either AgentErrorType ByteString
agentCbDecrypt dhSecret nonce msg =
first cryptoError $
C.cbDecrypt dhSecret nonce msg
+20 -7
View File
@@ -79,6 +79,7 @@ module Simplex.Messaging.Agent.Protocol
SMPConfirmation (..),
AgentMsgEnvelope (..),
AgentMessage (..),
RatchetInfo (..),
RequestSignature (..),
AgentMessageType (..),
APrivHeader (..),
@@ -245,7 +246,6 @@ import Simplex.Messaging.Crypto.Ratchet
RcvE2ERatchetParams,
RcvE2ERatchetParamsUri,
SndE2ERatchetParams,
RcvE2EPrivRatchetParams,
pattern PQSupportOff,
pattern PQSupportOn,
)
@@ -611,6 +611,7 @@ data RcvSwitchStatus
| RSSendingQADD
| RSSendingQUSE
| RSReceivedMessage
| RSReceivedQEND
deriving (Eq, Show)
instance StrEncoding RcvSwitchStatus where
@@ -619,12 +620,14 @@ instance StrEncoding RcvSwitchStatus where
RSSendingQADD -> "sending_qadd"
RSSendingQUSE -> "sending_quse"
RSReceivedMessage -> "received_message"
RSReceivedQEND -> "received_qend"
strP =
A.takeTill (== ' ') >>= \case
"switch_started" -> pure RSSwitchStarted
"sending_qadd" -> pure RSSendingQADD
"sending_quse" -> pure RSSendingQUSE
"received_message" -> pure RSReceivedMessage
"received_qend" -> pure RSReceivedQEND
_ -> fail "bad RcvSwitchStatus"
instance ToField RcvSwitchStatus where toField = toField . decodeLatin1 . strEncode
@@ -928,7 +931,7 @@ data AgentMessage
| -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation.
-- It made removed REPLY message unnecessary.
AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo
| AgentRatchetInfo ByteString
| AgentRatchetInfo RatchetInfo
| AgentMessage APrivHeader AMessage
| AgentServiceRequest (NonEmpty SMPQueueInfo) (Maybe RequestSignature) MsgBody
| AgentServiceResponse MsgBody
@@ -939,7 +942,7 @@ instance Encoding AgentMessage where
smpEncode = \case
AgentConnInfo cInfo -> smpEncode ('I', Tail cInfo)
AgentConnInfoReply smpQueues cInfo -> smpEncode ('D', smpQueues, Tail cInfo) -- 'D' stands for "duplex"
AgentRatchetInfo info -> smpEncode ('R', Tail info)
AgentRatchetInfo info -> smpEncode ('R', info)
AgentMessage hdr aMsg -> smpEncode ('M', hdr, aMsg)
AgentServiceRequest qs sig_ body -> smpEncode ('A', qs, sig_, Tail body)
AgentServiceResponse body -> smpEncode ('P', Tail body)
@@ -948,13 +951,23 @@ instance Encoding AgentMessage where
smpP >>= \case
'I' -> AgentConnInfo . unTail <$> smpP
'D' -> AgentConnInfoReply <$> smpP <*> (unTail <$> smpP)
'R' -> AgentRatchetInfo . unTail <$> smpP
'R' -> AgentRatchetInfo <$> smpP
'M' -> AgentMessage <$> smpP <*> smpP
'A' -> AgentServiceRequest <$> smpP <*> smpP <*> (unTail <$> smpP)
'P' -> AgentServiceResponse . unTail <$> smpP
'J' -> AgentRejection . unTail <$> smpP
_ -> fail "bad AgentMessage"
data RatchetInfo = RatchetInfo {answeredKeyHash :: Maybe ByteString}
deriving (Eq, Show)
instance Encoding RatchetInfo where
smpEncode RatchetInfo {answeredKeyHash} = smpEncode answeredKeyHash
smpP = do
answeredKeyHash <- smpP <|> pure Nothing
_ <- A.takeByteString
pure RatchetInfo {answeredKeyHash}
-- internal type for storing message type in the database
data AgentMessageType
= AM_CONN_INFO
@@ -1613,7 +1626,7 @@ newtype LinkKey = LinkKey ByteString -- sha3-256(fixed_data)
instance ToField LinkKey where toField (LinkKey s) = toField $ Binary s
-- | Parameters for creating a connection with a prepared link.
data PreparedLinkParams = PreparedLinkParams
data PreparedLinkParams k = PreparedLinkParams
{ -- | Correlation ID / determines sender ID
plpNonce :: C.CbNonce,
-- | Queue E2EE DH key pair
@@ -1628,8 +1641,8 @@ data PreparedLinkParams = PreparedLinkParams
plpSrvWithAuth :: SMPServerWithAuth,
-- | Initial PQ keys
plpInitKeys :: InitialKeys,
-- | Contact address double ratchet keys
plpAddressKeys :: Maybe (RatchetKeyId, RcvE2EPrivRatchetParams 'C.X448)
-- | Contact address double ratchet keys or invitation X3DH keys
plpRatchetKeys :: k
}
instance ConnectionModeI c => ToField (ConnectionLink c) where toField = toField . Binary . strEncode
+1
View File
@@ -219,6 +219,7 @@ canAbortRcvSwitch ConnData {connAgentVersion} = maybe False canAbort . rcvSwchSt
-- if switch is in RSReceivedMessage status, aborting switch (deleting new queue)
-- will break the connection because the sender would have original queue deleted
RSReceivedMessage -> False
RSReceivedQEND -> False
data ClientNtfCreds = ClientNtfCreds
{ -- | key pair to be used by the notification server to authorize transmissions
+52 -22
View File
@@ -76,6 +76,7 @@ module Simplex.Messaging.Agent.Store.AgentStore
getExpiredServiceConns,
deleteExpiredServiceRequests,
getDeletedWaitingDeliveryConnIds,
getConnRatchetSync,
setConnRatchetSync,
addProcessedRatchetKeyHash,
checkRatchetKeyHashExists,
@@ -99,6 +100,7 @@ module Simplex.Messaging.Agent.Store.AgentStore
setSndQueuePrimary,
deleteConnRcvQueue,
incRcvDeleteErrors,
getEndedRcvQueueConnIds,
deleteConnSndQueue,
getPrimaryRcvQueue,
getRcvQueue,
@@ -762,6 +764,10 @@ incRcvDeleteErrors :: DB.Connection -> RcvQueue -> IO ()
incRcvDeleteErrors db RcvQueue {connId, dbQueueId} =
DB.execute db "UPDATE rcv_queues SET delete_errors = delete_errors + 1 WHERE conn_id = ? AND rcv_queue_id = ?" (connId, dbQueueId)
getEndedRcvQueueConnIds :: DB.Connection -> IO [ConnId]
getEndedRcvQueueConnIds db =
map fromOnly <$> DB.query db "SELECT conn_id FROM rcv_queues WHERE switch_status = ? AND deleted = 0" (Only RSReceivedQEND)
deleteConnRcvQueue :: DB.Connection -> RcvQueue -> IO ()
deleteConnRcvQueue db RcvQueue {connId, dbQueueId} =
DB.execute db "DELETE FROM rcv_queues WHERE conn_id = ? AND rcv_queue_id = ?" (connId, dbQueueId)
@@ -1254,7 +1260,7 @@ getLastMsg db connId msgId =
LEFT JOIN snd_messages s ON s.conn_id = r.conn_id AND s.rcpt_internal_id = r.internal_id
WHERE r.conn_id = ? AND r.broker_id = ?
|]
(connId, Binary msgId)
(connId, msgId)
toRcvMsg :: (Int64, InternalTs, BrokerId, BrokerTs) :. (AgentMsgId, MsgIntegrity, MsgHash, AgentMessageType, MsgBody, PQEncryption, Maybe AgentMsgId, Maybe MsgReceiptStatus, BoolInt) -> RcvMsg
toRcvMsg ((agentMsgId, internalTs, brokerId, brokerTs) :. (sndMsgId, integrity, internalHash, msgType, msgBody, pqEncryption, rcptInternalId_, rcptStatus_, BI userAck)) =
@@ -1286,7 +1292,7 @@ checkRcvMsgHashExists db connId hash =
getRcvMsgBrokerTs :: DB.Connection -> ConnId -> SMP.MsgId -> IO (Either StoreError BrokerTs)
getRcvMsgBrokerTs db connId msgId =
firstRow fromOnly (SEMsgNotFound "getRcvMsgBrokerTs") $
DB.query db "SELECT broker_ts FROM rcv_messages WHERE conn_id = ? AND broker_id = ?" (connId, Binary msgId)
DB.query db "SELECT broker_ts FROM rcv_messages WHERE conn_id = ? AND broker_id = ?" (connId, msgId)
deleteMsg :: DB.Connection -> ConnId -> InternalId -> IO ()
deleteMsg db connId msgId =
@@ -1594,12 +1600,16 @@ getRatchet_ q db connId =
where
ratchet = maybe (Left SERatchetNotFound) Right . fromOnly
getSkippedMsgKeys :: DB.Connection -> ConnId -> IO SkippedMsgKeys
getSkippedMsgKeys db connId =
skipped <$> DB.query db "SELECT header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ?" (Only connId)
getSkippedMsgKeys :: DB.Connection -> ConnId -> Int -> IO SkippedMsgKeys
getSkippedMsgKeys db connId maxKeys = do
(keys, oldKeys) <- splitAt maxKeys <$> DB.query db "SELECT skipped_message_id, header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ? ORDER BY skipped_message_id DESC LIMIT ?" (connId, maxKeys + 1)
case oldKeys of
(skippedMsgId :: Int64, _, _, _) : _ -> DB.execute db "DELETE FROM skipped_messages WHERE conn_id = ? AND skipped_message_id <= ?" (connId, skippedMsgId)
[] -> pure ()
pure $ skipped keys
where
skipped = foldl' addSkippedKey M.empty
addSkippedKey smks (hk, msgN, mk) = M.alter (Just . addMsgKey) hk smks
addSkippedKey smks (_, hk, msgN, mk) = M.alter (Just . addMsgKey) hk smks
where
addMsgKey = maybe (M.singleton msgN mk) (M.insert msgN mk)
@@ -2793,6 +2803,11 @@ getDeletedWaitingDeliveryConnIds :: DB.Connection -> IO [ConnId]
getDeletedWaitingDeliveryConnIds db =
map fromOnly <$> DB.query_ db "SELECT conn_id FROM connections WHERE deleted_at_wait_delivery IS NOT NULL"
getConnRatchetSync :: DB.Connection -> ConnId -> IO (Either StoreError RatchetSyncState)
getConnRatchetSync db connId =
firstRow fromOnly SEConnNotFound $
DB.query db "SELECT ratchet_sync_state FROM connections WHERE conn_id = ?" (Only connId)
setConnRatchetSync :: DB.Connection -> ConnId -> RatchetSyncState -> IO ()
setConnRatchetSync db connId ratchetSyncState =
DB.execute db "UPDATE connections SET ratchet_sync_state = ? WHERE conn_id = ?" (ratchetSyncState, connId)
@@ -2814,21 +2829,35 @@ checkRatchetKeyHashExists db connId hash =
(connId, Binary hash)
deleteRatchetKeyHashesExpired :: DB.Connection -> NominalDiffTime -> Int -> IO ()
deleteRatchetKeyHashesExpired db ttl limit = do
deleteRatchetKeyHashesExpired db ttl maxConnHashes = do
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
#if defined(dbPostgres)
DB.execute
db
[sql|
DELETE FROM processed_ratchet_key_hashes
WHERE processed_ratchet_key_hash_id IN (
SELECT processed_ratchet_key_hash_id
FROM processed_ratchet_key_hashes
WHERE created_at < ?
ORDER BY created_at ASC
LIMIT ?
)
|]
(cutoffTs, limit)
("DELETE FROM processed_ratchet_key_hashes h USING (" <> maxExcessIdsQuery <> ") e WHERE h.conn_id = e.conn_id AND h.processed_ratchet_key_hash_id <= e.max_excess_id AND h.created_at < ?")
(maxConnHashes, maxConnHashes, cutoffTs)
#else
maxExcessIds <- DB.query db maxExcessIdsQuery (maxConnHashes, maxConnHashes)
DB.executeMany
db
"DELETE FROM processed_ratchet_key_hashes WHERE conn_id = ? AND processed_ratchet_key_hash_id <= ? AND created_at < ?"
(map (\(connId :: ConnId, maxExcessId :: Int64) -> (connId, maxExcessId, cutoffTs)) maxExcessIds)
#endif
where
maxExcessIdsQuery :: Query
maxExcessIdsQuery =
[sql|
SELECT conn_id, (
SELECT processed_ratchet_key_hash_id
FROM processed_ratchet_key_hashes
WHERE conn_id = c.conn_id
ORDER BY processed_ratchet_key_hash_id DESC
LIMIT 1 OFFSET ?
) AS max_excess_id
FROM processed_ratchet_key_hashes c
GROUP BY conn_id
HAVING COUNT(*) > ?
|]
-- | returns all connection queues, the first queue is the primary one
getRcvQueuesByConnId_ :: DB.Connection -> ConnId -> IO (Maybe (NonEmpty RcvQueue))
@@ -3011,7 +3040,7 @@ insertRcvMsgDetails_ db connId RcvQueue {dbQueueId} RcvMsgData {msgMeta, interna
VALUES
(?,?,?,?,?,?,?,?,?,?)
|]
(connId, dbQueueId, internalRcvId, fst recipient, sndMsgId, Binary (fst broker), snd broker, Binary internalHash, Binary externalPrevSndHash, integrity)
(connId, dbQueueId, internalRcvId, fst recipient, sndMsgId, fst broker, snd broker, Binary internalHash, Binary externalPrevSndHash, integrity)
DB.execute db "INSERT INTO encrypted_rcv_message_hashes (conn_id, hash) VALUES (?,?)" (connId, Binary encryptedMsgHash)
updateRcvMsgHash :: DB.Connection -> ConnId -> AgentMsgId -> InternalRcvId -> MsgHash -> IO ()
@@ -3159,7 +3188,8 @@ createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file
createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId)
createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect"
createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile approvedRelays = runExceptT $ do
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing approvedRelays
nonce <- atomically $ C.randomCbNonce gVar
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId (dummyDst nonce) prefixPath dstPath dstFile Nothing Nothing approvedRelays
(_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId) approvedRelays
liftIO $
forM_ redirectChunks $ \fc@FileChunk {replicas} -> do
@@ -3167,7 +3197,7 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId
pure dstEntityId
where
dummyDst =
dummyDst nonce =
FileDescription
{ party = SFRecipient,
size,
@@ -3175,7 +3205,7 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire
redirect = Nothing,
-- updated later with updateRcvFileRedirect
key = C.unsafeSbKey $ B.replicate 32 '#',
nonce = C.cbNonce "",
nonce,
chunkSize = FileSize 0,
chunks = []
}
@@ -16,6 +16,8 @@ import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260411_service_certs
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260712_address_dr_rpc
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260823_snd_files_entitlement
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260919_ratchet_verify_codes
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260929_ratchet_indexes
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20261008_rcv_switch_status
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -31,7 +33,9 @@ schemaMigrations =
("20260411_service_certs", m20260411_service_certs, Just down_m20260411_service_certs),
("20260712_address_dr_rpc", m20260712_address_dr_rpc, Just down_m20260712_address_dr_rpc),
("20260823_snd_files_entitlement", m20260823_snd_files_entitlement, Just down_m20260823_snd_files_entitlement),
("20260919_ratchet_verify_codes", m20260919_ratchet_verify_codes, Just down_m20260919_ratchet_verify_codes)
("20260919_ratchet_verify_codes", m20260919_ratchet_verify_codes, Just down_m20260919_ratchet_verify_codes),
("20260929_ratchet_indexes", m20260929_ratchet_indexes, Just down_m20260929_ratchet_indexes),
("20261008_rcv_switch_status", m20261008_rcv_switch_status, Just down_m20261008_rcv_switch_status)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,34 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260929_ratchet_indexes where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260929_ratchet_indexes :: Text
m20260929_ratchet_indexes =
[r|
DROP INDEX idx_skipped_messages_conn_id;
CREATE INDEX idx_skipped_messages_conn_id ON skipped_messages(conn_id, skipped_message_id);
DELETE FROM processed_ratchet_key_hashes AS h
WHERE EXISTS (
SELECT 1 FROM processed_ratchet_key_hashes d
WHERE d.conn_id = h.conn_id AND d.hash = h.hash AND d.processed_ratchet_key_hash_id < h.processed_ratchet_key_hash_id
);
DROP INDEX idx_processed_ratchet_key_hashes_hash;
CREATE UNIQUE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(conn_id, hash);
CREATE INDEX idx_processed_ratchet_key_hashes_conn_id ON processed_ratchet_key_hashes(conn_id, processed_ratchet_key_hash_id);
|]
down_m20260929_ratchet_indexes :: Text
down_m20260929_ratchet_indexes =
[r|
DROP INDEX idx_processed_ratchet_key_hashes_conn_id;
DROP INDEX idx_processed_ratchet_key_hashes_hash;
CREATE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(conn_id, hash);
DROP INDEX idx_skipped_messages_conn_id;
CREATE INDEX idx_skipped_messages_conn_id ON skipped_messages(conn_id);
|]
@@ -0,0 +1,20 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20261008_rcv_switch_status where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20261008_rcv_switch_status :: Text
m20261008_rcv_switch_status =
[r|
CREATE INDEX idx_rcv_queues_switch_status ON rcv_queues(switch_status);
|]
down_m20261008_rcv_switch_status :: Text
down_m20261008_rcv_switch_status =
[r|
UPDATE rcv_queues SET switch_status = NULL WHERE switch_status = 'received_qend';
DROP INDEX idx_rcv_queues_switch_status;
|]
@@ -1169,11 +1169,15 @@ CREATE INDEX idx_ntf_tokens_ntf_host_ntf_port ON smp_agent_test_protocol_schema.
CREATE INDEX idx_processed_ratchet_key_hashes_conn_id ON smp_agent_test_protocol_schema.processed_ratchet_key_hashes USING btree (conn_id, processed_ratchet_key_hash_id);
CREATE INDEX idx_processed_ratchet_key_hashes_created_at ON smp_agent_test_protocol_schema.processed_ratchet_key_hashes USING btree (created_at);
CREATE INDEX idx_processed_ratchet_key_hashes_hash ON smp_agent_test_protocol_schema.processed_ratchet_key_hashes USING btree (conn_id, hash);
CREATE UNIQUE INDEX idx_processed_ratchet_key_hashes_hash ON smp_agent_test_protocol_schema.processed_ratchet_key_hashes USING btree (conn_id, hash);
@@ -1229,6 +1233,10 @@ CREATE UNIQUE INDEX idx_rcv_queues_ntf ON smp_agent_test_protocol_schema.rcv_que
CREATE INDEX idx_rcv_queues_switch_status ON smp_agent_test_protocol_schema.rcv_queues USING btree (switch_status);
CREATE INDEX idx_rcv_queues_to_subscribe ON smp_agent_test_protocol_schema.rcv_queues USING btree (to_subscribe);
@@ -1241,7 +1249,7 @@ CREATE UNIQUE INDEX idx_server_certs_user_id_host_port ON smp_agent_test_protoco
CREATE INDEX idx_skipped_messages_conn_id ON smp_agent_test_protocol_schema.skipped_messages USING btree (conn_id);
CREATE INDEX idx_skipped_messages_conn_id ON smp_agent_test_protocol_schema.skipped_messages USING btree (conn_id, skipped_message_id);
@@ -100,7 +100,8 @@ withTransaction st = withTransactionPriority st False
{-# INLINE withTransaction #-}
withTransactionPriority :: DBStore -> Bool -> (DB.Connection -> IO a) -> IO a
withTransactionPriority st priority action = withConnectionPriority st priority $ dbBusyLoop . transaction
withTransactionPriority st@DBStore {dbClosed} priority action = withConnectionPriority st priority $ \db ->
ifM (readTVarIO dbClosed) (fail "withTransaction: database closed") (dbBusyLoop $ transaction db)
where
transaction db@DB.Connection {conn} = SQL.withImmediateTransaction conn $ action db
@@ -52,6 +52,9 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260411_service_certs
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260712_address_dr_rpc
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260823_snd_files_entitlement
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260919_ratchet_verify_codes
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260929_ratchet_indexes
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20261005_commands_autoincrement
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20261008_rcv_switch_status
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -103,7 +106,10 @@ schemaMigrations =
("m20260411_service_certs", m20260411_service_certs, Just down_m20260411_service_certs),
("m20260712_address_dr_rpc", m20260712_address_dr_rpc, Just down_m20260712_address_dr_rpc),
("m20260823_snd_files_entitlement", m20260823_snd_files_entitlement, Just down_m20260823_snd_files_entitlement),
("m20260919_ratchet_verify_codes", m20260919_ratchet_verify_codes, Just down_m20260919_ratchet_verify_codes)
("m20260919_ratchet_verify_codes", m20260919_ratchet_verify_codes, Just down_m20260919_ratchet_verify_codes),
("m20260929_ratchet_indexes", m20260929_ratchet_indexes, Just down_m20260929_ratchet_indexes),
("m20261005_commands_autoincrement", m20261005_commands_autoincrement, Just down_m20261005_commands_autoincrement),
("m20261008_rcv_switch_status", m20261008_rcv_switch_status, Just down_m20261008_rcv_switch_status)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,29 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260929_ratchet_indexes where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
-- idx_skipped_messages_conn_id is not changed: SQLite index entries include rowid (skipped_message_id)
m20260929_ratchet_indexes :: Query
m20260929_ratchet_indexes =
[sql|
DELETE FROM processed_ratchet_key_hashes AS h
WHERE EXISTS (
SELECT 1 FROM processed_ratchet_key_hashes d
WHERE d.conn_id = h.conn_id AND d.hash = h.hash AND d.processed_ratchet_key_hash_id < h.processed_ratchet_key_hash_id
);
DROP INDEX idx_processed_ratchet_key_hashes_hash;
CREATE UNIQUE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(conn_id, hash);
CREATE INDEX idx_processed_ratchet_key_hashes_conn_id ON processed_ratchet_key_hashes(conn_id, processed_ratchet_key_hash_id);
|]
down_m20260929_ratchet_indexes :: Query
down_m20260929_ratchet_indexes =
[sql|
DROP INDEX idx_processed_ratchet_key_hashes_conn_id;
DROP INDEX idx_processed_ratchet_key_hashes_hash;
CREATE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(conn_id, hash);
|]
@@ -0,0 +1,33 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20261005_commands_autoincrement where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20261005_commands_autoincrement :: Query
m20261005_commands_autoincrement =
[sql|
INSERT INTO sqlite_sequence (name, seq)
SELECT 'commands', MAX(ROWID) FROM commands;
PRAGMA writable_schema=1;
UPDATE sqlite_master SET sql = replace(sql, 'command_id INTEGER PRIMARY KEY,', 'command_id INTEGER PRIMARY KEY AUTOINCREMENT,')
WHERE name = 'commands' AND type = 'table';
PRAGMA writable_schema=RESET;
|]
down_m20261005_commands_autoincrement :: Query
down_m20261005_commands_autoincrement =
[sql|
DELETE FROM sqlite_sequence WHERE name = 'commands';
PRAGMA writable_schema=1;
UPDATE sqlite_master SET sql = replace(sql, 'command_id INTEGER PRIMARY KEY AUTOINCREMENT,', 'command_id INTEGER PRIMARY KEY,')
WHERE name = 'commands' AND type = 'table';
PRAGMA writable_schema=RESET;
|]
@@ -0,0 +1,19 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20261008_rcv_switch_status where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20261008_rcv_switch_status :: Query
m20261008_rcv_switch_status =
[sql|
CREATE INDEX idx_rcv_queues_switch_status ON rcv_queues(switch_status);
|]
down_m20261008_rcv_switch_status :: Query
down_m20261008_rcv_switch_status =
[sql|
UPDATE rcv_queues SET switch_status = NULL WHERE switch_status = 'received_qend';
DROP INDEX idx_rcv_queues_switch_status;
|]
@@ -247,7 +247,7 @@ CREATE TABLE ntf_subscriptions(
ON DELETE RESTRICT ON UPDATE CASCADE
) WITHOUT ROWID, STRICT;
CREATE TABLE commands(
command_id INTEGER PRIMARY KEY,
command_id INTEGER PRIMARY KEY AUTOINCREMENT,
conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
host TEXT,
port TEXT,
@@ -261,6 +261,7 @@ CREATE TABLE commands(
FOREIGN KEY(host, port) REFERENCES servers
ON DELETE RESTRICT ON UPDATE CASCADE
) STRICT;
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE snd_message_deliveries(
snd_message_delivery_id INTEGER PRIMARY KEY AUTOINCREMENT,
conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
@@ -269,7 +270,6 @@ CREATE TABLE snd_message_deliveries(
failed INTEGER DEFAULT 0,
FOREIGN KEY(conn_id, internal_id) REFERENCES messages ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
) STRICT;
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE users(
user_id INTEGER PRIMARY KEY AUTOINCREMENT
,
@@ -573,10 +573,6 @@ CREATE INDEX idx_encrypted_rcv_message_hashes_hash ON encrypted_rcv_message_hash
conn_id,
hash
);
CREATE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(
conn_id,
hash
);
CREATE INDEX idx_snd_messages_rcpt_internal_id ON snd_messages(
conn_id,
rcpt_internal_id
@@ -641,6 +637,15 @@ CREATE INDEX idx_connections_deleted ON connections(deleted);
CREATE INDEX idx_connections_service_request_expires_at ON connections(
service_request_expires_at
);
CREATE UNIQUE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(
conn_id,
hash
);
CREATE INDEX idx_processed_ratchet_key_hashes_conn_id ON processed_ratchet_key_hashes(
conn_id,
processed_ratchet_key_hash_id
);
CREATE INDEX idx_rcv_queues_switch_status ON rcv_queues(switch_status);
CREATE TRIGGER tr_rcv_queue_insert
AFTER INSERT ON rcv_queues
FOR EACH ROW
+14 -11
View File
@@ -67,6 +67,7 @@ module Simplex.Messaging.Client
enableSMPQueuesNtfs,
disableSMPQueuesNtfs,
sendSMPMessage,
QueueDrained,
ackSMPMessage,
suspendSMPQueue,
deleteSMPQueue,
@@ -1087,15 +1088,17 @@ resolvedNameOrNotFound d NameResponse {registration} = case registration of
NRRegistered {nameRecord} -> T.toLower (nrName nameRecord) == fullDomainName d
_ -> True
type QueueDrained = Bool
-- | Acknowledge message delivery (server deletes the message).
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
-- This command is always sent in background request mode
ackSMPMessage :: SMPClient -> RcvPrivateAuthKey -> QueueId -> MsgId -> ExceptT SMPClientError IO ()
ackSMPMessage :: SMPClient -> RcvPrivateAuthKey -> QueueId -> MsgId -> ExceptT SMPClientError IO QueueDrained
ackSMPMessage c rpKey rId msgId =
sendSMPCommand c NRMBackground (Just rpKey) rId (ACK msgId) >>= \case
OK -> return ()
cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd
OK -> pure True
cmd@MSG {} -> liftIO (writeSMPMessage c rId cmd) $> False
r -> throwE $ unexpectedResponse r
-- | Irreversibly suspend SMP queue.
@@ -1228,7 +1231,7 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c
-- encode
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd (sParty @p) command)
-- serviceAuth is False here – proxied commands are not used with service certificates
auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth False spKey nonce tForAuth
auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth False spKey (Just nonce) tForAuth
b <- case batchTransmissions serverThParams [Right (auth, tToSend)] of
[] -> throwE $ PCETransportError TELargeMsg
TBError e _ : _ -> throwE $ PCETransportError e
@@ -1262,7 +1265,7 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c
-- receives RRES :: Maybe C.CbNonce -> EncFwdResponse -> BrokerMsg
-- proxy should send PRES to the client with EncResponse
-- Always uses background timeout mode
forwardSMPTransmission :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO (Maybe C.CbNonce, EncResponse)
forwardSMPTransmission :: SMPClient -> C.CorrCbNonce -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO (Maybe C.CbNonce, EncResponse)
forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do
-- prepare params
sessSecret <- case thAuth thParams of
@@ -1400,7 +1403,7 @@ mkTransmission_ :: forall v err msg. Protocol v err msg => ProtocolClient v err
mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (entityId, pKey_, command) = do
nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entityId, command)
auth = authTransmission (thAuth thParams) (useServiceAuth command) pKey_ nonce tForAuth
auth = authTransmission (thAuth thParams) (useServiceAuth command) pKey_ (Just nonce) tForAuth
r <- mkRequest (CorrId corrId)
pure ((,tToSend) <$> auth, r)
where
@@ -1419,14 +1422,14 @@ mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentC
atomically $ TM.insert corrId r sentCommands
pure r
authTransmission :: Maybe (THandleAuth 'TClient) -> Bool -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TAuthorizations)
authTransmission thAuth serviceAuth pKey_ nonce t = traverse authenticate pKey_
authTransmission :: Maybe (THandleAuth 'TClient) -> Bool -> Maybe C.APrivateAuthKey -> Maybe C.CbNonce -> ByteString -> Either TransportError (Maybe TAuthorizations)
authTransmission thAuth serviceAuth pKey_ nonce_ t = traverse authenticate pKey_
where
authenticate :: C.APrivateAuthKey -> Either TransportError TAuthorizations
authenticate (C.APrivateAuthKey a pk) = (,serviceSig) <$> case a of
C.SX25519 -> case thAuth of
Just THAuthClient {peerServerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t'
Nothing -> Left TENoServerAuth
C.SX25519 -> case (thAuth, nonce_) of
(Just THAuthClient {peerServerPubKey = k}, Just nonce) -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t'
_ -> Left TENoServerAuth
C.SEd25519 -> sign pk
C.SEd448 -> sign pk
-- When command is signed by both entity key and service key,
+66 -20
View File
@@ -140,8 +140,10 @@ module Simplex.Messaging.Crypto
gcmIV,
-- * NaCl crypto_box
CbNonce (unCbNonce),
CbNonceI (..),
CbNonce,
pattern CbNonce,
CorrCbNonce,
cbEncrypt,
cbEncryptNoPad,
cbEncryptMaxLenBS,
@@ -152,8 +154,12 @@ module Simplex.Messaging.Crypto
sbEncryptNoPad,
sbDecryptNoPad,
cbNonce,
unsafeCbNonce,
corrCbNonce,
randomCbNonce,
reverseNonce,
xorNonce,
toCbNonce,
-- * NaCl crypto_secretbox
SbKey (unSbKey),
@@ -173,12 +179,14 @@ module Simplex.Messaging.Crypto
-- * pseudo-random bytes
randomBytes,
randomBytes',
-- * digests
sha256Hash,
sha512Hash,
sha3_256,
sha3_384,
keccak256,
md5Hash,
-- * Message padding / un-padding
@@ -218,7 +226,7 @@ import Crypto.Cipher.AES (AES256)
import qualified Crypto.Cipher.Types as AES
import qualified Crypto.Cipher.XSalsa as XSalsa
import qualified Crypto.Error as CE
import Crypto.Hash (Digest, MD5, SHA3_256, SHA3_384, SHA256 (..), SHA512 (..), hash, hashDigestSize)
import Crypto.Hash (Digest, Keccak_256, MD5, SHA3_256, SHA3_384, SHA256 (..), SHA512 (..), hash, hashDigestSize)
import qualified Crypto.KDF.HKDF as H
import qualified Crypto.MAC.Poly1305 as Poly1305
import qualified Crypto.PubKey.Curve25519 as X25519
@@ -232,7 +240,8 @@ import Data.ASN1.Types
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (bimap, first)
import Data.ByteArray (ByteArrayAccess)
import Data.Bits (xor)
import Data.ByteArray (ByteArray, ByteArrayAccess)
import qualified Data.ByteArray as BA
import Data.ByteString.Base64 (decode)
import qualified Data.ByteString.Base64.URL as U
@@ -254,7 +263,7 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (.
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll, parseString)
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Util (packZipWith, (<$?>))
-- | Cryptographic algorithms.
data Algorithm = Ed25519 | Ed448 | X25519 | X448
@@ -1042,6 +1051,11 @@ sha3_384 :: ByteString -> ByteString
sha3_384 = BA.convert . (hash :: ByteString -> Digest SHA3_384)
{-# INLINE sha3_384 #-}
-- | Keccak-256, the hash Ethereum uses. Not SHA3-256: they differ in the padding byte and produce entirely different digests.
keccak256 :: ByteString -> ByteString
keccak256 = BA.convert . (hash :: ByteString -> Digest Keccak_256)
{-# INLINE keccak256 #-}
md5Hash :: ByteString -> ByteString
md5Hash = BA.convert . (hash :: ByteString -> Digest MD5)
@@ -1324,8 +1338,8 @@ sbEncryptNoPad (SbKey key) (CbNonce nonce) = cryptoBox key nonce
{-# INLINE sbEncryptNoPad #-}
-- | NaCl @crypto_box@ encrypt with a shared DH secret and 192-bit nonce.
cbEncryptMaxLenBS :: KnownNat i => DhSecret X25519 -> CbNonce -> MaxLenBS i -> ByteString
cbEncryptMaxLenBS (DhSecretX25519 secret) (CbNonce nonce) = cryptoBox secret nonce . unMaxLenBS . padMaxLenBS
cbEncryptMaxLenBS :: (KnownNat i, CbNonceI n) => DhSecret X25519 -> n -> MaxLenBS i -> ByteString
cbEncryptMaxLenBS (DhSecretX25519 secret) nonce = cryptoBox secret (unCbNonce nonce) . unMaxLenBS . padMaxLenBS
{-# INLINE cbEncryptMaxLenBS #-}
cryptoBox :: ByteArrayAccess key => key -> ByteString -> ByteString -> ByteString
@@ -1335,8 +1349,8 @@ cryptoBox secret nonce s = BA.convert tag <> c
tag = Poly1305.auth rs c
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce.
cbDecrypt :: DhSecret X25519 -> CbNonce -> ByteString -> Either CryptoError ByteString
cbDecrypt (DhSecretX25519 secret) = sbDecrypt_ secret
cbDecrypt :: CbNonceI n => DhSecret X25519 -> n -> ByteString -> Either CryptoError ByteString
cbDecrypt (DhSecretX25519 secret) = sbDecrypt_ secret . toCbNonce
{-# INLINE cbDecrypt #-}
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce (without unpadding).
@@ -1383,9 +1397,16 @@ cbAuthenticate k pk nonce msg = CbAuthenticator $ cbEncryptNoPad (dh' k pk) nonc
cbVerify :: PublicKeyX25519 -> PrivateKeyX25519 -> CbNonce -> CbAuthenticator -> ByteString -> Bool
cbVerify k pk nonce (CbAuthenticator s) authorized = cbDecryptNoPad (dh' k pk) nonce s == Right (sha512Hash authorized)
newtype CbNonce = CryptoBoxNonce {unCbNonce :: ByteString}
class CbNonceI n where
unCbNonce :: n -> ByteString
newtype CbNonce = CryptoBoxNonce ByteString
deriving (Eq, Show)
deriving newtype (FromField)
instance CbNonceI CbNonce where
unCbNonce (CryptoBoxNonce s) = s
instance FromField CbNonce where fromField = blobFieldDecoder cbNonce
instance ToField CbNonce where toField (CryptoBoxNonce s) = toField $ Binary s
@@ -1396,7 +1417,7 @@ pattern CbNonce s <- CryptoBoxNonce s
instance StrEncoding CbNonce where
strEncode (CbNonce s) = strEncode s
strP = cbNonce <$> strP
strP = cbNonce <$?> strP
instance ToJSON CbNonce where
toJSON = strToJSON
@@ -1405,27 +1426,52 @@ instance ToJSON CbNonce where
instance FromJSON CbNonce where
parseJSON = strParseJSON "CbNonce"
cbNonce :: ByteString -> CbNonce
cbNonce :: ByteString -> Either String CbNonce
cbNonce s
| len == 24 = CryptoBoxNonce s
| len > 24 = CryptoBoxNonce . fst $ B.splitAt 24 s
| otherwise = CryptoBoxNonce $ s <> B.replicate (24 - len) (toEnum 0)
where
len = B.length s
| B.length s == 24 = Right $ CryptoBoxNonce s
| otherwise = Left "CbNonce: invalid length"
unsafeCbNonce :: ByteString -> CbNonce
unsafeCbNonce s = either error id $ cbNonce s
corrCbNonce :: ByteString -> Either String CorrCbNonce
corrCbNonce s = CorrCbNonce . unCbNonce <$> cbNonce s
randomCbNonce :: TVar ChaChaDRG -> STM CbNonce
randomCbNonce = fmap CryptoBoxNonce . randomBytes 24
randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString
randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n
randomBytes = randomBytes'
{-# INLINE randomBytes #-}
reverseNonce :: CbNonce -> CbNonce
reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s)
randomBytes' :: ByteArray a => Int -> TVar ChaChaDRG -> STM a
randomBytes' n gVar = stateTVar gVar $ randomBytesGenerate n
{-# INLINE randomBytes' #-}
reverseNonce :: CbNonceI n => n -> CbNonce
reverseNonce = CryptoBoxNonce . B.reverse . unCbNonce
xorNonce :: CbNonceI n => ByteString -> n -> CbNonce
xorNonce s nonce = CryptoBoxNonce $ packZipWith xor s n <> B.drop (B.length s) n
where
n = unCbNonce nonce
toCbNonce :: CbNonceI n => n -> CbNonce
toCbNonce = CryptoBoxNonce . unCbNonce
instance Encoding CbNonce where
smpEncode = unCbNonce
smpP = CryptoBoxNonce <$> A.take 24
newtype CorrCbNonce = CorrCbNonce ByteString
instance CbNonceI CorrCbNonce where
unCbNonce (CorrCbNonce s) = s
instance Encoding CorrCbNonce where
smpEncode = smpEncode . unCbNonce
smpP = corrCbNonce <$?> smpP
newtype SbKey = SecretBoxKey {unSbKey :: ByteString}
deriving (Eq, Show)
deriving newtype (FromField)
+101
View File
@@ -0,0 +1,101 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
-- | BIP-32 HD derivation over secp256k1, private only: we hold the seed, so CKDpub, xpub and fingerprints are not implemented. An invalid master or child key is recomputed as SLIP-0010 specifies, up to two times.
module Simplex.Messaging.Crypto.BIP32
( ExtendedKey,
xkKey,
xkChainCode,
masterKey,
derivePath,
derivePath_,
renderPath,
hardened,
isHardened,
)
where
import Control.Concurrent.STM (TVar)
import Control.Monad (foldM)
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
import qualified Crypto.Hash as H
import qualified Crypto.MAC.HMAC as HMAC
import Crypto.Random (ChaChaDRG)
import Data.Bits ((.|.))
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BC
import Data.List (intercalate)
import Data.Word (Word32)
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
import Simplex.Messaging.Encoding (smpEncode)
data ExtendedKey = ExtendedKey
{ xkKey :: S.Secp256k1PrivateKey,
xkChainCode :: ScrubbedBytes
}
deriving (Eq)
hardenedOffset :: Word32
hardenedOffset = 0x80000000
hardened :: Word32 -> Word32
hardened = (.|. hardenedOffset)
isHardened :: Word32 -> Bool
isHardened i = i >= hardenedOffset
masterKey :: ScrubbedBytes -> Either String ExtendedKey
masterKey seed
| seedLen < 16 || seedLen > 64 = Left $ "seed: expected 16 to 64 bytes, got " <> show seedLen
| otherwise = go attempts seed
where
seedLen = BA.length seed
go :: Int -> ScrubbedBytes -> Either String ExtendedKey
go 0 _ = Left derivationFailed
go n s = case S.mkPrivateKey il of
Right k -> Right ExtendedKey {xkKey = k, xkChainCode = ir}
Left _ -> go (n - 1) i
where
i = hmacSHA512 "Bitcoin seed" s
(il, ir) = BA.splitAt 32 i
deriveChild :: S.Secp256k1Context -> ExtendedKey -> Word32 -> IO (Either String ExtendedKey)
deriveChild ctx ExtendedKey {xkKey, xkChainCode} i = do
dat <-
if isHardened i
then pure $ BA.cons 0 (S.unPrivateKey xkKey)
else BA.convert <$> (S.serializePublicKey S.Compressed =<< S.secp256k1PublicKey_ ctx xkKey)
go attempts dat
where
go :: Int -> ScrubbedBytes -> IO (Either String ExtendedKey)
go 0 _ = pure $ Left derivationFailed
go n dat = do
let (il, ir) = BA.splitAt 32 $ hmacSHA512 xkChainCode (dat <> BA.convert (smpEncode i))
S.privateKeyTweakAdd xkKey il >>= \case
Just k -> pure $ Right ExtendedKey {xkKey = k, xkChainCode = ir}
Nothing -> go (n - 1) $ BA.cons 1 ir
derivePath :: TVar ChaChaDRG -> ExtendedKey -> [Word32] -> IO (Either String ExtendedKey)
derivePath g xk path = S.withContext g $ \ctx -> derivePath_ ctx xk path
derivePath_ :: S.Secp256k1Context -> ExtendedKey -> [Word32] -> IO (Either String ExtendedKey)
derivePath_ ctx xk = runExceptT . foldM (\k -> ExceptT . deriveChild ctx k) xk
attempts :: Int
attempts = 3
derivationFailed :: String
derivationFailed = "derivation failed after " <> show attempts <> " attempts"
renderPath :: [Word32] -> ByteString
renderPath is = BC.pack $ intercalate "/" ("m" : map component is)
where
component i
| isHardened i = show (i - hardenedOffset) <> "'"
| otherwise = show i
hmacSHA512 :: ScrubbedBytes -> ScrubbedBytes -> ScrubbedBytes
hmacSHA512 key msg = BA.convert (HMAC.hmac key msg :: HMAC.HMAC H.SHA512)
+149
View File
@@ -0,0 +1,149 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | BIP-39 mnemonics, English only: every word is ASCII, any Unicode space character separates words, and full NFKD normalization is not applied; a passphrase is bytes the caller normalizes.
module Simplex.Messaging.Crypto.BIP39
( WalletEntropy,
unEntropy,
EntropyStrength (..),
mkEntropy,
randomEntropy,
parsePhrase,
entropyPhrase,
entropySeed,
)
where
import Control.Concurrent.STM
import Crypto.Hash (Digest, SHA256, SHA512 (..), hash)
import qualified Crypto.KDF.PBKDF2 as PBKDF2
import Crypto.Number.Serialize (i2ospOf_, os2ip)
import Crypto.Random (ChaChaDRG)
import qualified Data.Attoparsec.Text as A
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BC
import Data.Char (isSpace)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM
import Data.List (foldl')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..), blobFieldDecoder)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.BIP39.English (englishWordList)
-- | 16, 20, 24, 28 or 32 bytes of entropy: a valid BIP-39 phrase in another encoding.
newtype WalletEntropy = WalletEntropy ScrubbedBytes
deriving (Eq, Show)
unEntropy :: WalletEntropy -> ScrubbedBytes
unEntropy (WalletEntropy ent) = ent
data EntropyStrength = ES128 | ES160 | ES192 | ES224 | ES256
deriving (Eq, Show, Bounded, Enum)
newtype Mnemonic = Mnemonic [Int]
deriving (Eq)
strengthBytes :: EntropyStrength -> Int
strengthBytes = \case
ES128 -> 16
ES160 -> 20
ES192 -> 24
ES224 -> 28
ES256 -> 32
wordCount :: Int -> Int
wordCount entBytes = (entBits + entBits `div` 32) `div` 11
where
entBits = entBytes * 8
seedSize :: Int
seedSize = 64
validEntropySizes :: [Int]
validEntropySizes = map strengthBytes [minBound .. maxBound]
validWordCounts :: [Int]
validWordCounts = map wordCount validEntropySizes
wordByIndex :: IntMap ByteString
wordByIndex = IM.fromList $ zip [0 ..] englishWordList
indexByWord :: Map ByteString Int
indexByWord = M.fromList $ zip englishWordList [0 ..]
mkEntropy :: ScrubbedBytes -> Either String WalletEntropy
mkEntropy ent
| entLen `elem` validEntropySizes = Right $ WalletEntropy ent
| otherwise = Left $ "entropy: expected 16, 20, 24, 28 or 32 bytes, got " <> show entLen
where
entLen = BA.length ent
instance ToField WalletEntropy where toField (WalletEntropy ent) = toField $ Binary (BA.convert ent :: ByteString)
instance FromField WalletEntropy where fromField = blobFieldDecoder $ mkEntropy . BA.convert
randomEntropy :: EntropyStrength -> TVar ChaChaDRG -> STM WalletEntropy
randomEntropy s = fmap WalletEntropy . C.randomBytes' (strengthBytes s)
-- | The words joined by single spaces, the PBKDF2 password BIP-39 specifies.
entropyPhrase :: WalletEntropy -> ByteString
entropyPhrase = BC.unwords . mnemonicWords . entropyToMnemonic
-- | Indexes are in @[0, 2047]@: they are masked to 11 bits or looked up in the wordlist.
mnemonicWords :: Mnemonic -> [ByteString]
mnemonicWords (Mnemonic idxs) = map (wordByIndex IM.!) idxs
entropyToMnemonic :: WalletEntropy -> Mnemonic
entropyToMnemonic (WalletEntropy ent) =
Mnemonic [fromIntegral ((combined `shiftR` (11 * (n - 1 - i))) .&. 0x7FF) | i <- [0 .. n - 1]]
where
entBits = BA.length ent * 8
csBits = entBits `div` 32
-- csBits is at most 8 (256/32), so the first checksum byte always suffices.
csByte = BA.index (hash ent :: Digest SHA256) 0
combined = os2ip ent `shiftL` csBits .|. fromIntegral (csByte `shiftR` (8 - csBits))
n = (entBits + csBits) `div` 11
mnemonicToEntropy :: Mnemonic -> WalletEntropy
mnemonicToEntropy (Mnemonic idxs) = WalletEntropy $ i2ospOf_ (entBits `div` 8) (combined `shiftR` csBits)
where
totalBits = length idxs * 11
entBits = totalBits * 32 `div` 33
csBits = totalBits - entBits
combined = foldl' (\acc i -> acc `shiftL` 11 .|. fromIntegral i) (0 :: Integer) idxs
parsePhrase :: Text -> Either String WalletEntropy
parsePhrase = A.parseOnly (phraseP <* A.endOfInput)
phraseP :: A.Parser WalletEntropy
phraseP = do
ws <- A.skipSpace *> (wordP `A.sepBy'` A.takeWhile1 isSpace) <* A.skipSpace
let n = length ws
if n `notElem` validWordCounts
then fail $ "mnemonic: expected 12, 15, 18, 21 or 24 words, got " <> show n
else do
m <- Mnemonic <$> traverse lookupWord ws
let ent = mnemonicToEntropy m
-- recomputing the checksum bits from the decoded entropy rejects wrong checksum bits
if entropyToMnemonic ent == m
then pure ent
else fail "mnemonic: checksum mismatch"
where
wordP = T.toLower <$> A.takeWhile1 (not . isSpace)
lookupWord w = maybe (fail "mnemonic: not in wordlist") pure $ M.lookup (encodeUtf8 w) indexByWord
entropySeed :: WalletEntropy -> ByteString -> ScrubbedBytes
entropySeed ent passphrase =
PBKDF2.generate
(PBKDF2.prfHMAC SHA512)
PBKDF2.Parameters {PBKDF2.iterCounts = 2048, PBKDF2.outputLength = seedSize}
(entropyPhrase ent)
("mnemonic" <> passphrase)
@@ -0,0 +1,11 @@
{-# LANGUAGE TemplateHaskell #-}
-- | The BIP-39 English wordlist, embedded verbatim from <https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt>, pinned by a checksum test.
module Simplex.Messaging.Crypto.BIP39.English (englishWordList) where
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as BC
import Data.FileEmbed (embedFile)
englishWordList :: [ByteString]
englishWordList = BC.words $(embedFile "src/Simplex/Messaging/Crypto/BIP39/english.txt")
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
-- | BIP-44 paths, @m/44'/coin'/account'/0/0@: one account per key, at the hardened account level.
module Simplex.Messaging.Crypto.BIP44
( CoinType (..),
AccountIndex,
mkAccountIndex,
unAccountIndex,
bip44Path,
)
where
import Data.Aeson (FromJSON (..), ToJSON)
import Data.Word (Word32)
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..))
import Simplex.Messaging.Crypto.BIP32 (hardened, isHardened)
import Simplex.Messaging.Util ((<$?>))
#if defined(dbPostgres)
import Database.PostgreSQL.Simple.FromField (ResultError (..), returnError)
#else
import Database.SQLite.Simple (ResultError (..))
import Database.SQLite.Simple.FromField (returnError)
#endif
-- | SLIP-44 coin types.
data CoinType = Ethereum
deriving (Eq, Show)
coinIndex :: CoinType -> Word32
coinIndex = \case
Ethereum -> 60
-- | Below 2^31, so it can be hardened without colliding with another index.
newtype AccountIndex = AccountIndex Word32
deriving (Eq, Ord, Show)
deriving newtype (ToJSON, ToField)
unAccountIndex :: AccountIndex -> Word32
unAccountIndex (AccountIndex i) = i
mkAccountIndex :: Word32 -> Either String AccountIndex
mkAccountIndex i
| isHardened i = Left "account index at or above 2^31"
| otherwise = Right $ AccountIndex i
instance FromJSON AccountIndex where
parseJSON v = mkAccountIndex <$?> parseJSON v
#if defined(dbPostgres)
instance FromField AccountIndex where
fromField f dat = fromField f dat >>= either (returnError ConversionFailed f) pure . mkAccountIndex
#else
instance FromField AccountIndex where
fromField f = fromField f >>= either (returnError ConversionFailed f) pure . mkAccountIndex
#endif
bip44Path :: CoinType -> AccountIndex -> [Word32]
bip44Path coin (AccountIndex account) = [hardened 44, hardened (coinIndex coin), hardened account, 0, 0]
+4
View File
@@ -72,6 +72,7 @@ module Simplex.Messaging.Crypto.Ratchet
rcEncryptHeader,
rcEncryptMsg,
rcDecrypt,
maxSkippedMsgKeys,
-- used in tests
MsgHeader (..),
RatchetInitParams (..),
@@ -962,6 +963,9 @@ type DecryptResult a = (Either CryptoError ByteString, Ratchet a, SkippedMsgDiff
maxSkip :: Word32
maxSkip = 512
maxSkippedMsgKeys :: Int
maxSkippedMsgKeys = 2000
rcDecrypt ::
forall a.
(AlgorithmI a, DhAlgorithm a) =>
+195
View File
@@ -0,0 +1,195 @@
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | FFI bindings to libsecp256k1.
module Simplex.Messaging.Crypto.Secp256k1
( Secp256k1PrivateKey,
Secp256k1PublicKey,
Secp256k1Context,
PubKeyFormat (..),
RecoverableSignature (..),
mkPrivateKey,
unPrivateKey,
withContext,
secp256k1PublicKey,
secp256k1PublicKey_,
serializePublicKey,
privateKeyTweakAdd,
signRecoverable,
)
where
import Control.Concurrent.STM
import Control.Exception (bracket, throwIO)
import Control.Monad (void, when)
import Crypto.Number.Serialize (i2ospOf_)
import qualified Crypto.PubKey.ECC.Types as ECT
import Crypto.Random (ChaChaDRG)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Foreign hiding (void)
import Foreign.C
import qualified Simplex.Messaging.Crypto as C
import System.IO.Unsafe (unsafePerformIO)
-- Sizes
privateKeySize :: Int
privateKeySize = 32
compressedSize :: Int
compressedSize = 33
uncompressedSize :: Int
uncompressedSize = 65
-- | Internal size of @secp256k1_pubkey@ (opaque, not a serialization).
pubKeyInternalSize :: Int
pubKeyInternalSize = 64
-- | Internal size of @secp256k1_ecdsa_recoverable_signature@.
recSigInternalSize :: Int
recSigInternalSize = 65
compactSigSize :: Int
compactSigSize = 64
digestSize :: Int
digestSize = 32
-- Types
newtype Secp256k1PrivateKey = Secp256k1PrivateKey ScrubbedBytes
deriving (Eq)
-- | A public key in libsecp256k1's opaque 64-byte form; 'serializePublicKey' returns the SEC1 bytes.
newtype Secp256k1PublicKey = Secp256k1PublicKey ByteString
newtype Secp256k1Context = Secp256k1Context (Ptr Ctx)
data PubKeyFormat = Compressed | Uncompressed
deriving (Eq, Show)
-- | @r@ and @s@, 32 bytes big-endian each, and the recovery id, EIP-1559's @yParity@.
data RecoverableSignature = RecoverableSignature
{ rsR :: ByteString,
rsS :: ByteString,
rsRecId :: Word8
}
deriving (Eq, Show)
-- FFI
data Ctx
data PubKeyRaw
data RecSigRaw
foreign import ccall "secp256k1_context_create"
c_context_create :: CUInt -> IO (Ptr Ctx)
foreign import ccall "secp256k1_context_destroy"
c_context_destroy :: Ptr Ctx -> IO ()
foreign import ccall "secp256k1_context_randomize"
c_context_randomize :: Ptr Ctx -> Ptr Word8 -> IO CInt
foreign import ccall "&secp256k1_context_static"
c_context_static :: Ptr (Ptr Ctx)
foreign import ccall "secp256k1_ec_pubkey_create"
c_ec_pubkey_create :: Ptr Ctx -> Ptr PubKeyRaw -> Ptr Word8 -> IO CInt
foreign import ccall "secp256k1_ec_pubkey_serialize"
c_ec_pubkey_serialize :: Ptr Ctx -> Ptr Word8 -> Ptr CSize -> Ptr PubKeyRaw -> CUInt -> IO CInt
foreign import ccall "secp256k1_ec_seckey_tweak_add"
c_ec_seckey_tweak_add :: Ptr Ctx -> Ptr Word8 -> Ptr Word8 -> IO CInt
foreign import ccall "secp256k1_ecdsa_sign_recoverable"
c_ecdsa_sign_recoverable :: Ptr Ctx -> Ptr RecSigRaw -> Ptr Word8 -> Ptr Word8 -> Ptr () -> Ptr () -> IO CInt
foreign import ccall "secp256k1_ecdsa_recoverable_signature_serialize_compact"
c_ecdsa_recoverable_signature_serialize_compact :: Ptr Ctx -> Ptr Word8 -> Ptr CInt -> Ptr RecSigRaw -> IO CInt
-- SECP256K1_CONTEXT_NONE = SECP256K1_FLAGS_TYPE_CONTEXT
contextNone :: CUInt
contextNone = 1
staticContext :: Ptr Ctx
staticContext = unsafePerformIO $ peek c_context_static
{-# NOINLINE staticContext #-}
withContext :: TVar ChaChaDRG -> (Secp256k1Context -> IO a) -> IO a
withContext g f = bracket (c_context_create contextNone) c_context_destroy $ \ctx -> do
seed :: ScrubbedBytes <- atomically $ C.randomBytes' 32 g
rc <- BA.withByteArray seed $ c_context_randomize ctx
when (rc /= 1) $ throwIO (userError "secp256k1_context_randomize failed")
f $ Secp256k1Context ctx
-- Public API
-- | Reject zero and anything at or above the group order, which is what makes 'secp256k1PublicKey' total.
mkPrivateKey :: ScrubbedBytes -> Either String Secp256k1PrivateKey
mkPrivateKey bs
| BA.length bs /= privateKeySize = Left $ "private key: expected 32 bytes, got " <> show (BA.length bs)
| BA.all (== 0) bs || bs >= groupOrder = Left "private key: not in [1, n-1]"
| otherwise = Right $ Secp256k1PrivateKey bs
-- | Compared as bytes: both are 32 bytes big-endian, and 'ScrubbedBytes' compares lexicographically.
groupOrder :: ScrubbedBytes
groupOrder = i2ospOf_ privateKeySize $ ECT.ecc_n $ ECT.common_curve $ ECT.getCurveByName ECT.SEC_p256k1
unPrivateKey :: Secp256k1PrivateKey -> ScrubbedBytes
unPrivateKey (Secp256k1PrivateKey bs) = bs
secp256k1PublicKey :: TVar ChaChaDRG -> Secp256k1PrivateKey -> IO Secp256k1PublicKey
secp256k1PublicKey g k = withContext g (`secp256k1PublicKey_` k)
secp256k1PublicKey_ :: Secp256k1Context -> Secp256k1PrivateKey -> IO Secp256k1PublicKey
secp256k1PublicKey_ (Secp256k1Context ctx) (Secp256k1PrivateKey sk) = do
(rc, pk) <- BA.allocRet pubKeyInternalSize $ \pkPtr -> BA.withByteArray sk $ c_ec_pubkey_create ctx pkPtr
when (rc /= 1) $ throwIO (userError "secp256k1_ec_pubkey_create failed on a validated key")
pure $ Secp256k1PublicKey pk
serializePublicKey :: PubKeyFormat -> Secp256k1PublicKey -> IO ByteString
serializePublicKey fmt (Secp256k1PublicKey pk) =
BA.alloc outLen $ \outPtr ->
with (fromIntegral outLen) $ \lenPtr ->
BA.withByteArray pk $ \pkPtr ->
void $ c_ec_pubkey_serialize staticContext outPtr lenPtr pkPtr flag
where
-- SECP256K1_EC_COMPRESSED = SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION, SECP256K1_EC_UNCOMPRESSED = SECP256K1_FLAGS_TYPE_COMPRESSION
(flag, outLen) = case fmt of
Compressed -> (2 .|. 256, compressedSize)
Uncompressed -> (2, uncompressedSize)
-- | @sk + tweak mod n@, as BIP-32 child derivation requires. 'Nothing' when the tweak is not 32 bytes or not below n, or when the result is zero.
privateKeyTweakAdd :: Secp256k1PrivateKey -> ScrubbedBytes -> IO (Maybe Secp256k1PrivateKey)
privateKeyTweakAdd (Secp256k1PrivateKey sk) tweak
| BA.length tweak /= privateKeySize = pure Nothing
| otherwise =
BA.withByteArray tweak $ \twPtr -> do
(rc, sk') <- BA.copyRet sk $ \skPtr -> c_ec_seckey_tweak_add staticContext skPtr twPtr
pure $ if rc == 1 then Just (Secp256k1PrivateKey sk') else Nothing
-- | Signs a 32-byte digest, deterministically (RFC 6979) and always low-@s@, as EIP-2 requires.
signRecoverable :: TVar ChaChaDRG -> Secp256k1PrivateKey -> ByteString -> IO (Either String RecoverableSignature)
signRecoverable g (Secp256k1PrivateKey sk) digest
| B.length digest /= digestSize = pure $ Left $ "digest: expected 32 bytes, got " <> show (B.length digest)
| otherwise = withContext g $ \(Secp256k1Context ctx) ->
allocaBytes recSigInternalSize $ \sigPtr -> do
rc <- BA.withByteArray digest $ \msgPtr -> BA.withByteArray sk $ \skPtr -> c_ecdsa_sign_recoverable ctx sigPtr msgPtr skPtr nullPtr nullPtr
if rc /= 1
then pure $ Left "secp256k1_ecdsa_sign_recoverable failed on a validated key"
else do
(recId, compact) <- BA.allocRet compactSigSize $ \outPtr ->
alloca $ \recIdPtr -> do
void $ c_ecdsa_recoverable_signature_serialize_compact ctx outPtr recIdPtr sigPtr
peek recIdPtr
let (rsR, rsS) = B.splitAt (compactSigSize `div` 2) compact
pure $ Right RecoverableSignature {rsR, rsS, rsRecId = fromIntegral recId}
+5 -3
View File
@@ -17,6 +17,7 @@ module Simplex.Messaging.Crypto.ShortLink
encodeSignUserData,
newOwnerAuth,
encryptLinkData,
encryptFixedData,
encryptUserData,
decryptLinkData,
)
@@ -84,9 +85,10 @@ newOwnerAuth g ownerId signingKey = do
pure (ownerPrivKey, OwnerAuth {ownerId, ownerKey, authOwnerSig})
encryptLinkData :: TVar ChaChaDRG -> C.SbKey -> (ByteString, ByteString) -> ExceptT AgentErrorType IO QueueLinkData
encryptLinkData g k = bimapM (encrypt fixedDataPaddedLength) (encrypt userDataPaddedLength)
where
encrypt len = encryptData g k len
encryptLinkData g k = bimapM (encryptFixedData g k) (encryptUserData g k)
encryptFixedData :: TVar ChaChaDRG -> C.SbKey -> ByteString -> ExceptT AgentErrorType IO EncDataBytes
encryptFixedData g k s = encryptData g k fixedDataPaddedLength s
encryptUserData :: TVar ChaChaDRG -> C.SbKey -> ByteString -> ExceptT AgentErrorType IO EncDataBytes
encryptUserData g k s = encryptData g k userDataPaddedLength s
+78
View File
@@ -0,0 +1,78 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Eth.Address
( Address,
unAddress,
addressFromPublicKey,
addressFromPrivateKey,
deriveAddress,
)
where
import Control.Applicative (optional)
import Control.Concurrent.STM (TVar)
import Control.Monad (unless, when, (<=<))
import Crypto.Random (ChaChaDRG)
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bits (shiftR, (.&.))
import qualified Data.ByteArray.Encoding as BAE
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import Data.Char (isHexDigit, isLower, isUpper, toUpper)
import Data.Word (Word32)
import Simplex.Messaging.Crypto (keccak256)
import Simplex.Messaging.Crypto.BIP32 (ExtendedKey, derivePath_, xkKey)
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
import Simplex.Messaging.Encoding.String
newtype Address = Address ByteString
deriving (Eq, Show)
deriving (ToJSON, FromJSON) via (StrJSON "Address" Address)
instance StrEncoding Address where
strEncode = checksumAddress
strP = do
_ <- optional $ A.string "0x"
body <- A.takeWhile isHexDigit
unless (B.length body == addressSize * 2) $ fail $ "address: expected 40 hex digits, got " <> show (B.length body)
a <- Address <$> either fail pure (BAE.convertFromBase BAE.Base16 body)
when (mixedCase body && checksumAddress a /= "0x" <> body) $ fail "address: EIP-55 checksum mismatch"
pure a
where
mixedCase s = BC.any isUpper s && BC.any isLower s
addressSize :: Int
addressSize = 20
unAddress :: Address -> ByteString
unAddress (Address bs) = bs
-- | The last 20 bytes of @keccak256@ of the uncompressed public key with its @0x04@ SEC1 prefix removed.
addressFromPublicKey :: S.Secp256k1PublicKey -> IO Address
addressFromPublicKey pk = Address . B.drop 12 . keccak256 . B.drop 1 <$> S.serializePublicKey S.Uncompressed pk
addressFromPrivateKey :: TVar ChaChaDRG -> S.Secp256k1PrivateKey -> IO Address
addressFromPrivateKey g k = S.withContext g (`addressFromPrivateKey_` k)
addressFromPrivateKey_ :: S.Secp256k1Context -> S.Secp256k1PrivateKey -> IO Address
addressFromPrivateKey_ ctx = addressFromPublicKey <=< S.secp256k1PublicKey_ ctx
deriveAddress :: TVar ChaChaDRG -> ExtendedKey -> [Word32] -> IO (Either String (ExtendedKey, Address))
deriveAddress g xk path = S.withContext g $ \ctx ->
derivePath_ ctx xk path >>= mapM (\k -> (k,) <$> addressFromPrivateKey_ ctx (xkKey k))
-- | EIP-55: @0x@ and 40 hex digits whose case encodes a checksum over the lowercase hex form.
checksumAddress :: Address -> ByteString
checksumAddress (Address bs) = "0x" <> BC.pack (zipWith adjust [0 ..] (BC.unpack lowerHex))
where
lowerHex = BAE.convertToBase BAE.Base16 bs
hashed = keccak256 lowerHex
adjust i c = if isLower c && nibbleAt i >= 8 then toUpper c else c
nibbleAt i =
let byte = B.index hashed (i `div` 2)
in if even i then byte `shiftR` 4 else byte .&. 0x0F
+34
View File
@@ -0,0 +1,34 @@
{-# LANGUAGE LambdaCase #-}
-- | Recursive Length Prefix, the encoding of Ethereum transactions.
module Simplex.Messaging.Eth.RLP
( RLPItem (..),
rlpEncode,
scalarItem,
)
where
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Word (Word32)
import Simplex.Messaging.Encoding (smpEncode)
data RLPItem = RLPBytes ByteString | RLPList [RLPItem]
rlpEncode :: RLPItem -> ByteString
rlpEncode = \case
RLPBytes b
| B.length b == 1 && B.head b < 0x80 -> b
| otherwise -> lengthPrefixed 0x80 b
RLPList items -> lengthPrefixed 0xc0 $ B.concat $ map rlpEncode items
where
lengthPrefixed offset payload
| len < 56 = B.cons (offset + fromIntegral len) payload
| otherwise = B.cons (offset + 55 + fromIntegral (B.length lenBytes)) lenBytes <> payload
where
len = B.length payload
lenBytes = B.dropWhile (== 0) $ smpEncode (fromIntegral len :: Word32)
-- | Big-endian with no leading zeros, and zero as the empty string.
scalarItem :: ByteString -> RLPItem
scalarItem = RLPBytes . B.dropWhile (== 0)
+58
View File
@@ -0,0 +1,58 @@
{-# LANGUAGE NamedFieldPuns #-}
-- | EIP-1559 (type 2) transactions with an empty access list, signed for @eth_sendRawTransaction@.
module Simplex.Messaging.Eth.Transaction
( Eip1559Tx (..),
signEip1559Tx,
)
where
import Control.Concurrent.STM (TVar)
import Crypto.Random (ChaChaDRG)
import Data.Bits (shiftR)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.WideWord.Word128 (Word128 (..))
import Data.WideWord.Word256 (Word256 (..))
import Data.Word (Word32, Word64)
import Simplex.Messaging.Crypto (keccak256)
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature (..), Secp256k1PrivateKey, signRecoverable)
import Simplex.Messaging.Encoding (smpEncode)
import Simplex.Messaging.Eth.Address (Address, unAddress)
import Simplex.Messaging.Eth.RLP
import Simplex.Messaging.Util ((<$$>))
-- | Each width is the narrowest that both geth and reth accept.
data Eip1559Tx = Eip1559Tx
{ txChainId :: Word64,
txNonce :: Word64,
txMaxPriorityFeePerGas :: Word128,
txMaxFeePerGas :: Word128,
txGasLimit :: Word64,
txTo :: Address,
txValue :: Word256,
txData :: ByteString
}
-- | @0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, yParity, r, s])@, signing @keccak256@ of the same without the last three.
signEip1559Tx :: TVar ChaChaDRG -> Secp256k1PrivateKey -> Eip1559Tx -> IO (Either String ByteString)
signEip1559Tx g sk Eip1559Tx {txChainId, txNonce, txMaxPriorityFeePerGas, txMaxFeePerGas, txGasLimit, txTo, txValue, txData} =
signed <$$> signRecoverable g sk (keccak256 $ typed fields)
where
signed RecoverableSignature {rsR, rsS, rsRecId} = typed $ fields <> [scalarItem (B.singleton rsRecId), scalarItem rsR, scalarItem rsS]
fields =
[ w64 txChainId,
w64 txNonce,
w128 txMaxPriorityFeePerGas,
w128 txMaxFeePerGas,
w64 txGasLimit,
RLPBytes $ unAddress txTo,
w256 txValue,
RLPBytes txData,
RLPList []
]
w64 = scalarItem . be64
w128 (Word128 hi lo) = scalarItem $ be64 hi <> be64 lo
w256 (Word256 a3 a2 a1 a0) = scalarItem $ be64 a3 <> be64 a2 <> be64 a1 <> be64 a0
be64 n = smpEncode (fromIntegral (n `shiftR` 32) :: Word32) <> smpEncode (fromIntegral n :: Word32)
typed items = B.cons 0x02 $ rlpEncode $ RLPList items
@@ -1,5 +1,6 @@
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
@@ -312,3 +313,4 @@ ALTER TABLE ONLY ntf_server.subscriptions
+66 -38
View File
@@ -145,7 +145,10 @@ module Simplex.Messaging.Protocol
RcvNtfDhSecret,
Message (..),
RcvMessage (..),
MsgId,
MsgId (unMsgId),
mkMsgId,
unsafeMsgId,
randomMsgId,
MsgBody,
IdsHash (..),
ServiceSub (..),
@@ -233,8 +236,10 @@ module Simplex.Messaging.Protocol
where
import Control.Applicative (optional, (<|>))
import Control.Concurrent.STM (STM, TVar)
import Control.Exception (Exception, SomeException, displayException, fromException)
import Control.Monad.Except
import Crypto.Random (ChaChaDRG)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as J
@@ -269,7 +274,7 @@ import qualified GHC.TypeLits as TE
import qualified GHC.TypeLits as Type
import Network.Socket (ServiceName)
import qualified Network.TLS as TLS
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..))
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..), blobFieldDecoder)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
@@ -281,7 +286,7 @@ import Simplex.Messaging.ServiceScheme
import Simplex.Messaging.SimplexName (LabelHash, SimplexDomain (..), SimplexTLD (..), fullDomainName, labelHash)
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..))
import Simplex.Messaging.Util (bshow, eitherToMaybe, packZipWith, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
@@ -703,24 +708,24 @@ instance Encoding NewNtfCreds where
newtype EncTransmission = EncTransmission ByteString
deriving (Show)
encTransmissionNonce :: VersionSMP -> C.CbNonce -> C.CbNonce
encTransmissionNonce v nonce@(C.CbNonce s)
| v >= fwdNoncesSMPVersion = C.cbNonce $ packZipWith xor (smpEncode v) s <> BS.drop 2 s
| otherwise = nonce
encTransmissionNonce :: C.CbNonceI n => VersionSMP -> n -> C.CbNonce
encTransmissionNonce v nonce
| v >= fwdNoncesSMPVersion = C.xorNonce (smpEncode v) nonce
| otherwise = C.toCbNonce nonce
data FwdTransmission = FwdTransmission
{ fwdCorrId :: CorrId,
{ fwdCorrId :: C.CorrCbNonce,
fwdVersion :: VersionSMP,
fwdKey :: C.PublicKeyX25519,
fwdTransmission :: EncTransmission
}
instance Encoding FwdTransmission where
smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} =
smpEncode (corrId, fwdVersion, fwdKey, Tail t)
smpEncode FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} =
smpEncode (fwdCorrId, fwdVersion, fwdKey, Tail t)
smpP = do
(corrId, fwdVersion, fwdKey, Tail t) <- smpP
pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t}
(fwdCorrId, fwdVersion, fwdKey, Tail t) <- smpP
pure FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t}
newtype EncFwdTransmission = EncFwdTransmission ByteString
deriving (Show)
@@ -766,16 +771,16 @@ newtype EncFwdResponse = EncFwdResponse ByteString
deriving (Eq, Show)
data FwdResponse = FwdResponse
{ fwdCorrId :: CorrId,
{ fwdCorrId :: C.CorrCbNonce,
fwdResponse :: EncResponse
}
instance Encoding FwdResponse where
smpEncode FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} =
smpEncode (corrId, Tail t)
smpEncode FwdResponse {fwdCorrId, fwdResponse = EncResponse t} =
smpEncode (fwdCorrId, Tail t)
smpP = do
(corrId, Tail t) <- smpP
pure FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t}
(fwdCorrId, Tail t) <- smpP
pure FwdResponse {fwdCorrId, fwdResponse = EncResponse t}
newtype EncResponse = EncResponse ByteString
deriving (Eq, Show)
@@ -798,7 +803,7 @@ toMsgInfo = \case
Message {msgId, msgTs} -> msgInfo msgId msgTs MTMessage
MessageQuota {msgId, msgTs} -> msgInfo msgId msgTs MTQuota
where
msgInfo msgId msgTs msgType = MsgInfo {msgId = decodeLatin1 $ B64.encode msgId, msgTs = systemToUTCTime msgTs, msgType}
msgInfo msgId msgTs msgType = MsgInfo {msgId = decodeLatin1 $ B64.encode $ unMsgId msgId, msgTs = systemToUTCTime msgTs, msgType}
messageId :: Message -> MsgId
messageId = \case
@@ -1499,7 +1504,34 @@ type RcvNtfPublicDhKey = C.PublicKeyX25519
type RcvNtfDhSecret = C.DhSecretX25519
-- | SMP message server ID.
type MsgId = ByteString
newtype MsgId = MsgId {unMsgId :: ByteString}
deriving (Eq, Show)
instance C.CbNonceI MsgId where
unCbNonce (MsgId s) = B.take 24 s
instance Encoding MsgId where
smpEncode (MsgId s) = smpEncode s
smpP = mkMsgId <$?> smpP
instance StrEncoding MsgId where
strEncode (MsgId s) = strEncode s
strP = mkMsgId <$?> strP
instance FromField MsgId where fromField = blobFieldDecoder mkMsgId
instance ToField MsgId where toField (MsgId s) = toField $ Binary s
mkMsgId :: ByteString -> Either String MsgId
mkMsgId s
| B.length s >= 24 = Right $ MsgId s
| otherwise = Left "MsgId: invalid length"
unsafeMsgId :: ByteString -> MsgId
unsafeMsgId s = either error id $ mkMsgId s
randomMsgId :: Int -> TVar ChaChaDRG -> STM MsgId
randomMsgId n = fmap MsgId . C.randomBytes (max 24 n)
-- | SMP message body.
type MsgBody = ByteString
@@ -1824,8 +1856,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
encodeProtocol v = \case
NEW NewQueueReq {rcvAuthKey = rKey, rcvDhKey = dhKey, auth_, subMode, queueReqData, ntfCreds}
| v >= newNtfCredsSMPVersion -> new <> e (subMode, queueReqData, ntfCreds)
| v >= shortLinksSMPVersion -> new <> e (subMode, queueReqData)
| otherwise -> new <> e (subMode, senderCanSecure (queueReqMode <$> queueReqData))
| otherwise -> new <> e (subMode, queueReqData)
where
new = e (NEW_, ' ', rKey, dhKey, auth_)
SUB -> e SUB_
@@ -1912,20 +1943,18 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
CT SCreator NEW_ -> Cmd SCreator <$> newCmd
where
newCmd
| v >= newNtfCredsSMPVersion = new smpP smpP
| v >= shortLinksSMPVersion = new smpP nothing
| otherwise = new (qReq <$> smpP) nothing
| v >= newNtfCredsSMPVersion = new smpP
| otherwise = new nothing
where
nothing = pure Nothing
new p2 p3 = NEW <$> do
new p3 = NEW <$> do
rcvAuthKey <- _smpP
rcvDhKey <- smpP
auth_ <- smpP
subMode <- smpP
queueReqData <- p2
queueReqData <- smpP
ntfCreds <- p3
pure NewQueueReq {rcvAuthKey, rcvDhKey, auth_, subMode, queueReqData, ntfCreds}
qReq sndSecure = Just $ if sndSecure then QRMessaging Nothing else QRContact Nothing
CT SRecipient tag ->
Cmd SRecipient <$> case tag of
SUB_ -> pure SUB
@@ -1976,8 +2005,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
IDS QIK {rcvId, sndId, rcvPublicDhKey = srvDh, queueMode, linkId, serviceId, serverNtfCreds}
| v >= newNtfCredsSMPVersion -> ids <> e (queueMode, linkId, serviceId, serverNtfCreds)
| v >= serviceCertsSMPVersion -> ids <> e (queueMode, linkId, serviceId)
| v >= shortLinksSMPVersion -> ids <> e (queueMode, linkId)
| otherwise -> ids <> e (senderCanSecure queueMode)
| otherwise -> ids <> e (queueMode, linkId)
where
ids = e (IDS_, ' ', rcvId, sndId, srvDh)
LNK sId d -> e (LNK_, ' ', sId, d)
@@ -2028,19 +2056,17 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
bodyP = EncRcvMsgBody . unTail <$> smpP
ALLS_ -> pure ALLS
IDS_
| v >= newNtfCredsSMPVersion -> ids smpP smpP smpP smpP
| v >= serviceCertsSMPVersion -> ids smpP smpP smpP nothing
| v >= shortLinksSMPVersion -> ids smpP smpP nothing nothing
| otherwise -> ids (qm <$> smpP) nothing nothing nothing
| v >= newNtfCredsSMPVersion -> ids smpP smpP
| v >= serviceCertsSMPVersion -> ids smpP nothing
| otherwise -> ids nothing nothing
where
qm sndSecure = Just $ if sndSecure then QMMessaging else QMContact
nothing = pure Nothing
ids p1 p2 p3 p4 = do
ids p3 p4 = do
rcvId <- _smpP
sndId <- smpP
rcvPublicDhKey <- smpP
queueMode <- p1
linkId <- p2
queueMode <- smpP
linkId <- smpP
serviceId <- p3
serverNtfCreds <- p4
pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId, serviceId, serverNtfCreds}
@@ -2423,7 +2449,9 @@ tDecodeServer THandleParams {sessionId, thVersion = v, implySessId} = \case
where
cmdOrErr = parseProtocol @v @err @cmd v command >>= checkCredentials tAuth entityId
t :: a -> (CorrId, EntityId, a)
t = (corrId,entityId,)
-- IDs are slices of the ~16 KB received block and are kept as subscription keys,
-- so without a copy one live key retains the whole block
t = (CorrId $ B.copy $ bs corrId,EntityId $ B.copy $ unEntityId entityId,)
Left _ -> tError corrId PEBlock
| otherwise -> tError corrId PESession
Left _ -> tError "" PEBlock
+47 -15
View File
@@ -1145,14 +1145,16 @@ cancelSub s = case subThread s of
type VerifiedTransmissionOrError s = Either (Transmission BrokerMsg) (VerifiedTransmission s)
receive :: forall c s. (Transport c, MsgStoreClass s) => THandleSMP c 'TServer -> s -> Client s -> M s ()
receive h@THandle {params = THandleParams {thAuth, sessionId}} ms Client {rcvQ, sndQ, rcvActiveAt} = do
receive h@THandle {params = THandleParams {thAuth, sessionId}} ms c@Client {rcvQ, rcvActiveAt} = do
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive"
sa <- asks serverActive
stats <- asks serverStats
authDelay <- asks $ authErrorDelay . config
liftIO $ forever $ do
ts <- tGetServer h
unlessM (readTVarIO sa) $ throwIO $ userError "server stopped"
atomically . (writeTVar rcvActiveAt $!) =<< getSystemTime
receivedAt <- getSystemTime
atomically $ writeTVar rcvActiveAt $! receivedAt
let (es, ts') = partitionEithers $ L.toList ts
errs = map (second ERR) es
errs' <- case ts' of
@@ -1168,7 +1170,7 @@ receive h@THandle {params = THandleParams {thAuth, sessionId}} ms Client {rcvQ,
mapM_ (atomically . writeTBQueue rcvQ) $ L.nonEmpty cmds
pure $ errs ++ errs'
[] -> pure errs
mapM_ (atomically . writeTBQueue sndQ . (,[])) $ L.nonEmpty errs'
mapM_ (sendResponses c stats authDelay receivedAt . (,[])) $ L.nonEmpty errs'
where
sameParty :: SParty p -> SignedTransmission Cmd -> Bool
sameParty p (_, _, (_, _, Cmd p' _)) = isJust $ testEquality p p'
@@ -1191,6 +1193,27 @@ receive h@THandle {params = THandleParams {thAuth, sessionId}} ms Client {rcvQ,
GET -> incStat $ msgGetAuth stats
_ -> pure ()
isAuthError :: Transmission BrokerMsg -> Bool
isAuthError (_, _, r) = case r of
ERR AUTH -> True
_ -> False
sendResponses :: Client s -> ServerStats -> Int64 -> SystemTime -> (NonEmpty (Transmission BrokerMsg), [Transmission BrokerMsg]) -> IO ()
sendResponses c@Client {sndQ} stats authDelay since rs@(ts, _)
| any isAuthError ts = sendAfterDelay c stats authDelay since rs
| otherwise = atomically $ writeTBQueue sndQ rs
sendAfterDelay :: Client s -> ServerStats -> Int64 -> SystemTime -> (NonEmpty (Transmission BrokerMsg), [Transmission BrokerMsg]) -> IO ()
sendAfterDelay c@Client {sndQ, clientTHParams = THandleParams {sessionId}} stats delay since rs
| delay > 0 = do
elapsed <- subtract (microseconds since) . microseconds <$> getSystemTime
when (elapsed >= delay) $ incStat $ authDelayExceeded stats
forkClient c (B.unpack $ "client $" <> encode sessionId <> " auth") $ threadDelay' (delay - elapsed `mod` delay) >> write
| otherwise = write
where
write = atomically $ writeTBQueue sndQ rs
microseconds (MkSystemTime s ns) = s * 1000000 + fromIntegral (ns `div` 1000)
send :: Transport c => MVar (THandleSMP c 'TServer) -> Client s -> IO ()
send th c@Client {sndQ, msgQ, clientTHParams = THandleParams {sessionId}} = do
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send"
@@ -1334,7 +1357,7 @@ verifyCmdAuthorization thAuth tAuth authorized corrId key = maybe False (verify
verifyCmdAuth :: Maybe (THandleAuth 'TServer) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> CorrId -> Bool
verifyCmdAuth thAuth k authenticator authorized (CorrId corrId) = case thAuth of
Just THAuthServer {serverPrivKey = pk} -> C.cbVerify k pk (C.cbNonce corrId) authenticator authorized
Just THAuthServer {serverPrivKey = pk} -> either (const False) (\nonce -> C.cbVerify k pk nonce authenticator authorized) $ C.cbNonce corrId
Nothing -> False
dummyVerifyCmd :: Maybe (THandleAuth 'TServer) -> Maybe TAuthorizations -> ByteString -> CorrId -> Bool
@@ -1374,15 +1397,18 @@ client
ms
clnt@Client {clientId, rcvQ, sndQ, msgQ, clientTHParams = thParams'@THandleParams {sessionId}, procThreads} = do
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands"
stats <- asks serverStats
authDelay <- asks $ authErrorDelay . config
let clntServiceId = (\THClientService {serviceId} -> serviceId) <$> (peerClientService =<< thAuth thParams')
process batchSubs t acc@(rs, msgs) =
(maybe acc (\(!r, !msg_) -> (r : rs, maybe msgs (: msgs) msg_)))
<$> processCommand clntServiceId batchSubs t
forever $ do
batch <- atomically (readTBQueue rcvQ)
processedAt <- liftIO getSystemTime
batchSubs <- prepareBatchSubs clntServiceId batch
foldrM (process batchSubs) ([], []) batch
>>= \(rs_, msgs) -> mapM_ (atomically . writeTBQueue sndQ . (,msgs)) (L.nonEmpty rs_)
>>= \(rs_, msgs) -> mapM_ (liftIO . sendResponses clnt stats authDelay processedAt . (,msgs)) (L.nonEmpty rs_)
where
prepareBatchSubs ::
Maybe ServiceId ->
@@ -1453,14 +1479,15 @@ client
ServerStats {pMsgFwds, pMsgFwdsOwn} <- asks serverStats
let inc = mkIncProxyStats pMsgFwds pMsgFwdsOwn
liftIO (lookupSMPServerClient a sessId) >>= \case
Just (own, smp) -> do
Just (own, smp) | Right fwdCorrId <- C.corrCbNonce (bs corrId) -> do
inc own pRequests
forkProxiedCmd $ do
liftIO (runExceptT (forwardSMPTransmission smp corrId fwdV pubKey encBlock) `E.catches` clientHandlers) >>= \case
liftIO (runExceptT (forwardSMPTransmission smp fwdCorrId fwdV pubKey encBlock) `E.catches` clientHandlers) >>= \case
Right (nonce_, r) -> PRES nonce_ r <$ inc own pSuccesses
Left e -> ERR (smpProxyError e) <$ case e of
PCEProtocolError {} -> inc own pSuccesses
_ -> inc own pErrorsOther
Just _ -> pure $ Just $ ERR $ CMD SYNTAX
Nothing -> inc False pRequests >> inc False pErrorsConnect $> Just (ERR $ PROXY NO_SESSION)
where
forkProxiedCmd :: M s BrokerMsg -> M s (Maybe BrokerMsg)
@@ -1950,7 +1977,7 @@ client
getDelivered :: Sub -> STM (Maybe (ServerSub, SystemSeconds))
getDelivered Sub {delivered, subThread} = do
readTVar delivered $>>= \(msgId', ts) ->
if msgId == msgId' || B.null msgId
if msgId == msgId'
then writeTVar delivered Nothing $> Just (subThread, ts)
else pure Nothing
updateStats :: ServerStats -> Bool -> SystemSeconds -> Message -> IO ()
@@ -1996,7 +2023,7 @@ client
when (isJust (queueData qr) && isSecuredMsgQueue qr) $ void $ liftIO $
deleteQueueLinkData (queueStore ms) q
ServerConfig {messageExpiration, expireMessagesOnSend, msgIdBytes} <- asks config
msgId <- randomId' msgIdBytes
msgId <- atomically . randomMsgId msgIdBytes =<< asks random
msg_ <- liftIO $ runExceptT $ do
when expireMessagesOnSend $ mapM_ (expireMessages stats) messageExpiration
msg <- liftIO $ mkMessage msgId body
@@ -2109,7 +2136,7 @@ client
liftIO $ storeNtf ns nId ntf
incStat . ntfCount =<< asks serverStats
mkMessageNotification :: ByteString -> SystemTime -> RcvNtfDhSecret -> M s MsgNtf
mkMessageNotification :: MsgId -> SystemTime -> RcvNtfDhSecret -> M s MsgNtf
mkMessageNotification msgId msgTs rcvNtfDhSecret = do
ntfNonce <- atomically . C.randomCbNonce =<< asks random
let msgMeta = NMsgMeta {msgId, msgTs}
@@ -2118,14 +2145,15 @@ client
processForwardedCommand :: EncFwdTransmission -> M s (Maybe BrokerMsg)
processForwardedCommand (EncFwdTransmission s) = fmap (either (Just . ERR) id) . runExceptT $ do
startedAt <- liftIO getSystemTime
THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE $ transportErr TENoServerAuth) pure (thAuth thParams')
sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret'
let proxyNonce = C.cbNonce $ bs corrId
proxyNonce <- liftEitherWith (const CRYPTO) $ C.cbNonce $ bs corrId
s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s
FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s'
unless (fwdVersion `isCompatible` thServerVRange thParams') $ throwE $ transportErr TEVersion
let clientSecret = C.dh' fwdKey serverPrivKey
clientNonce = C.cbNonce $ bs fwdCorrId
clientNonce = fwdCorrId
b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret (encTransmissionNonce fwdVersion clientNonce) et
let clntTHParams = smpTHParamsSetVersion fwdVersion thParams'
-- only allowing single forwarded transactions
@@ -2161,7 +2189,11 @@ client
_ -> Just . maybe (corrId', entId', ERR INTERNAL) fst <$> lift (processCommand Nothing (Right (M.empty, M.empty, M.empty)) t'')
stats <- asks serverStats
incStat $ pMsgFwdsRecv stats
traverse encodeResp r_
traverse encodeResp r_ >>= \case
Just rres | any isAuthError r_ -> do
authDelay <- asks $ authErrorDelay . config
Nothing <$ liftIO (sendAfterDelay clnt stats authDelay startedAt ([(corrId, NoEntity, rres)], []))
rres_ -> pure rres_
where
rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmissionOrError ErrorType Cmd -> M s (VerifiedTransmissionOrError s)
rejectOrVerify clntThAuth = \case
@@ -2187,7 +2219,7 @@ client
MessageQuota {} -> RcvMsgQuota msgTs'
where
encrypt :: KnownNat i => C.MaxLenBS i -> RcvMessage
encrypt body = RcvMessage msgId' . EncRcvMsgBody $ C.cbEncryptMaxLenBS (rcvDhSecret qr) (C.cbNonce msgId') body
encrypt body = RcvMessage msgId' . EncRcvMsgBody $ C.cbEncryptMaxLenBS (rcvDhSecret qr) msgId' body
msgId' = messageId msg
msgTs' = messageTs msg
@@ -2236,7 +2268,7 @@ client
SubPending -> QSubPending
SubThread _ -> QSubThread
ProhibitSub -> pure QProhibitSub
qDelivered <- decodeLatin1 . encode . fst <$$> readTVarIO delivered
qDelivered <- decodeLatin1 . encode . unMsgId . fst <$$> readTVarIO delivered
pure QSub {qSubThread, qDelivered}
ok :: Transmission BrokerMsg
+9 -1
View File
@@ -68,6 +68,7 @@ module Simplex.Messaging.Server.Env.STM
defaultInactiveClientExpiration,
defaultProxyClientConcurrency,
defaultNameResolverConcurrency,
defaultAuthErrorDelayMs,
defaultMaxJournalMsgCount,
defaultMaxJournalStateLines,
defaultIdleQueueInterval,
@@ -187,6 +188,7 @@ data ServerConfig s = ServerConfig
ntfDeliveryInterval :: Int,
-- | interval between sending pending END events to unsubscribed clients, seconds
pendingENDInterval :: Int,
authErrorDelay :: Int64,
smpCredentials :: ServerCredentials,
httpCredentials :: Maybe ServerCredentials,
-- | SMP client-server protocol version range
@@ -255,6 +257,9 @@ defaultProxyClientConcurrency = 32
defaultNameResolverConcurrency :: Int
defaultNameResolverConcurrency = 1000
defaultAuthErrorDelayMs :: Int64
defaultAuthErrorDelayMs = 50
journalMsgStoreDepth :: Int
journalMsgStoreDepth = 5
@@ -571,7 +576,10 @@ newProhibitedSub = do
return Sub {subThread = ProhibitSub, delivered}
newEnv :: ServerConfig s -> IO (Env s)
newEnv config@ServerConfig {smpCredentials, httpCredentials, serverStoreCfg, smpAgentCfg, information, messageExpiration, idleQueueInterval, msgQueueQuota, maxJournalMsgCount, maxJournalStateLines, namesConfig} = do
newEnv config@ServerConfig {smpCredentials, httpCredentials, serverStoreCfg, smpAgentCfg, information, messageExpiration, idleQueueInterval, msgQueueQuota, maxJournalMsgCount, maxJournalStateLines, namesConfig, msgIdBytes} = do
when (msgIdBytes < 24) $ do
logError "Error: msgIdBytes must be at least 24"
exitFailure
serverActive <- newTVarIO True
server <- newServer
msgStore_ <- case serverStoreCfg of
+1
View File
@@ -580,6 +580,7 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
prometheusMetricsFile = combine logPath "smp-server-metrics.txt",
pendingENDInterval = 15000000, -- 15 seconds
ntfDeliveryInterval = 1500000, -- 1.5 second
authErrorDelay = 1000 * readIniDefault defaultAuthErrorDelayMs "TRANSPORT" "auth_error_delay_ms" ini,
smpServerVRange = supportedServerSMPRelayVRange,
transportConfig =
mkTransportServerConfig
+3 -1
View File
@@ -130,7 +130,9 @@ iniFileContent cfgPath logPath opts host basicAuth controlPortPwds =
<> ("host = " <> T.pack host <> "\n")
<> ("port = " <> defaultServerPorts <> "\n")
<> "log_tls_errors = off\n\n\
\# Use `websockets = 443` to run websockets server in addition to plain TLS.\n\
\# Minimum time to respond with AUTH error, milliseconds.\n"
<> ("# auth_error_delay_ms = " <> tshow defaultAuthErrorDelayMs <> "\n\n")
<> "# Use `websockets = 443` to run websockets server in addition to plain TLS.\n\
\# This option is deprecated and should be used for testing only.\n\
\# , port 443 should be specified in port above\n\
\websockets = off\n"
@@ -202,7 +202,7 @@ instance MsgStoreClass PostgresMsgStore where
DB.query
db
"SELECT quota_written, was_empty FROM write_message(?,?,?,?,?,?,?)"
(recipientId' q, Binary (messageId msg), systemSeconds (messageTs msg), msgQuota, ntf, Binary body, quota)
(recipientId' q, messageId msg, systemSeconds (messageTs msg), msgQuota, ntf, Binary body, quota)
where
toResult = \case
((msgQuota, wasEmpty) : _) -> if msgQuota then Nothing else Just (msg, wasEmpty)
@@ -271,14 +271,14 @@ instance MsgStoreClass PostgresMsgStore where
uninterruptibleMask_ $
withDB' "tryDelMsg" (queueStore_ ms) $ \db ->
maybeFirstRow toMessage $
DB.query db "SELECT r_msg_id, r_msg_ts, r_msg_quota, r_msg_ntf_flag, r_msg_body FROM try_del_msg(?, ?)" (recipientId' q, Binary msgId)
DB.query db "SELECT r_msg_id, r_msg_ts, r_msg_quota, r_msg_ntf_flag, r_msg_body FROM try_del_msg(?, ?)" (recipientId' q, msgId)
tryDelPeekMsg :: PostgresMsgStore -> PostgresQueue -> MsgId -> ExceptT ErrorType IO (Maybe Message, Maybe Message)
tryDelPeekMsg ms q msgId =
uninterruptibleMask_ $
withDB' "tryDelPeekMsg" (queueStore_ ms) $ \db ->
toResult . map toMessage
<$> DB.query db "SELECT r_msg_id, r_msg_ts, r_msg_quota, r_msg_ntf_flag, r_msg_body FROM try_del_peek_msg(?, ?)" (recipientId' q, Binary msgId)
<$> DB.query db "SELECT r_msg_id, r_msg_ts, r_msg_quota, r_msg_ntf_flag, r_msg_body FROM try_del_peek_msg(?, ?)" (recipientId' q, msgId)
where
toResult = \case
[] -> (Nothing, Nothing)
@@ -297,13 +297,13 @@ uninterruptibleMask_ :: ExceptT ErrorType IO a -> ExceptT ErrorType IO a
uninterruptibleMask_ = ExceptT . E.uninterruptibleMask_ . runExceptT
{-# INLINE uninterruptibleMask_ #-}
toMaybeMessage :: (Maybe (Binary MsgId), Maybe Int64, Maybe Bool, Maybe Bool, Maybe (Binary MsgBody)) -> Maybe Message
toMaybeMessage :: (Maybe MsgId, Maybe Int64, Maybe Bool, Maybe Bool, Maybe (Binary MsgBody)) -> Maybe Message
toMaybeMessage = \case
(Just msgId, Just ts, Just msgQuota, Just ntf, Just body) -> Just $ toMessage (msgId, ts, msgQuota, ntf, body)
_ -> Nothing
toMessage :: (Binary MsgId, Int64, Bool, Bool, Binary MsgBody) -> Message
toMessage (Binary msgId, ts, msgQuota, ntf, Binary body)
toMessage :: (MsgId, Int64, Bool, Bool, Binary MsgBody) -> Message
toMessage (msgId, ts, msgQuota, ntf, Binary body)
| msgQuota = MessageQuota {msgId, msgTs}
| otherwise = Message {msgId, msgTs, msgFlags = MsgFlags ntf, msgBody = C.unsafeMaxLenBS body} -- TODO [messages] unsafeMaxLenBS?
where
@@ -429,7 +429,7 @@ messageRecToText rId msg =
tabFields = BB.char7 ',' `intersperse` fields
fields =
[ renderField (toField rId),
renderField (toField $ Binary (messageId msg)),
renderField (toField $ messageId msg),
renderField (toField $ systemSeconds (messageTs msg)),
renderField (toField msgQuota),
renderField (toField ntf),
@@ -126,6 +126,7 @@ prometheusMetrics sm rtm ts =
_ntfServices,
_rcvServicesSubMsg,
_rcvServicesSubDuplicate,
_authDelayExceeded,
_qCount,
_msgCount,
_ntfCount,
@@ -192,6 +193,10 @@ prometheusMetrics sm rtm ts =
\# HELP simplex_smp_queues_notify_monthly Monthly active queues with notifications.\n\
\# TYPE simplex_smp_queues_notify_monthly gauge\n\
\simplex_smp_queues_notify_monthly " <> mstr (monthCount psNtf) <> "\n# monthCountNtf\n\
\\n\
\# HELP simplex_smp_auth_delay_exceeded Responses with AUTH errors sent without delay, as processing took longer than the delay.\n\
\# TYPE simplex_smp_auth_delay_exceeded counter\n\
\simplex_smp_auth_delay_exceeded " <> mshow _authDelayExceeded <> "\n# authDelayExceeded\n\
\\n"
subscriptions =
"# Subscriptions\n\
@@ -825,6 +825,10 @@ instance ToField EncDataBytes where toField (EncDataBytes s) = toField (Binary s
deriving newtype instance FromField EncDataBytes
instance ToField MsgId where toField = toField . Binary . unMsgId
instance FromField MsgId where fromField = blobFieldDecoder mkMsgId
deriving newtype instance ToField (RoundedSystemTime t)
deriving newtype instance FromField (RoundedSystemTime t)
+8
View File
@@ -128,6 +128,7 @@ data ServerStats = ServerStats
ntfServices :: ServiceStats,
rcvServicesSubMsg :: IORef Int,
rcvServicesSubDuplicate :: IORef Int,
authDelayExceeded :: IORef Int,
qCount :: IORef Int,
msgCount :: IORef Int,
ntfCount :: IORef Int,
@@ -190,6 +191,7 @@ data ServerStatsData = ServerStatsData
_rcvServices :: ServiceStatsData,
_rcvServicesSubMsg :: Int,
_rcvServicesSubDuplicate :: Int,
_authDelayExceeded :: Int,
_qCount :: Int,
_msgCount :: Int,
_ntfCount :: Int,
@@ -254,6 +256,7 @@ newServerStats ts = do
ntfServices <- newServiceStats
rcvServicesSubMsg <- newIORef 0
rcvServicesSubDuplicate <- newIORef 0
authDelayExceeded <- newIORef 0
qCount <- newIORef 0
msgCount <- newIORef 0
ntfCount <- newIORef 0
@@ -315,6 +318,7 @@ newServerStats ts = do
ntfServices,
rcvServicesSubMsg,
rcvServicesSubDuplicate,
authDelayExceeded,
qCount,
msgCount,
ntfCount,
@@ -378,6 +382,7 @@ getServerStatsData s = do
_ntfServices <- getServiceStatsData $ ntfServices s
_rcvServicesSubMsg <- readIORef $ rcvServicesSubMsg s
_rcvServicesSubDuplicate <- readIORef $ rcvServicesSubDuplicate s
_authDelayExceeded <- readIORef $ authDelayExceeded s
_qCount <- readIORef $ qCount s
_msgCount <- readIORef $ msgCount s
_ntfCount <- readIORef $ ntfCount s
@@ -439,6 +444,7 @@ getServerStatsData s = do
_ntfServices,
_rcvServicesSubMsg,
_rcvServicesSubDuplicate,
_authDelayExceeded,
_qCount,
_msgCount,
_ntfCount,
@@ -503,6 +509,7 @@ setServerStats s d = do
setServiceStats (ntfServices s) $! _ntfServices d
writeIORef (rcvServicesSubMsg s) $! _rcvServicesSubMsg d
writeIORef (rcvServicesSubDuplicate s) $! _rcvServicesSubDuplicate d
writeIORef (authDelayExceeded s) $! _authDelayExceeded d
writeIORef (qCount s) $! _qCount d
writeIORef (msgCount s) $! _msgCount d
writeIORef (ntfCount s) $! _ntfCount d
@@ -705,6 +712,7 @@ instance StrEncoding ServerStatsData where
_ntfServices,
_rcvServicesSubMsg = 0,
_rcvServicesSubDuplicate = 0,
_authDelayExceeded = 0,
_qCount,
_msgCount = 0,
_ntfCount = 0,
+5 -9
View File
@@ -46,7 +46,6 @@ module Simplex.Messaging.Transport
minServerSMPRelayVersion,
currentClientSMPRelayVersion,
currentServerSMPRelayVersion,
shortLinksSMPVersion,
serviceCertsSMPVersion,
newNtfCredsSMPVersion,
clientNoticesSMPVersion,
@@ -191,11 +190,8 @@ type VersionRangeSMP = VersionRange SMPVersion
pattern VersionSMP :: Word16 -> VersionSMP
pattern VersionSMP v = Version v
_proxyServerHandshakeSMPVersion :: VersionSMP
_proxyServerHandshakeSMPVersion = VersionSMP 14
shortLinksSMPVersion :: VersionSMP
shortLinksSMPVersion = VersionSMP 15
_shortLinksSMPVersion :: VersionSMP
_shortLinksSMPVersion = VersionSMP 15
serviceCertsSMPVersion :: VersionSMP
serviceCertsSMPVersion = VersionSMP 16
@@ -224,10 +220,10 @@ fwdNoncesSMPVersion :: VersionSMP
fwdNoncesSMPVersion = VersionSMP 23
minClientSMPRelayVersion :: VersionSMP
minClientSMPRelayVersion = VersionSMP 14
minClientSMPRelayVersion = VersionSMP 15
minServerSMPRelayVersion :: VersionSMP
minServerSMPRelayVersion = VersionSMP 14
minServerSMPRelayVersion = VersionSMP 15
currentClientSMPRelayVersion :: VersionSMP
currentClientSMPRelayVersion = VersionSMP 23
@@ -245,7 +241,7 @@ currentServerSMPRelayVersion = VersionSMP 23
proxiedSMPRelayVersion :: VersionSMP
proxiedSMPRelayVersion = VersionSMP 23
-- minimal supported protocol version is 14
-- minimal supported protocol version is 15
supportedClientSMPRelayVRange :: VersionRangeSMP
supportedClientSMPRelayVRange = mkVersionRange minClientSMPRelayVersion currentClientSMPRelayVersion
@@ -362,6 +362,10 @@ connectionRequestTests =
smpEncodingTest $ AgentServiceRequest [qInfo] Nothing "service request payload"
smpEncodingTest $ AgentServiceResponse "service response payload"
smpEncodingTest $ AgentRejection "rejected: not allowed"
it "should serialize and parse ratchet key info" $ do
smpDecode "R" `shouldBe` Right (AgentRatchetInfo RatchetInfo {answeredKeyHash = Nothing})
smpDecode "R1\3abcdef" `shouldBe` Right (AgentRatchetInfo RatchetInfo {answeredKeyHash = Just "abc"})
smpEncodingTest $ AgentRatchetInfo RatchetInfo {answeredKeyHash = Just "0123456789abcdef0123456789abcdef"}
where
smpEncodingTest :: (Encoding a, Eq a, Show a, HasCallStack) => a -> Expectation
smpEncodingTest a = smpDecode (smpEncode a) `shouldBe` Right a
+289 -30
View File
@@ -70,8 +70,8 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Either (isRight)
import Data.Int (Int64)
import Data.List (find, isPrefixOf, isSuffixOf)
import Data.List.NonEmpty (NonEmpty)
import Data.List (find, isInfixOf, isPrefixOf, isSuffixOf)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map as M
import Data.Maybe (isJust, isNothing)
import qualified Data.Set as S
@@ -87,12 +87,12 @@ import SMPAgentClient
import SMPClient
import Simplex.Messaging.Agent hiding (acceptContact, createConnection, deleteConnection, deleteConnections, getConnShortLink, joinConnection, sendMessage, setConnShortLink, subscribeConnection, suspendConnection)
import qualified Simplex.Messaging.Agent as A
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), ServerQueueInfo (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork)
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), ServerQueueInfo (..), UserNetworkInfo (..), UserNetworkType (..), sendAgentMessage, waitForUserNetwork)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Env (..), InitialAgentServers (..), createAgentStore)
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT)
import qualified Simplex.Messaging.Agent.Protocol as A
import Simplex.Messaging.Agent.Store (Connection' (..), SomeConn' (..), StoredRcvQueue (..))
import Simplex.Messaging.Agent.Store.AgentStore (getConn)
import Simplex.Messaging.Agent.Store.AgentStore (deleteRatchetKeyHashesExpired, getConn, getConnIds, getRatchetX3dhKeys)
import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction)
import Simplex.Messaging.Agent.Store.Interface
import qualified Simplex.Messaging.Agent.Store.DB as DB
@@ -112,7 +112,7 @@ import Simplex.Messaging.Server.Information (ServerPublicInfo (..))
import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..))
import Simplex.Messaging.Server.QueueStore.QueueInfo
import Simplex.Messaging.Server.StoreLog (StoreLogRecord (..))
import Simplex.Messaging.Transport (ASrvTransport, SMPVersion, VersionSMP, currentServerSMPRelayVersion, minClientSMPRelayVersion, minServerSMPRelayVersion, alpnSupportedSMPHandshakes, supportedServerSMPRelayVRange)
import Simplex.Messaging.Transport (ASrvTransport, SMPVersion, VersionSMP, currentServerSMPRelayVersion, minClientSMPRelayVersion, minServerSMPRelayVersion, alpnSupportedSMPHandshakes)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..))
import Simplex.Messaging.Util (bshow, diffToMicroseconds)
import Simplex.Messaging.Version (VersionRange (..))
@@ -371,6 +371,10 @@ functionalAPITests ps = do
testAddDRViaSetConnShortLink ps
it "should resume DR accept after a transient failure (reuse the send queue and ratchet)" $
testAcceptContactDRResumeAfterOffline ps
it "should store the ratchet when DR accept is prepared" $
withSmpServer ps $ testAcceptContactDRPrepared False
it "should create the ratchet at DR accept when the prepared connection has none" $
withSmpServer ps $ testAcceptContactDRPrepared True
it "should support rejecting contact request" $
withSmpServer ps testRejectContactRequest
it "should communicate rejection reason via double ratchet" $
@@ -415,13 +419,16 @@ functionalAPITests ps = do
describe "should connect via 1-time short link with async join" $ testProxyMatrix ps testInvitationShortLinkAsync
describe "should connect via contact short link" $ testProxyMatrix ps testContactShortLink
describe "should add short link to existing contact and connect" $ testProxyMatrix ps testAddContactShortLink
xdescribe "try to create 1-time short link with prev versions" $ testProxyMatrixWithPrev ps testInvitationShortLinkPrev
describe "server restart" $ do
it "should get 1-time link data after restart" $ testInvitationShortLinkRestart ps
it "should connect via contact short link after restart" $ testContactShortLinkRestart ps
it "should connect via added contact short link after restart" $ testAddContactShortLinkRestart ps
it "should create and get short links with the old contact queues" $ testOldContactQueueShortLink ps
it "should connect via prepared connection link" $ testPrepareCreateConnectionLink ps
it "should connect via prepared invitation link" $ testPrepareCreateInvitationLink ps
it "should not create connection for prepared invitation link with too large data" $ testPrepareInvitationLinkTooLarge ps
it "should not create connection for prepared contact link with too large data" $ testPrepareContactLinkTooLarge ps
it "should connect via short link prepared for existing contact connection" $ testPrepareConnShortLink ps
describe "Message delivery" $ do
describe "update connection agent version on received messages" $ do
it "should increase if compatible, shouldn'ps decrease" $
@@ -463,6 +470,16 @@ functionalAPITests ps = do
testRatchetSyncSuspendForeground ps
it "should synchronize ratchets when clients start synchronization simultaneously" $
testRatchetSyncSimultaneous ps
it "should ignore replayed ratchet key after expired hashes are deleted" $
testRatchetSyncReplayedKey ps
it "should synchronize ratchets when synchronization is forced again" $
testRatchetSyncRepeated ps
it "should not mark ratchet key as processed when ratchet recreation fails" $
testRatchetSyncFailedKeyNotProcessed ps
it "should not store reply ratchet key when ratchet recreation fails" $
testRatchetSyncFailedRecreationNoReply ps
it "should not store ratchet key when starting synchronization fails" $
testRatchetSyncStartFailedNoKey ps
#endif
describe "Subscription mode OnlyCreate" $ do
it "messages delivered only when polled" $
@@ -541,6 +558,10 @@ functionalAPITests ps = do
testServerMatrix2 ps testFastSwitchConnection
it "should switch delivery to the new queue when the old server is down" $
testFastSwitchDeadOldServer ps
it "should drain the old queue after QEND on the new queue" $
testFastSwitchDrainOldQueue ps
it "should drain the old queue after QEND on the new queue and restart" $
testFastSwitchDrainOldQueueRestart ps
describe "should switch delivery to the new queue" $
testServerMatrix2 ps testSwitchConnection
describe "should switch to new queue asynchronously" $
@@ -667,19 +688,6 @@ testProxyMatrix ps runTest = do
it "2 servers, directly" $ withSmpServers2 ps $ withAgentClientsServers2 (agentCfg, initAgentServers) (agentCfg, initAgentServers2) $ runTest False
it "2 servers, via proxy" $ withSmpServersProxy2 ps $ withAgentClientsServers2 (agentCfg, initAgentServersProxy) (agentCfg, initAgentServersProxy2) $ runTest True
testProxyMatrixWithPrev :: HasCallStack => (ASrvTransport, AStoreType) -> (Bool -> Bool -> AgentClient -> AgentClient -> IO ()) -> Spec
testProxyMatrixWithPrev ps@(t, msType@(ASType qs _ms)) runTest = do
it "2 servers, directly, curr clients, prev servers" $ withSmpServers2Prev $ withAgentClientsServers2 (agentCfg, initAgentServers) (agentCfg, initAgentServers2) $ runTest False True
it "2 servers, via proxy, curr clients, prev servers" $ withSmpServersProxy2Prev $ withAgentClientsServers2 (agentCfg, initAgentServersProxy) (agentCfg, initAgentServersProxy2) $ runTest True True
it "2 servers, directly, prev clients, curr servers" $ withSmpServers2 ps $ withAgentClientsServers2 (agentCfgVPrevPQ, initAgentServers) (agentCfgVPrevPQ, initAgentServers2) $ runTest False False
it "2 servers, via proxy, prev clients, curr servers" $ withSmpServersProxy2 ps $ withAgentClientsServers2 (agentCfgVPrevPQ, initAgentServersProxy) (agentCfgVPrevPQ, initAgentServersProxy2) $ runTest True False
where
prev cfg' = updateCfg cfg' $ \cfg_ -> cfg_ {smpServerVRange = prevRange supportedServerSMPRelayVRange}
withSmpServers2Prev a = withServers2 (prev $ cfgMS msType) (prev $ cfgJ2QS qs) a
withSmpServersProxy2Prev a = withServers2 (prev $ proxyCfgMS msType) (prev $ proxyCfgJ2QS qs) a
withServers2 cfg1 cfg2 a =
withSmpServerConfigOn t cfg1 testPort $ \_ -> withSmpServerConfigOn t cfg2 testPort2 $ \_ -> a
testPQMatrix2 :: HasCallStack => (ASrvTransport, AStoreType) -> (HasCallStack => (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO ()) -> Spec
testPQMatrix2 = pqMatrix2_ True
@@ -1241,6 +1249,30 @@ testAcceptContactDRResumeAfterOffline ps = withAgentClients2 $ \alice bob -> do
("", _, A.CONF confId _ _ "alice's connInfo") <- get bob
allowConfirmGreet alice bobId bob aliceId confId addrIK pqEnc
testAcceptContactDRPrepared :: HasCallStack => Bool -> IO ()
testAcceptContactDRPrepared ratchetDeleted =
withAgentClients2 $ \alice bob -> runRight_ $ do
let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData "test user data", ratchetKeys = Nothing}
pqEnc = PQEncryption $ pqConnectionMode IKPQOn PQSupportOn
(_, CCLink connReq _) <- A.createConnection alice NRMInteractive 1 True True SCMContact (Just userLinkData) Nothing IKPQOn True SMSubscribe
(aliceId, CRBRatchet codes) <- A.prepareConnectionToJoin bob 1 True connReq PQSupportOn
void $ A.joinConnection bob NRMInteractive 1 aliceId True connReq "bob's connInfo" PQSupportOn SMSubscribe
("", _, A.REQ invId _ _ "bob's connInfo" (CRBRatchet reqCodes) _) <- get alice
(bobId, CRBRatchet acceptCodes) <- A.prepareConnectionToAccept alice 1 True invId PQSupportOn
liftIO $ do
reqCodes `shouldBe` codes
acceptCodes `shouldBe` codes
if ratchetDeleted
then do
liftIO $ withTransaction (store $ agentEnv alice) (`DB.execute_` "DELETE FROM ratchets")
Left (CONN NOT_FOUND _) <- tryError $ getConnectionVerifyCodes alice bobId
pure ()
else getConnectionVerifyCodes alice bobId >>= liftIO . (`shouldBe` codes)
void $ acceptContact alice 1 bobId True invId "alice's connInfo" PQSupportOn SMSubscribe
("", _, A.CONF confId _ _ "alice's connInfo") <- get bob
allowConfirmGreet alice bobId bob aliceId confId IKPQOn pqEnc
getConnectionVerifyCodes alice bobId >>= liftIO . (`shouldBe` codes)
runAgentClientContactTestPQ3 :: HasCallStack => Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> (AgentClient, PQSupport) -> AgentMsgId -> IO ()
runAgentClientContactTestPQ3 viaProxy (alice, aPQ) (bob, bPQ) (tom, tPQ) baseId = runRight_ $ do
(_, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMContact Nothing Nothing aPQ False SMSubscribe
@@ -1781,14 +1813,6 @@ testJoinConn_ viaProxy sndSecure a bId b connReq = do
get b ##> ("", aId, CON)
exchangeGreetingsViaProxy viaProxy a bId b aId
testInvitationShortLinkPrev :: HasCallStack => Bool -> Bool -> AgentClient -> AgentClient -> IO ()
testInvitationShortLinkPrev viaProxy sndSecure a b = runRight_ $ do
let userData = UserLinkData "some user data"
newLinkData = UserInvLinkData userData
-- can't create short link with previous version
(bId, CCLink connReq Nothing) <- A.createConnection a NRMInteractive 1 True True SCMInvitation (Just newLinkData) Nothing CR.IKPQOn False SMSubscribe
testJoinConn_ viaProxy sndSecure a bId b connReq
testInvitationShortLinkAsync :: HasCallStack => Bool -> AgentClient -> AgentClient -> IO ()
testInvitationShortLinkAsync viaProxy a b = do
let userData = UserLinkData "some user data"
@@ -2063,6 +2087,64 @@ testPrepareCreateConnectionLink ps = withSmpServer ps $ withAgentClients2 $ \a b
get b ##> ("", bId, CON)
exchangeGreetings a aId b bId
testPrepareCreateInvitationLink :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testPrepareCreateInvitationLink ps = withSmpServer ps $ withAgentClients2 $ \a b -> do
let userData = UserLinkData "test user data"
runRight_ $ do
(preparedReq, preparedParams@PreparedLinkParams {plpLinkKey}) <- A.prepareInvitationLink a 1 CR.IKPQOn Nothing
(bId, CCLink connReq (Just shortLink@(CSLInvitation _ _ _ linkKey))) <- A.createInvitationForLink a NRMInteractive 1 True preparedReq preparedParams (UserInvLinkData userData) SMSubscribe
liftIO $ linkKey `shouldBe` plpLinkKey
(_, connData', connReq') <- getConnShortLink b 1 shortLink
liftIO $ connReq' `shouldBe` preparedReq
liftIO $ connReq `shouldNotBe` preparedReq
liftIO $ linkUserData connData' `shouldBe` userData
testJoinConn_ False True a bId b connReq
testPrepareInvitationLinkTooLarge :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testPrepareInvitationLinkTooLarge ps = withSmpServer ps $ withAgent 1 agentCfg initAgentServers testDB $ \a -> do
runRight_ $ do
(preparedReq, preparedParams) <- A.prepareInvitationLink a 1 CR.IKPQOn Nothing
Left (A.CMD LARGE _) <- tryError $ A.createInvitationForLink a NRMInteractive 1 True preparedReq preparedParams (UserInvLinkData $ UserLinkData $ B.replicate 14000 'a') SMSubscribe
pure ()
withTransaction (store $ agentEnv a) getConnIds `shouldReturn` []
testPrepareContactLinkTooLarge :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testPrepareContactLinkTooLarge ps = withSmpServer ps $ withAgent 1 agentCfg initAgentServers testDB $ \a -> do
let userCtData = UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData $ B.replicate 14000 'a', ratchetKeys = Nothing}
g <- C.newRandom
rootKey <- atomically $ C.generateKeyPair g
linkEntId <- atomically $ C.randomBytes 32 g
runRight_ $ do
(ccLink, preparedParams) <- A.prepareConnectionLink a 1 rootKey linkEntId True Nothing CR.IKPQOn True Nothing
Left (A.CMD LARGE _) <- tryError $ A.createConnectionForLink a NRMInteractive 1 True ccLink preparedParams (UserContactLinkData userCtData) SMSubscribe
pure ()
withTransaction (store $ agentEnv a) getConnIds `shouldReturn` []
testPrepareConnShortLink :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testPrepareConnShortLink ps = withSmpServer ps $ withAgentClients2 $ \a b -> do
let userCtData = UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData "test user data", ratchetKeys = Nothing}
runRight_ $ do
(contactId, CCLink _ Nothing) <- A.createConnection a NRMInteractive 1 True True SCMContact Nothing Nothing CR.IKPQOn False SMSubscribe
shortLink <- A.prepareConnShortLink a contactId Nothing
shortLink' <- A.prepareConnShortLink a contactId Nothing
liftIO $ shortLink' `shouldBe` shortLink
shortLink'' <- A.setConnShortLink a NRMInteractive contactId SCMContact (UserContactLinkData userCtData) Nothing False Nothing
liftIO $ shortLink'' `shouldBe` shortLink
(_, ContactLinkData _ userCtData', connReq) <- getConnShortLink b 1 shortLink
liftIO $ userCtData' `shouldBe` userCtData
(aId, sndSecure) <- joinConnection b 1 True connReq "bob's connInfo" SMSubscribe
liftIO $ sndSecure `shouldBe` False
("", _, REQ invId _ "bob's connInfo") <- get a
(bId, _) <- A.prepareConnectionToAccept a 1 True invId PQSupportOn
sndSecure' <- acceptContact a 1 bId True invId "alice's connInfo" PQSupportOn SMSubscribe
liftIO $ sndSecure' `shouldBe` True
("", _, CONF confId _ "alice's connInfo") <- get b
allowConnection b aId confId "bob's connInfo"
get a ##> ("", bId, INFO "bob's connInfo")
get a ##> ("", bId, CON)
get b ##> ("", aId, CON)
exchangeGreetings a bId b aId
connReqWithKeys :: BinaryConnectionRequestUri m -> Maybe AddressRatchetKeys -> ConnectionRequestUri m
connReqWithKeys cr rk = case cr of
BCRInvitationUri crData e2eParams -> CRInvitationUri crData e2eParams
@@ -2748,6 +2830,117 @@ testRatchetSyncSimultaneous ps = do
disposeAgentClient bob
disposeAgentClient bob2
testRatchetSyncReplayedKey :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testRatchetSyncReplayedKey ps = withAgentClients2 $ \alice bob -> do
(aliceId, bobId, bob2) <- withSmpServerStoreMsgLogOn ps testPort $ \_ ->
setupDesynchronizedRatchet alice bob
("", "", DOWN _ _) <- nGet alice
("", "", DOWN _ _) <- nGet bob2
_ <- runRight $ synchronizeRatchet bob2 aliceId PQSupportOn False
Right pks <- withTransaction (store $ agentEnv bob2) (`getRatchetX3dhKeys` aliceId)
Right (SomeConn _ (DuplexConnection _ _ (sq :| _))) <- withTransaction (store $ agentEnv bob2) (`getConn` aliceId)
withSmpServerStoreMsgLogOn ps testPort $ \_ -> do
concurrently_
(getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP])
(getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP])
get alice =##> ratchetSyncP bobId RSOk
get bob2 =##> ratchetSyncP aliceId RSOk
withTransaction (store $ agentEnv alice) $ \db -> deleteRatchetKeyHashesExpired db 0 100
let keyMsg = AgentRatchetKey {agentVersion = currentSMPAgentVersion, e2eEncryption = CR.mkRcvE2ERatchetParams CR.currentE2EEncryptVersion pks, info = ""}
Right _ <- runReaderT (runExceptT $ sendAgentMessage bob2 sq SMP.noMsgFlags $ smpEncode keyMsg) (agentEnv bob2)
runRight_ $ exchangeGreetingsMsgIds alice bobId 10 bob2 aliceId 7
disposeAgentClient bob2
testRatchetSyncRepeated :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testRatchetSyncRepeated ps = withAgentClients2 $ \alice bob -> do
(aliceId, bobId, bob2) <- startRatchetSyncOffline ps alice bob
ConnectionStats {ratchetSyncState = rss2} <- runRight $ synchronizeRatchet bob2 aliceId PQSupportOn True
rss2 `shouldBe` RSStarted
withSmpServerStoreMsgLogOn ps testPort $ \_ -> do
concurrently_
(getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP])
(getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP])
runRight_ $ do
get alice =##> ratchetSyncP bobId RSAgreed
get alice =##> ratchetSyncP bobId RSOk
get bob2 =##> ratchetSyncP aliceId RSOk
msgId <- sendMessage alice bobId SMP.noMsgFlags "hello"
get alice ##> ("", bobId, SENT msgId)
get bob2 =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
ackMessage bob2 aliceId 8 Nothing
map fst <$> processedRatchetKeyHashes bob2 `shouldReturn` [aliceId, aliceId]
disposeAgentClient bob2
testRatchetSyncFailedKeyNotProcessed :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testRatchetSyncFailedKeyNotProcessed ps = withAgentClients2 $ \alice bob -> do
(aliceId, bobId, bob2) <- startRatchetSyncOffline ps alice bob
withTransaction (store $ agentEnv bob2) $ \db ->
DB.execute_ db "UPDATE ratchets SET x3dh_priv_key_1 = NULL"
withSmpServerStoreMsgLogOn ps testPort $ \_ ->
concurrently_
(getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP])
(getInAnyOrder bob2 [x3dhKeysNotFoundP aliceId, serverUpP])
map fst <$> processedRatchetKeyHashes alice `shouldReturn` [bobId]
processedRatchetKeyHashes bob2 `shouldReturn` []
disposeAgentClient bob2
where
x3dhKeysNotFoundP :: ConnId -> ATransmission -> Bool
x3dhKeysNotFoundP cId = \case
(_, cId', AEvt SAEConn (ERR (A.INTERNAL e))) -> cId' == cId && "SEX3dhKeysNotFound" `isPrefixOf` e
_ -> False
testRatchetSyncFailedRecreationNoReply :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testRatchetSyncFailedRecreationNoReply ps = withAgentClients2 $ \alice bob -> do
(_, bobId, bob2) <- startRatchetSyncOffline ps alice bob
aliceSndMsgs <- sndMessages alice
withTransaction (store $ agentEnv alice) $ \db ->
DB.execute_ db "CREATE TRIGGER fail_ratchet_insert BEFORE INSERT ON ratchets BEGIN SELECT RAISE(ABORT, 'ratchet insert failed'); END"
withSmpServerStoreMsgLogOn ps testPort $ \_ ->
concurrently_
(getInAnyOrder alice [ratchetInsertFailedP bobId, serverUpP])
(getInAnyOrder bob2 [serverUpP])
processedRatchetKeyHashes alice `shouldReturn` []
sndMessages alice `shouldReturn` aliceSndMsgs
disposeAgentClient bob2
where
ratchetInsertFailedP :: ConnId -> ATransmission -> Bool
ratchetInsertFailedP cId = \case
(_, cId', AEvt SAEConn (ERR (A.INTERNAL e))) -> cId' == cId && "ratchet insert failed" `isInfixOf` e
_ -> False
testRatchetSyncStartFailedNoKey :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testRatchetSyncStartFailedNoKey ps = withAgentClients2 $ \alice bob -> do
(aliceId, _, bob2) <- withSmpServerStoreMsgLogOn ps testPort $ \_ ->
setupDesynchronizedRatchet alice bob
bobSndMsgs <- sndMessages bob2
withTransaction (store $ agentEnv bob2) $ \db ->
DB.execute_ db "CREATE TRIGGER fail_ratchet_update BEFORE UPDATE ON ratchets BEGIN SELECT RAISE(ABORT, 'ratchet update failed'); END"
Left (A.INTERNAL e) <- runExceptT $ synchronizeRatchet bob2 aliceId PQSupportOff False
e `shouldContain` "ratchet update failed"
ConnectionStats {ratchetSyncState} <- runRight $ getConnectionServers bob2 aliceId
ratchetSyncState `shouldBe` RSRequired
withTransaction (store $ agentEnv bob2) (`DB.query_` "SELECT conn_id, pq_support FROM connections") `shouldReturn` [(aliceId, PQSupportOn)]
sndMessages bob2 `shouldReturn` bobSndMsgs
disposeAgentClient bob2
startRatchetSyncOffline :: HasCallStack => (ASrvTransport, AStoreType) -> AgentClient -> AgentClient -> IO (ConnId, ConnId, AgentClient)
startRatchetSyncOffline ps alice bob = do
(aliceId, bobId, bob2) <- withSmpServerStoreMsgLogOn ps testPort $ \_ ->
setupDesynchronizedRatchet alice bob
("", "", DOWN _ _) <- nGet alice
("", "", DOWN _ _) <- nGet bob2
ConnectionStats {ratchetSyncState} <- runRight $ synchronizeRatchet bob2 aliceId PQSupportOn False
ratchetSyncState `shouldBe` RSStarted
pure (aliceId, bobId, bob2)
processedRatchetKeyHashes :: AgentClient -> IO [(ConnId, ByteString)]
processedRatchetKeyHashes c = withTransaction (store $ agentEnv c) (`DB.query_` "SELECT conn_id, hash FROM processed_ratchet_key_hashes")
sndMessages :: AgentClient -> IO [(ConnId, Int64)]
sndMessages c = withTransaction (store $ agentEnv c) (`DB.query_` "SELECT conn_id, internal_id FROM snd_messages ORDER BY conn_id, internal_id")
getMsg :: AgentClient -> ConnId -> ExceptT AgentErrorType IO a -> ExceptT AgentErrorType IO a
getMsg c cId action = do
liftIO $ noMessages c "nothing should be delivered before GET"
@@ -3630,7 +3823,7 @@ testFastSwitchDeadOldServer :: HasCallStack => (ASrvTransport, AStoreType) -> IO
testFastSwitchDeadOldServer ps@(t, ASType qsType _) = do
let bServers = initAgentServers {smp = userServers [testSMPServer2]}
withSmpServerConfigOn t (cfgJ2QS qsType) testPort2 $ \_ ->
withAgent 1 agentCfg initAgentServers testDB $ \a ->
withAgent 1 aCfg initAgentServers testDB $ \a ->
withAgent 2 agentCfg bServers testDB2 $ \b -> do
(aId, bId) <- withSmpServerStoreLogOn ps testPort $ \_ -> runRight $ do
(aId, bId) <- makeConnection a b
@@ -3646,7 +3839,10 @@ testFastSwitchDeadOldServer ps@(t, ASType qsType _) = do
queuedReceived <- drainSwitchCompletedRcvMsg a bId "queued while down"
liftIO $ queuedReceived `shouldBe` True
drainSwitchCompleted b aId QDSnd
waitOneRcvQueue a bId
exchangeGreetingsMsgId 7 a bId b aId
where
aCfg = agentCfg {initialCleanupDelay = 10000, cleanupInterval = 10000, deleteErrorCount = 3}
-- drains switch and network events until the connection reports SPCompleted in the given direction,
-- tolerating DOWN/UP and intermediate phases (the old server is stopped mid-rotation)
@@ -3666,6 +3862,69 @@ drainSwitchCompletedRcvMsg c connId body = go False
(_, connId', AEvt SAEConn (Msg' mId _ body')) | connId' == connId && body' == body -> ackMessage c connId' mId Nothing >> go True
_ -> go seen
testFastSwitchDrainOldQueue :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testFastSwitchDrainOldQueue ps =
withSmpServers2 ps $ withAgentClients2 $ \a b -> runRight_ $ do
(aId, bId, aM1Id) <- endSwitchOnNewQueue a b
ackMessage a bId aM1Id Nothing
drainOldQueue a bId b aId
testFastSwitchDrainOldQueueRestart :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
testFastSwitchDrainOldQueueRestart ps =
withSmpServers2 ps $
withAgent 2 agentCfg initAgentServers testDB2 $ \b -> do
(aId, bId, _) <- withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight $ endSwitchOnNewQueue a b
withAgent 1 agentCfg initAgentServers testDB $ \a -> runRight_ $ do
subscribeConnection a bId
drainOldQueue a bId b aId
endSwitchOnNewQueue :: HasCallStack => AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId, AgentMsgId)
endSwitchOnNewQueue a b = do
(aId, bId) <- makeConnection a b
exchangeGreetings a bId b aId
liftIO $ setProtocolServers a 1 [noAuthSrvCfg testSMPServer2]
m1Id <- sendMessage b aId SMP.noMsgFlags "m1"
get b ##> ("", aId, SENT m1Id)
("", c, Msg' aM1Id _ "m1") <- get a
liftIO $ c `shouldBe` bId
m2Id <- sendMessage b aId SMP.noMsgFlags "m2"
get b ##> ("", aId, SENT m2Id)
_ <- switchConnectionAsync a "" bId
fastSwitchComplete a bId b aId
stats <- getConnectionServers a bId
liftIO $ rcvSwchStatuses' stats `shouldMatchList` [Nothing, Just RSReceivedQEND]
pure (aId, bId, aM1Id)
drainOldQueue :: HasCallStack => AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
drainOldQueue a bId b aId = do
("", c, MSG MsgMeta {recipient = (aM2Id, _)} _ "m2") <- get a
liftIO $ c `shouldBe` bId
ackMessage a bId aM2Id Nothing
waitOneRcvQueue a bId
bMsgId <- sendMessage b aId SMP.noMsgFlags "hello"
get b ##> ("", aId, SENT bMsgId)
("", c', MSG MsgMeta {recipient = (aMsgId, _)} _ "hello") <- get a
liftIO $ c' `shouldBe` bId
ackMessage a bId aMsgId Nothing
aMsgId' <- sendMessage a bId SMP.noMsgFlags "hello too"
get a ##> ("", bId, SENT aMsgId')
("", c'', MSG MsgMeta {recipient = (bMsgId', _)} _ "hello too") <- get b
liftIO $ c'' `shouldBe` aId
ackMessage b aId bMsgId' Nothing
liftIO $ noMessages a "nothing else should be delivered to alice"
liftIO $ noMessages b "nothing else should be delivered to bob"
waitOneRcvQueue :: HasCallStack => AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
waitOneRcvQueue c connId = go (300 :: Int)
where
go n = do
stats <- getConnectionServers c connId
case rcvSwchStatuses' stats of
[Nothing] -> pure ()
statuses
| n > 0 -> liftIO (threadDelay 100000) >> go (n - 1)
| otherwise -> liftIO $ expectationFailure $ "old receive queue was not deleted: " <> show statuses
phaseRcv :: AgentClient -> ByteString -> SwitchPhase -> [Maybe RcvSwitchStatus] -> ExceptT AgentErrorType IO ()
phaseRcv c connId p swchStatuses = phase c connId QDRcv p (\stats -> rcvSwchStatuses' stats `shouldMatchList` swchStatuses)
@@ -4762,7 +5021,7 @@ testServerQueueInfo = do
Just srvMsgId <- checkMsgQ bob aliceId 1
get bob =##> \case
("", c, MSG MsgMeta {integrity = MsgOk, broker = (smId, _), recipient = (mId, _), pqEncryption = PQEncOn} _ "hello") ->
c == aliceId && decodeLatin1 (B64.encode smId) == srvMsgId && mId == msgId
c == aliceId && decodeLatin1 (B64.encode $ SMP.unMsgId smId) == srvMsgId && mId == msgId
_ -> False
ackMessage bob aliceId msgId Nothing
liftIO $ threadDelay 200000
@@ -4793,9 +5052,9 @@ testServerQueueInfo = do
get bob =##> \case ("", c, Msg' mId PQEncOn "hello 4") -> c == aliceId && mId == msgId4; _ -> False
ackMessage bob aliceId msgId4 Nothing
liftIO $ threadDelay 200000
Just _ <- checkMsgQ bob aliceId 1 -- the one that did not fit now accepted
get alice ##> ("", bobId, QCONT)
get alice ##> ("", bobId, SENT msgId5)
Just _ <- checkMsgQ bob aliceId 1 -- the one that did not fit now accepted
liftIO $ threadDelay 200000
Just _srvMsgId <- checkQ bob aliceId True (Just QNoSub) 1 (Just MTMessage)
get bob =##> \case ("", c, Msg' mId PQEncOn "hello: quota exceeded") -> c == aliceId && mId == msgId5 + 1; _ -> False
+13 -13
View File
@@ -230,7 +230,7 @@ testNotificationToken apns = do
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a tkn nonce verification
NTActive <- checkNtfToken a tkn
deleteNtfToken a tkn
@@ -254,7 +254,7 @@ testNtfTokenRepeatRegistration apns = do
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
NTRegistered <- registerNtfToken a tkn NMPeriodic
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}} <-
getMockNotification apns tkn
@@ -273,14 +273,14 @@ testNtfTokenSecondRegistration apns =
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a tkn nonce verification
NTRegistered <- registerNtfToken a' tkn NMPeriodic
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}} <-
getMockNotification apns tkn
verification' <- ntfData' .-> "verification"
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
nonce' <- C.unsafeCbNonce <$> ntfData' .-> "nonce"
-- at this point the first token is still active
NTActive <- checkNtfToken a tkn
@@ -311,7 +311,7 @@ testNtfTokenServerRestart t apns = do
-- so that repeat verification happens without restarting the clients, when notification arrives
withNtfServer t $ runRight_ $ do
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a' tkn nonce verification
NTActive <- checkNtfToken a' tkn
pure ()
@@ -327,7 +327,7 @@ testNtfTokenServerRestartReverify t apns = do
pure ntfData
runRight_ $ do
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
Left (BROKER _ (NETWORK _)) <- tryE $ verifyNtfToken a tkn nonce verification
pure ()
threadDelay 1500000
@@ -348,7 +348,7 @@ testNtfTokenServerRestartReverifyTimeout t apns = do
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a tkn nonce verification
pure (nonce, verification)
-- this emulates the situation when server verified token but the client did not receive the response
@@ -393,7 +393,7 @@ testNtfTokenServerRestartReregister t apns = do
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a' tkn nonce verification
NTActive <- checkNtfToken a' tkn
pure ()
@@ -428,7 +428,7 @@ testNtfTokenServerRestartReregisterTimeout t apns = do
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a' tkn nonce verification
NTActive <- checkNtfToken a' tkn
pure ()
@@ -450,7 +450,7 @@ testNtfTokenMultipleServers t apns = do
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken a tkn nonce verification
NTActive <- checkNtfToken a tkn
-- shut down the "other" server
@@ -566,7 +566,7 @@ testNotificationSubscriptionExistingConnection apns baseId alice@AgentClient {ag
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
getMockNotification apns tkn
verification <- ntfData .-> "verification"
vNonce <- C.cbNonce <$> ntfData .-> "nonce"
vNonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
verifyNtfToken alice tkn vNonce verification
NTActive <- checkNtfToken alice tkn
-- send message
@@ -657,7 +657,7 @@ registerTestToken a token mode apns = do
Just APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}} <-
timeout 1000000 $ getMockNotification apns tkn
verification' <- ntfData' .-> "verification"
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
nonce' <- C.unsafeCbNonce <$> ntfData' .-> "nonce"
verifyNtfToken a tkn nonce' verification'
NTActive <- checkNtfToken a tkn
pure tkn
@@ -1035,7 +1035,7 @@ messageNotification apns tkn = do
500000 `timeout` getMockNotification apns tkn >>= \case
Nothing -> error "no notification"
Just APNSMockRequest {notification = APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData}} -> do
nonce <- C.cbNonce <$> ntfData .-> "nonce"
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
message <- ntfData .-> "message"
pure (nonce, message)
_ -> error "bad notification"
+44 -4
View File
@@ -20,12 +20,13 @@ import Control.Concurrent.Async (concurrently_)
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Exception (SomeException)
import Control.Monad (replicateM_)
import Control.Monad (forM_, replicateM_)
import Control.Monad.Trans.Except
import Crypto.Random (ChaChaDRG)
import Data.ByteArray (ScrubbedBytes)
import Data.ByteString.Char8 (ByteString)
import Data.List (isInfixOf)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time
@@ -51,6 +52,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import Simplex.Messaging.Crypto.Ratchet (pattern IKPQOn)
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding (Encoding (..))
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Protocol (EntityId (..), QueueMode (..), SubscriptionMode (..), pattern VersionSMPC)
import qualified Simplex.Messaging.Protocol as SMP
@@ -135,6 +137,8 @@ storeTests = do
testCreateRcvMsg
testCreateSndMsg
testCreateRcvAndSndMsgs
describe "deleteRatchetKeyHashesExpired" testDeleteRatchetKeyHashesExpired
it "should keep only the newest skipped message keys" testGetSkippedMsgKeys
describe "Work items" $ do
it "should getPendingQueueMsg" testGetPendingQueueMsg
it "should getPendingServerCommand" testGetPendingServerCommand
@@ -509,7 +513,7 @@ hw = encodeUtf8 "Hello world!"
ts :: UTCTime
ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> BrokerId -> MsgHash -> RcvMsgData
mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> ByteString -> MsgHash -> RcvMsgData
mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
RcvMsgData
{ internalRcvId,
@@ -518,7 +522,7 @@ mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
{ integrity = MsgOk,
recipient = (unId internalId, ts),
sndMsgId = externalSndId,
broker = (brokerId, ts),
broker = (SMP.unsafeMsgId $ mconcat (replicate 24 brokerId), ts),
pqEncryption = CR.PQEncOn
},
msgType = AM_A_MSG_,
@@ -599,6 +603,42 @@ testCreateRcvAndSndMsgs =
testCreateSndMsg_ db "snd_hash_1" connId sq $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
testCreateSndMsg_ db "snd_hash_2" connId sq $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
testDeleteRatchetKeyHashesExpired :: SpecWith DBStore
testDeleteRatchetKeyHashesExpired =
it "should delete expired ratchet key hashes except the newest in each connection" . withStoreTransaction $ \db -> do
g <- C.newRandom
Right connId <- createNewConn db g cData1 {connId = ""} SCMInvitation
Right connId' <- createNewConn db g cData1 {connId = ""} SCMContact
let hashes = ["h1", "h2", "h3", "h4", "h5", "h6"]
forM_ hashes $ addProcessedRatchetKeyHash db connId
forM_ (take 4 hashes) $ addProcessedRatchetKeyHash db connId'
deleteRatchetKeyHashesExpired db 86400 4
mapM (checkRatchetKeyHashExists db connId) hashes `shouldReturn` replicate 6 True
deleteRatchetKeyHashesExpired db 0 4
mapM (checkRatchetKeyHashExists db connId) hashes `shouldReturn` [False, False, True, True, True, True]
mapM (checkRatchetKeyHashExists db connId') (take 4 hashes) `shouldReturn` replicate 4 True
testGetSkippedMsgKeys :: DBStore -> Expectation
testGetSkippedMsgKeys st = do
g <- C.newRandom
withTransaction st $ \db -> do
Right connId <- createNewConn db g cData1 {connId = ""} SCMInvitation
Right connId' <- createNewConn db g cData1 {connId = ""} SCMInvitation
createSkippedKeys db connId'
createSkippedKeys db connId
M.map M.keys <$> getSkippedMsgKeys db connId 4
`shouldReturn` M.singleton (C.Key "header_key") [1, 2, 3, 4]
getMsgNs db connId `shouldReturn` [1 .. 4]
getMsgNs db connId' `shouldReturn` [1 .. 10]
where
createSkippedKeys :: DB.Connection -> ConnId -> IO ()
createSkippedKeys db connId = do
DB.execute db "INSERT INTO ratchets (conn_id) VALUES (?)" (Only connId)
forM_ ([10, 9 .. 1] :: [Int]) $ \msgN ->
DB.execute db "INSERT INTO skipped_messages (conn_id, header_key, msg_n, msg_key) VALUES (?, ?, ?, ?)" (connId, "header_key" :: ByteString, msgN, smpEncode ("key" :: ByteString, "iv" :: ByteString))
getMsgNs :: DB.Connection -> ConnId -> IO [Int]
getMsgNs db connId = map fromOnly <$> DB.query db "SELECT msg_n FROM skipped_messages WHERE conn_id = ? ORDER BY msg_n" (Only connId)
testCloseReopenStore :: IO ()
testCloseReopenStore = do
st <- createStore'
@@ -648,7 +688,7 @@ hasMigrations :: DBStore -> Expectation
hasMigrations st = getMigrations st `shouldReturn` True
errorGettingMigrations :: DBStore -> Expectation
errorGettingMigrations st = getMigrations st `shouldThrow` \(e :: SomeException) -> "ErrorMisuse" `isInfixOf` show e
errorGettingMigrations st = getMigrations st `shouldThrow` \(e :: SomeException) -> "withTransaction: database closed" `isInfixOf` show e
testGetPendingQueueMsg :: DBStore -> Expectation
testGetPendingQueueMsg st = do
+36 -1
View File
@@ -48,6 +48,8 @@ schemaDumpTest = do
it "verify strict tables" testVerifyStrict
it "should NOT create user record for new database" testUsersMigrationNew
it "should create user record for old database" testUsersMigrationOld
it "should remove duplicate ratchet key hashes before adding unique index" testRatchetKeyHashesUniqueMigration
it "should clear received_qend switch status on downgrade" testRcvSwitchStatusDowngrade
testVerifySchemaDump :: IO ()
testVerifySchemaDump = do
@@ -114,12 +116,45 @@ testUsersMigrationOld = do
`shouldReturn` ([Only (1 :: Int)])
closeDBStore st'
testRatchetKeyHashesUniqueMigration :: IO ()
testRatchetKeyHashesUniqueMigration = do
let beforeUnique = takeWhile (("m20260929_ratchet_indexes" /=) . name) appMigrations
Right st <- createDBStore (DBOpts testDB [] "" False True TQOff) beforeUnique (MigrationConfig MCError Nothing)
withTransaction' st $ \db -> do
SQL.execute_ db "INSERT INTO users (user_id) VALUES (1)"
SQL.execute_ db "INSERT INTO connections (conn_id, conn_mode, user_id) VALUES (x'01', 'INV', 1), (x'02', 'INV', 1)"
SQL.execute_ db "INSERT INTO processed_ratchet_key_hashes (conn_id, hash) VALUES (x'01', x'aa'), (x'01', x'aa'), (x'01', x'bb'), (x'02', x'aa'), (x'01', x'aa')"
closeDBStore st
Right st' <- createDBStore (DBOpts testDB [] "" False True TQOff) appMigrations (MigrationConfig MCYesUp Nothing)
withTransaction' st' (`SQL.query_` "SELECT processed_ratchet_key_hash_id FROM processed_ratchet_key_hashes ORDER BY processed_ratchet_key_hash_id")
`shouldReturn` [Only (1 :: Int), Only 3, Only 4]
closeDBStore st'
testRcvSwitchStatusDowngrade :: IO ()
testRcvSwitchStatusDowngrade = do
Right st <- createDBStore (DBOpts testDB [] "" False True TQOff) appMigrations (MigrationConfig MCError Nothing)
withTransaction' st $ \db -> do
SQL.execute_ db "INSERT INTO users (user_id) VALUES (1)"
SQL.execute_ db "INSERT INTO connections (conn_id, conn_mode, user_id) VALUES (x'01', 'INV', 1)"
SQL.execute_ db "INSERT INTO servers (host, port, key_hash) VALUES ('localhost', '5001', x'00')"
SQL.execute_ db "INSERT INTO rcv_queues (host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, snd_id, status, rcv_queue_id, rcv_primary, switch_status) VALUES ('localhost', '5001', x'01', x'01', x'00', x'00', x'00', x'01', 'active', 1, 0, 'received_qend'), ('localhost', '5001', x'02', x'01', x'00', x'00', x'00', x'02', 'active', 2, 1, 'sending_qadd')"
closeDBStore st
let beforeRcvSwitchStatus = takeWhile (("m20261008_rcv_switch_status" /=) . name) appMigrations
Right st' <- createDBStore (DBOpts testDB [] "" False True TQOff) beforeRcvSwitchStatus (MigrationConfig MCYesUpDown Nothing)
withTransaction' st' (`SQL.query_` "SELECT switch_status FROM rcv_queues ORDER BY rcv_queue_id")
`shouldReturn` [Only (Nothing :: Maybe Text), Only (Just "sending_qadd")]
closeDBStore st'
skipComparisonForDownMigrations :: [String]
skipComparisonForDownMigrations =
[ -- on down migration idx_messages_internal_snd_id_ts index moves down to the end of the file
"m20230814_indexes",
-- snd_secure and last_broker_ts columns swap order on down migration
"m20250322_short_links"
"m20250322_short_links",
-- on down migration idx_processed_ratchet_key_hashes_hash index moves down to the end of the file
"m20260929_ratchet_indexes",
-- sqlite_sequence moves after commands table after VACUUM
"m20261005_commands_autoincrement"
]
getSchema :: FilePath -> FilePath -> IO String
+29 -5
View File
@@ -14,11 +14,13 @@ import Control.Monad
import Crypto.Random (ChaChaDRG)
import qualified Data.ByteString as B
import Data.ByteString.Char8 (ByteString)
import Data.ByteString.Unsafe (unsafeUseAsCString)
import qualified Data.List.NonEmpty as L
import Data.Time.Clock.System (SystemTime, getSystemTime)
import qualified Data.X509 as X
import qualified Data.X509.CertificateStore as XS
import qualified Data.X509.File as XF
import Foreign.Ptr (plusPtr)
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
@@ -40,6 +42,8 @@ batchingTests = do
it "should batch subscription responses with message" testBatchSubResponses
it "should break on message that does not fit" testClientBatchWithMessage
it "should break on large message" testClientBatchWithLargeMessage
describe "tDecodeServer" $
it "should copy IDs out of received block" testDecodeServerCopiesIds
testBatchSubscriptions :: IO ()
testBatchSubscriptions = do
@@ -166,6 +170,26 @@ testClientBatchWithLargeMessage = do
(length rs1', length rs2') `shouldBe` (75, 135)
all lenOk [s1', s2'] `shouldBe` True
testDecodeServerCopiesIds :: IO ()
testDecodeServerCopiesIds = do
sessId <- atomically . C.randomBytes 32 =<< C.newRandom
subs <- replicateM 2 $ randomSUB sessId
let thParams = testTHandleParams sessId
[TBTransmissions s 2 _] <- pure $ batchTransmissions thParams $ L.fromList subs
forM_ (tParse thParams s) $ \t -> do
let (corrId, entId, _) = tDecodeClient @SMPVersion @ErrorType @Cmd thParams t
Right (_, _, (corrId', entId', _)) <- pure $ tDecodeServer @SMPVersion @ErrorType @Cmd (testTHandleParams sessId) t
(corrId', entId') `shouldBe` (corrId, entId)
sharesBuffer s (bs corrId) `shouldReturn` True
sharesBuffer s (unEntityId entId) `shouldReturn` True
sharesBuffer s (bs corrId') `shouldReturn` False
sharesBuffer s (unEntityId entId') `shouldReturn` False
sharesBuffer :: ByteString -> ByteString -> IO Bool
sharesBuffer block s =
unsafeUseAsCString block $ \blockPtr -> unsafeUseAsCString s $ \ptr ->
pure $ ptr >= blockPtr && ptr < blockPtr `plusPtr` B.length block
testClientStub :: IO (ProtocolClient SMPVersion ErrorType BrokerMsg)
testClientStub = do
g <- C.newRandom
@@ -183,7 +207,7 @@ randomSUB sessId = do
thAuth_ <- testTHandleAuth g rKey
let thParams = testTHandleParams sessId
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, EntityId rId, Cmd SRecipient SUB)
pure $ (,tToSend) <$> authTransmission thAuth_ True (Just rpKey) nonce tForAuth
pure $ (,tToSend) <$> authTransmission thAuth_ True (Just rpKey) (Just nonce) tForAuth
randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg)
randomSUBCmd c = do
@@ -202,7 +226,7 @@ randomNMSGCmd :: SystemTime -> IO (Transmission BrokerMsg)
randomNMSGCmd ts = do
g <- C.newRandom
nId <- atomically $ C.randomBytes 24 g
msgId <- atomically $ C.randomBytes 24 g
msgId <- atomically $ randomMsgId 24 g
(k, pk) <- atomically $ C.generateKeyPair g
nonce <- atomically $ C.randomCbNonce g
let msgMeta = NMsgMeta {msgId, msgTs = ts}
@@ -221,7 +245,7 @@ randomMSG = do
g <- C.newRandom
corrId <- atomically $ C.randomBytes 24 g
rId <- atomically $ C.randomBytes 24 g
msgId <- atomically $ C.randomBytes 24 g
msgId <- atomically $ randomMsgId 24 g
msg <- atomically $ C.randomBytes maxMessageLength g
pure (CorrId corrId, EntityId rId, MSG RcvMessage {msgId, msgBody = EncRcvMsgBody msg})
@@ -235,9 +259,9 @@ randomSEND sessId len = do
msg <- atomically $ C.randomBytes len g
let thParams = testTHandleParams sessId
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, EntityId sId, Cmd SSender $ SEND noMsgFlags msg)
pure $ (,tToSend) <$> authTransmission thAuth_ False (Just spKey) nonce tForAuth
pure $ (,tToSend) <$> authTransmission thAuth_ False (Just spKey) (Just nonce) tForAuth
testTHandleParams :: ByteString -> THandleParams SMPVersion 'TClient
testTHandleParams :: ByteString -> THandleParams SMPVersion p
testTHandleParams sessionId =
THandleParams
{ sessionId,
+370
View File
@@ -0,0 +1,370 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
module CoreTests.EthCryptoTests (ethCryptoTests) where
import Control.Concurrent.STM (TVar, atomically)
import Control.Monad (forM_)
import Crypto.Number.Serialize (i2osp)
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson as J
import qualified Data.ByteArray as BA
import qualified Data.ByteArray.Encoding as BAE
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import Data.Char (toLower)
import Data.Either (isLeft, isRight)
import Data.FileEmbed (embedFile)
import Data.List (elemIndex, foldl', nub)
import qualified Data.Map.Strict as M
import Data.Maybe (fromJust, isNothing, listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Word (Word32)
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.BIP32 as B32
import qualified Simplex.Messaging.Crypto.BIP39 as B39
import Simplex.Messaging.Crypto.BIP39.English (englishWordList)
import Simplex.Messaging.Crypto.BIP44
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
import Simplex.Messaging.Eth.Address
import Simplex.Messaging.Eth.RLP
import Simplex.Messaging.Eth.Transaction
import Simplex.Messaging.Util ((<$$>))
import Test.Hspec hiding (fit, it)
import Util
ethCryptoTests :: Spec
ethCryptoTests = do
g <- runIO C.newRandom
describe "Keccak-256" keccakTests
describe "secp256k1" $ secp256k1Tests g
describe "BIP-39" $ bip39Tests g
describe "BIP-32" $ bip32Tests g
describe "BIP-44 derivation" $ derivationTests g
describe "EIP-55 addresses" eip55Tests
describe "RLP" rlpTests
describe "recoverable signing" $ signingTests g
describe "EIP-1559 transactions" $ transactionTests g
-- helpers
hx :: BA.ByteArray a => ByteString -> a
hx s = either (const $ error $ "bad hex literal: " <> BC.unpack s) id $ BAE.convertFromBase BAE.Base16 s
toHex :: BA.ByteArrayAccess a => a -> ByteString
toHex = BAE.convertToBase BAE.Base16
right :: Either String a -> a
right = either (error . ("unexpected Left: " <>)) id
xkHex :: B32.ExtendedKey -> (ByteString, ByteString)
xkHex xk = (toHex . S.unPrivateKey $ B32.xkKey xk, toHex $ B32.xkChainCode xk)
-- | Key and chain code of a base58check @xprv@: 4 version, 1 depth, 4 fingerprint, 4 child, 32 chain code, 0x00 and 32 key bytes, 4 checksum.
xprvHex :: ByteString -> (ByteString, ByteString)
xprvHex s
| B.length payload /= 78 || B.index payload 45 /= 0 || checksum /= B.take 4 (C.sha256Hash $ C.sha256Hash payload) = error $ "bad xprv: " <> BC.unpack s
| otherwise = (toHex $ B.drop 46 payload, toHex $ B.take 32 $ B.drop 13 payload)
where
(payload, checksum) = B.splitAt 78 $ base58Decode s
base58Decode :: ByteString -> ByteString
base58Decode s = B.replicate (B.length zeros) 0 <> i2osp n
where
(zeros, digits) = BC.span (== '1') s
n = foldl' (\acc c -> acc * 58 + fromIntegral (digit c)) (0 :: Integer) (BC.unpack digits)
digit c = fromJust $ elemIndex c "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
-- fixtures
-- https://github.com/trezor/python-mnemonic/blob/master/vectors.json
bip39VectorsFile :: ByteString
bip39VectorsFile = $(embedFile "tests/fixtures/bip39-vectors.json")
-- https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
bip32SpecFile :: ByteString
bip32SpecFile = $(embedFile "tests/fixtures/bip-0032.mediawiki")
-- | Entropy, phrase, seed with the passphrase @TREZOR@, and master @xprv@ for each English vector.
bip39Vectors :: [(ByteString, ByteString, ByteString, ByteString)]
bip39Vectors = map row $ (right (J.eitherDecodeStrict bip39VectorsFile) :: M.Map Text [[Text]]) M.! "english"
where
row [ent, phrase, seed, xprv] = (encodeUtf8 ent, encodeUtf8 phrase, encodeUtf8 seed, encodeUtf8 xprv)
row r = error $ "unexpected vector: " <> show r
bip32Sections :: [(ByteString, [ByteString])]
bip32Sections = go $ dropWhile (not . isVector) (BC.lines bip32SpecFile)
where
isVector = B.isPrefixOf "===Test vector"
go (h : rest) | isVector h = let (body, rest') = break (B.isPrefixOf "==") rest in (h, body) : go rest'
go _ = []
-- | Vectors 1 to 4: name, seed, and the chains with their @xprv@. Vector 5 has no seed.
bip32Vectors :: [(String, ByteString, [(String, [Word32], ByteString)])]
bip32Vectors = [(BC.unpack $ BC.filter (/= '=') h, seed, chains body) | (h, body) <- bip32Sections, Just seed <- [listToMaybe $ prefixed "Seed (hex): " body]]
where
chains (l : ls) | Just c <- B.stripPrefix "* Chain " l, Just prv <- listToMaybe $ prefixed "** ext prv: " (take 2 ls) = (T.unpack $ chain c, path $ chain c, prv) : chains ls
chains (_ : ls) = chains ls
chains [] = []
chain = T.replace "<sub>H</sub>" "'" . decodeLatin1
path = map component . drop 1 . T.splitOn "/"
component s = maybe (index s) (B32.hardened . index) $ T.stripSuffix "'" s
index = read . T.unpack
-- | The vector 5 keys outside @[1, n-1]@; its other entries are malformed serializations, which this library does not parse.
bip32InvalidKeys :: [(String, ByteString)]
bip32InvalidKeys =
[ (BC.unpack reason, xprv)
| (h, body) <- bip32Sections,
h == "===Test vector 5===",
e <- prefixed "* " body,
let (xprv, rest) = BC.break (== ' ') e
reason = BC.takeWhile (/= ')') $ B.drop 2 rest,
"private key" `B.isPrefixOf` reason
]
prefixed :: ByteString -> [ByteString] -> [ByteString]
prefixed p = foldr (\l acc -> maybe acc (: acc) $ B.stripPrefix p l) []
keccakTests :: Spec
keccakTests = do
it "hashes the empty string" $
toHex (C.keccak256 "") `shouldBe` "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
it "hashes abc" $
toHex (C.keccak256 "abc") `shouldBe` "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"
secp256k1Tests :: TVar ChaChaDRG -> Spec
secp256k1Tests g = do
it "derives the known address for a known key" $
(strEncode <$> addressFromPrivateKey g testKey) `shouldReturn` "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"
it "serializes a public key in both SEC1 forms" $ do
pk <- S.secp256k1PublicKey g testKey
comp <- S.serializePublicKey S.Compressed pk
uncomp <- S.serializePublicKey S.Uncompressed pk
B.length comp `shouldBe` 33
B.length uncomp `shouldBe` 65
B.head uncomp `shouldBe` 0x04
-- both forms contain the same x coordinate, and the compressed prefix encodes the parity of y
B.take 32 (B.drop 1 uncomp) `shouldBe` B.drop 1 comp
B.head comp `shouldBe` (if odd (B.last uncomp) then 0x03 else 0x02)
forM_ bip32InvalidKeys $ \(reason, xprv) ->
it ("rejects the BIP-32 vector 5 " <> reason) $
isLeft (S.mkPrivateKey (hx . fst $ xprvHex xprv)) `shouldBe` True
it "rejects a short private key" $
isLeft (S.mkPrivateKey (BA.replicate 31 1)) `shouldBe` True
it "adds a tweak to a private key" $
(fmap (toHex . S.unPrivateKey) <$> S.privateKeyTweakAdd testKey (BA.replicate 31 0 <> BA.singleton 1))
`shouldReturn` Just "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362319"
it "returns Nothing for a tweak that makes the key zero" $
(isNothing <$> S.privateKeyTweakAdd testKey (hx "b3f77c596efd6c829dceb8e4a2449df9bc5db3853ec62710db698e7291001e29"))
`shouldReturn` True
it "returns Nothing for a tweak that is not 32 bytes" $
(isNothing <$> S.privateKeyTweakAdd testKey (BA.replicate 31 1)) `shouldReturn` True
where
testKey = right $ S.mkPrivateKey (hx "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318")
bip39Tests :: TVar ChaChaDRG -> Spec
bip39Tests g = do
describe "official English vectors" $
forM_ (zip [0 :: Int ..] bip39Vectors) $ \(i, (entHex, phrase, seedHex, xprv)) ->
it ("vector " <> show i) $ do
let ent = right $ B39.mkEntropy (hx entHex)
seed = B39.entropySeed ent "TREZOR"
B39.entropyPhrase ent `shouldBe` phrase
B39.parsePhrase (decodeLatin1 phrase) `shouldBe` Right ent
toHex seed `shouldBe` seedHex
xkHex (right $ B32.masterKey seed) `shouldBe` xprvHex xprv
it "embeds the upstream wordlist, unchanged" $
-- sha256 of bitcoin/bips/bip-0039/english.txt
C.sha256Hash (BC.unlines englishWordList)
`shouldBe` hx "2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda"
it "embeds the upstream test vectors, unchanged" $ do
C.sha256Hash bip39VectorsFile `shouldBe` hx "fa3b937b7cff9c9b8ecd3aa011faeb8d6dd67993174b72326e83f4de8fdb30f8"
length bip39Vectors `shouldBe` 24
it "rejects a bad checksum" $
B39.parsePhrase "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
`shouldSatisfy` isLeft
it "rejects a word outside the list" $
B39.parsePhrase "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon simplex"
`shouldSatisfy` isLeft
it "rejects an empty phrase, counting the words" $
B39.parsePhrase "" `shouldBe` Left "Failed reading: mnemonic: expected 12, 15, 18, 21 or 24 words, got 0"
it "rejects a word with trailing punctuation, naming it" $
B39.parsePhrase "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about."
`shouldBe` Left "Failed reading: mnemonic: not in wordlist"
it "rejects a wrong word count" $
B39.parsePhrase "abandon abandon about" `shouldSatisfy` isLeft
it "accepts a capitalised phrase and normalises it" $
(B39.entropyPhrase <$> B39.parsePhrase "Abandon ABANDON abandon abandon abandon abandon abandon abandon abandon abandon abandon About")
`shouldBe` Right canonicalPhrase
it "accepts extra whitespace" $
B39.parsePhrase " abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about "
`shouldBe` Right canonicalEntropy
it "accepts Unicode whitespace between words" $ do
let spaced sep = T.intercalate sep . T.words $ decodeLatin1 canonicalPhrase
B39.parsePhrase (spaced "\x00A0") `shouldBe` Right canonicalEntropy
B39.parsePhrase (spaced "\x3000") `shouldBe` Right canonicalEntropy
it "rejects an invalid entropy size" $
B39.mkEntropy (BA.replicate 17 0) `shouldSatisfy` isLeft
it "generates entropy whose phrase parses back" $
forM_ (zip [minBound .. maxBound] [12, 15, 18, 21, 24]) $ \(s, n) -> do
ent <- atomically $ B39.randomEntropy s g
length (BC.words $ B39.entropyPhrase ent) `shouldBe` n
B39.parsePhrase (decodeLatin1 $ B39.entropyPhrase ent) `shouldBe` Right ent
canonicalPhrase :: ByteString
canonicalPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
canonicalEntropy :: B39.WalletEntropy
canonicalEntropy = right $ B39.parsePhrase (decodeLatin1 canonicalPhrase)
bip32Tests :: TVar ChaChaDRG -> Spec
bip32Tests g = do
it "embeds the upstream specification, unchanged" $ do
C.sha256Hash bip32SpecFile `shouldBe` hx "e5e00a8289db2f681052cf24a745320afc225e66b25d1e489a7c884d2fc7f11f"
map (\(name, _, chains) -> (name, length chains)) bip32Vectors `shouldBe` [("Test vector 1", 6), ("Test vector 2", 6), ("Test vector 3", 2), ("Test vector 4", 3)]
map fst bip32InvalidKeys `shouldBe` ["private key 0 not in 1..n-1", "private key n not in 1..n-1"]
forM_ bip32Vectors $ \(name, seedHex, chains) ->
describe name $
forM_ chains $ \(chain, path, xprv) ->
it chain $ do
xk <- right <$> B32.derivePath g (right $ B32.masterKey (hx seedHex)) path
xkHex xk `shouldBe` xprvHex xprv
it "rejects a seed shorter than 16 bytes" $
isLeft (B32.masterKey (BA.replicate 15 1)) `shouldBe` True
it "rejects a seed longer than 64 bytes" $
isLeft (B32.masterKey (BA.replicate 65 1)) `shouldBe` True
it "renders a path" $
B32.renderPath [B32.hardened 44, B32.hardened 60, B32.hardened 0, 0, 0] `shouldBe` "m/44'/60'/0'/0/0"
canonicalMaster :: B32.ExtendedKey
canonicalMaster = right $ B32.masterKey (B39.entropySeed canonicalEntropy "")
derivationTests :: TVar ChaChaDRG -> Spec
derivationTests g = do
it "derives the standard BIP-39 seed" $
toHex seed
`shouldBe` "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
forM_ [(0, "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"), (1, "0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265"), (2, "0x07B5FdfEB4E11826D233403Fe8Db0611CCF4c231")] $ \(i, a) ->
it ("derives the account " <> show i <> " address") $
(strEncode <$> addrAt i) `shouldReturn` a
it "derives distinct addresses for accounts 0 to 4" $
mapM addrAt [0 .. 4] >>= (`shouldSatisfy` \as -> length as == length (nub as))
it "derives an account key together with its address" $ do
let path = bip44Path Ethereum $ account 0
(xk, a) <- right <$> deriveAddress g canonicalMaster path
xk' <- right <$> B32.derivePath g canonicalMaster path
xkHex xk `shouldBe` xkHex xk'
addressFromPrivateKey g (B32.xkKey xk) `shouldReturn` a
it "renders the Ethereum path of an account" $
B32.renderPath (bip44Path Ethereum $ account 7) `shouldBe` "m/44'/60'/7'/0/0"
it "rejects an account index at or above 2^31" $ do
mkAccountIndex 0x7fffffff `shouldSatisfy` isRight
mkAccountIndex 0x80000000 `shouldSatisfy` isLeft
where
seed = B39.entropySeed canonicalEntropy ""
account = right . mkAccountIndex
addrAt i = snd . right <$> deriveAddress g canonicalMaster (bip44Path Ethereum $ account i)
eip55Tests :: Spec
eip55Tests = do
describe "spec vectors round-trip" $
forM_ specAddresses $ \a ->
it (BC.unpack a) $
strEncode (right $ strDecode @Address a) `shouldBe` a
it "accepts an all-lowercase address" $
strDecode @Address "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" `shouldSatisfy` isRight
it "accepts an all-uppercase address" $
strDecode @Address "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED" `shouldSatisfy` isRight
it "accepts an address without the 0x prefix" $
strDecode @Address "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" `shouldSatisfy` isRight
it "rejects a bad EIP-55 checksum" $
strDecode @Address "0x5aAeb6053f3E94C9b9A09f33669435E7Ef1BeAed" `shouldSatisfy` isLeft
it "rejects the wrong length" $
strDecode @Address "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAe" `shouldSatisfy` isLeft
it "rejects non-hex characters" $
strDecode @Address "0xZaAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
`shouldBe` Left "Failed reading: address: expected 40 hex digits, got 0"
it "round-trips every byte value through the checksummed form" $
forM_ everyByteAddresses $ \a ->
strDecode (strEncode a) `shouldBe` Right a
it "round-trips every byte value through the lowercase form" $
forM_ everyByteAddresses $ \a ->
strDecode (BC.map toLower (strEncode a)) `shouldBe` Right a
where
-- 13 x 20 = 260 bytes, so every value 0x00..0xff appears at least once
everyByteAddresses =
[ right . strDecode @Address . ("0x" <>) . toHex . B.pack $
[fromIntegral ((i * 20 + j) `mod` 256) | j <- [0 .. 19 :: Int]]
| i <- [0 .. 12 :: Int]
]
specAddresses =
[ "0x52908400098527886E0F7030069857D2E4169EE7",
"0x8617E340B3D01FA5F11F306F4090FD50E238070D",
"0xde709f2102306220921060314715629080e2fb77",
"0x27b1fdb04752bbc536007a920d24acb045561c26",
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
]
rlpTests :: Spec
rlpTests = do
forM_ rlpVectors $ \(name, item, expected) ->
it ("encodes " <> name) $ toHex (rlpEncode item) `shouldBe` expected
it "encodes scalars without leading zeros" $ do
toHex (rlpEncode $ scalarItem $ B.replicate 32 0) `shouldBe` "80"
toHex (rlpEncode $ scalarItem $ hx "000f") `shouldBe` "0f"
toHex (rlpEncode $ scalarItem $ hx "000400") `shouldBe` "820400"
where
rlpVectors =
[ ("the empty string", RLPBytes "", "80"),
("the empty list", RLPList [], "c0"),
("dog", RLPBytes "dog", "83646f67"),
("[cat, dog]", RLPList [RLPBytes "cat", RLPBytes "dog"], "c88363617483646f67"),
("the set-theoretic three", RLPList [RLPList [], RLPList [RLPList []], RLPList [RLPList [], RLPList [RLPList []]]], "c7c0c1c0c3c0c1c0"),
("a 56-byte string", RLPBytes (B.replicate 56 0x61), "b838" <> B.concat (replicate 56 "61"))
]
-- expected signatures and transactions are viem's for the same key and inputs
signingTests :: TVar ChaChaDRG -> Spec
signingTests g = do
it "derives the EIP-155 example address" $
(strEncode <$> addressFromPrivateKey g txKey) `shouldReturn` "0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F"
it "signs a digest as viem does" $
S.signRecoverable g txKey (C.keccak256 "simplex")
`shouldReturn` Right
S.RecoverableSignature
{ S.rsR = hx "8d5aa94d4288fb8297dd9ff2c9cc5026b09b5305ace1f0b48a864ab116f2d3b0",
S.rsS = hx "3eb3b2c117d918736df192c69cef499294f2a070aff74c2bf69776a179b7cdb1",
S.rsRecId = 1
}
it "refuses a digest that is not 32 bytes" $
(isLeft <$> S.signRecoverable g txKey "digest") `shouldReturn` True
transactionTests :: TVar ChaChaDRG -> Spec
transactionTests g = do
it "signs a contract call as viem does" $
(toHex <$$> signEip1559Tx g txKey contractCall)
`shouldReturn` Right "02f89383aa36a709843b9aca008506fc23ac00830186a094353535353535353535353535353535353535353580a4f14fcbc81111111111111111111111111111111111111111111111111111111111111111c001a08be78ee2e58a62763be161e3c4440c74c3b6ae56d667b143554e12239a6328e5a0790d86c0398a5295d981d12f4abc10d61b58917b7b7fd21215f7686457aafc19"
it "signs a value transfer with zero fields as viem does" $
(toHex <$$> signEip1559Tx g txKey transfer)
`shouldReturn` Right "02f86a01808001825208943535353535353535353535353535353535353535880de0b6b3a764000080c001a0d04dbcdcfa4c64fe9deb8f0fea4d7a1f07407e80fd2b066b8f6d58a9baf6af0ba074c14688697464225bbe29301db1df7f0c1b9484a52eaf76162524baba2f2791"
it "encodes every word of fields wider than 64 bits" $
(B.isInfixOf wideFields . toHex <$$> signEip1559Tx g txKey wide)
`shouldReturn` Right True
where
to = right $ strDecode "0x3535353535353535353535353535353535353535"
contractCall = Eip1559Tx {txChainId = 11155111, txNonce = 9, txMaxPriorityFeePerGas = 1000000000, txMaxFeePerGas = 30000000000, txGasLimit = 100000, txTo = to, txValue = 0, txData = hx "f14fcbc8" <> B.replicate 32 0x11}
transfer = Eip1559Tx {txChainId = 1, txNonce = 0, txMaxPriorityFeePerGas = 0, txMaxFeePerGas = 1, txGasLimit = 21000, txTo = to, txValue = 1000000000000000000, txData = ""}
wide = Eip1559Tx {txChainId = maxBound, txNonce = maxBound - 1, txMaxPriorityFeePerGas = 0x10000000000000000000000007, txMaxFeePerGas = 0x80000000000000010000000000000003, txGasLimit = 0x10000000001, txTo = to, txValue = 0x8000000000000001000000000000000100000000000000010000000000000009, txData = ""}
wideFields = "88ffffffffffffffff88fffffffffffffffe8d10000000000000000000000007908000000000000001000000000000000386010000000001943535353535353535353535353535353535353535a0800000000000000100000000000000010000000000000001000000000000000980c0"
txKey :: S.Secp256k1PrivateKey
txKey = right $ S.mkPrivateKey $ BA.replicate 32 0x46
+2 -2
View File
@@ -34,7 +34,7 @@ import Data.Time.Clock.System (SystemTime (..), getSystemTime)
import SMPClient (testStoreLogFile, testStoreMsgsDir, testStoreMsgsDir2, testStoreMsgsFile, testStoreMsgsFile2)
import Simplex.Messaging.Crypto (pattern MaxLenBS)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol (EncDataBytes (..), EntityId (..), ErrorType (..), LinkId, Message (..), QueueLinkData, RecipientId, SParty (..), noMsgFlags)
import Simplex.Messaging.Protocol (EncDataBytes (..), EntityId (..), ErrorType (..), LinkId, Message (..), QueueLinkData, RecipientId, SParty (..), noMsgFlags, randomMsgId)
import Simplex.Messaging.Server (exportMessages, importMessages, printMessageStats)
import Simplex.Messaging.Server.Env.STM (MsgStore (..), journalMsgStoreDepth, readWriteQueueStore)
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..), expireBeforeEpoch)
@@ -147,7 +147,7 @@ mkMessage :: MonadIO m => ByteString -> m Message
mkMessage body = liftIO $ do
g <- C.newRandom
msgTs <- getSystemTime
msgId <- atomically $ C.randomBytes 24 g
msgId <- atomically $ randomMsgId 24 g
pure Message {msgId, msgTs, msgFlags = noMsgFlags, msgBody = C.unsafeMaxLenBS body}
pattern Msg :: ByteString -> Maybe Message
+11 -11
View File
@@ -136,7 +136,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
let dhSecret = C.dh' ntfDh dhPriv
decryptCode nd =
let Right verification = nd .-> "verification"
Right nonce = C.cbNonce <$> nd .-> "nonce"
Right nonce = C.cbNonce =<< nd .-> "nonce"
Right pt = C.cbDecrypt dhSecret nonce verification
in NtfRegCode pt
let code = decryptCode ntfData
@@ -160,7 +160,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
-- receive notification
APNSMockRequest {notification} <- getMockNotification apns tkn
let APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData'} = notification
Right nonce' = C.cbNonce <$> ntfData' .-> "nonce"
Right nonce' = C.cbNonce =<< ntfData' .-> "nonce"
Right message = ntfData' .-> "message"
Right ntfDataDecrypted = C.cbDecrypt dhSecret nonce' message
Right pnMsgs1 = parse pnMessagesP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted
@@ -171,7 +171,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
notifierId `shouldBe` nId
-- receive message
Resp "" _ (MSG RcvMessage {msgId = mId1, msgBody = EncRcvMsgBody body}) <- tGet1 rh
Right ClientRcvMsgBody {msgTs = mTs, msgBody} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret (C.cbNonce mId1) body)
Right ClientRcvMsgBody {msgTs = mTs, msgBody} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret mId1 body)
mId1 `shouldBe` msgId
mTs `shouldBe` msgTs
(msgBody, "hello") #== "delivered from queue"
@@ -183,7 +183,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData2}} <-
getMockNotification apns tkn'
let Right verification2 = ntfData2 .-> "verification"
Right nonce2 = C.cbNonce <$> ntfData2 .-> "nonce"
Right nonce2 = C.cbNonce =<< ntfData2 .-> "nonce"
Right code2 = NtfRegCode <$> C.cbDecrypt dhSecret nonce2 verification2
RespNtf "8" _ NROk <- signSendRecvNtf nh tknKey ("8", tId, TVFY code2)
RespNtf "8a" _ (NRTkn NTActive) <- signSendRecvNtf nh tknKey ("8a", tId, TCHK)
@@ -191,7 +191,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
Resp "9" _ OK <- signSendRecv sh sKey ("9", sId, _SEND' "hello 2")
APNSMockRequest {notification = notification3} <- getMockNotification apns tkn'
let APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData3} = notification3
Right nonce3 = C.cbNonce <$> ntfData3 .-> "nonce"
Right nonce3 = C.cbNonce =<< ntfData3 .-> "nonce"
Right message3 = ntfData3 .-> "message"
Right ntfDataDecrypted3 = C.cbDecrypt dhSecret nonce3 message3
Right pnMsgs2 = parse pnMessagesP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted3
@@ -199,7 +199,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
smpServer3 `shouldBe` srv
notifierId3 `shouldBe` nId
Resp "" _ (MSG RcvMessage {msgId = mId2, msgBody = EncRcvMsgBody body2}) <- tGet1 rh
Right ClientRcvMsgBody {msgBody = "hello 2"} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret (C.cbNonce mId2) body2)
Right ClientRcvMsgBody {msgBody = "hello 2"} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret mId2 body2)
Resp "10" _ OK <- signSendRecv rh rKey ("10", rId, ACK mId2)
q2 <- createQueue rh sPub nPub
@@ -218,7 +218,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
Resp "14" _ OK <- signSendRecv sh sKey ("14", sId, _SEND' "hello 3")
APNSMockRequest {notification = notification4} <- getMockNotification apns tkn'
let APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData4} = notification4
Right nonce4 = C.cbNonce <$> ntfData4 .-> "nonce"
Right nonce4 = C.cbNonce =<< ntfData4 .-> "nonce"
Right message4 = ntfData4 .-> "message"
Right ntfDataDecrypted4 = C.cbDecrypt dhSecret nonce4 message4
Right pnMsgs4 = parse pnMessagesP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted4
@@ -226,14 +226,14 @@ testNotificationSubscription (ATransport t, msType) createQueue =
smpServer4 `shouldBe` srv
notifierId4 `shouldBe` nId
Resp "" _ (MSG RcvMessage {msgId = mId3, msgBody = EncRcvMsgBody body3}) <- tGet1 rh
Right ClientRcvMsgBody {msgBody = "hello 3"} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret (C.cbNonce mId3) body3)
Right ClientRcvMsgBody {msgBody = "hello 3"} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret mId3 body3)
Resp "15" _ OK <- signSendRecv rh rKey ("15", rId, ACK mId3)
-- deliver to queue with ntf sub created while SMP was offline
Resp "16" _ OK <- signSendRecv sh sKey ("16", sId', _SEND' "hello 4")
APNSMockRequest {notification = notification5} <- getMockNotification apns tkn'
let APNSNotification {aps = APNSMutableContent {}, notificationData = Just ntfData5} = notification5
Right nonce5 = C.cbNonce <$> ntfData5 .-> "nonce"
Right nonce5 = C.cbNonce =<< ntfData5 .-> "nonce"
Right message5 = ntfData5 .-> "message"
Right ntfDataDecrypted5 = C.cbDecrypt dhSecret nonce5 message5
Right pnMsgs5 = parse pnMessagesP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted5
@@ -241,7 +241,7 @@ testNotificationSubscription (ATransport t, msType) createQueue =
smpServer5 `shouldBe` srv
notifierId5 `shouldBe` nId'
Resp "" _ (MSG RcvMessage {msgId = mId4, msgBody = EncRcvMsgBody body4}) <- tGet1 rh
Right ClientRcvMsgBody {msgBody = "hello 4"} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret' (C.cbNonce mId4) body4)
Right ClientRcvMsgBody {msgBody = "hello 4"} <- pure $ parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt rcvDhSecret' mId4 body4)
Resp "17" _ OK <- signSendRecv rh rKey' ("17", rId', ACK mId4)
pure ()
@@ -307,7 +307,7 @@ registerToken nh apns token = do
let dhSecret = C.dh' ntfDh dhPriv
decryptCode nd =
let Right verification = nd .-> "verification"
Right nonce = C.cbNonce <$> nd .-> "nonce"
Right nonce = C.cbNonce =<< nd .-> "nonce"
Right pt = C.cbDecrypt dhSecret nonce verification
in NtfRegCode pt
let code = decryptCode ntfData
+2 -2
View File
@@ -15,6 +15,7 @@ import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations
import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfig (..), MigrationConfirmation (..), MigrationsToRun (..), toDownMigration)
import Simplex.Messaging.Util (ifM, whenM)
import System.Directory (doesFileExist, removeFile)
import System.Info (os)
import System.Process (readCreateProcess, shell)
import Test.Hspec hiding (fit, it)
import Util
@@ -57,14 +58,13 @@ postgresSchemaDumpTest migrations skipComparisonForDownMigrations testDBOpts@DBO
getSchema :: FilePath -> IO String
getSchema schemaPath = do
ci <- envCI
let cmd =
("pg_dump " <> B.unpack connstr <> " --schema " <> B.unpack testDBSchema)
<> " --schema-only --no-owner --no-privileges --no-acl --no-subscriptions --no-tablespaces > "
<> schemaPath
void $ readCreateProcess (shell cmd) ""
threadDelay 20000
let sed = (if ci then "sed -i" else "sed -i ''")
let sed = (if os == "linux" then "sed -i" else "sed -i ''")
void $ readCreateProcess (shell $ sed <> " '/^--/d; /^\\\\restrict/d; /^\\\\unrestrict/d' " <> schemaPath) ""
sch <- readFile schemaPath
sch `deepseq` pure sch
+1
View File
@@ -263,6 +263,7 @@ cfgMS msType = withStoreCfg (testServerStoreConfig msType) $ \serverStoreCfg ->
prometheusMetricsFile = testPrometheusMetricsFile,
pendingENDInterval = 500000,
ntfDeliveryInterval = 200000,
authErrorDelay = 20000,
smpCredentials =
ServerCredentials
{ caCertificateFile = Just "tests/fixtures/ca.crt",
+31 -2
View File
@@ -26,7 +26,7 @@ import qualified Data.List.NonEmpty as L
import Data.Time.Clock (getCurrentTime)
import SMPAgentClient
import SMPClient
import ServerTests (decryptMsgV3, sendRecv)
import ServerTests (decryptMsgV3, responseTime, sendRecv)
import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage)
import qualified Simplex.Messaging.Agent as A
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..))
@@ -75,6 +75,10 @@ smpProxyTests = do
xit "batching proxy requests" todo
it "relay rejects forwarded command with changed version" $ \_ ->
testChangedFwdVersion
it "proxy rejects forwarded correlation ID that is not 24 bytes" $ \_ ->
testFwdCorrIdSize
it "relay delays AUTH errors to forwarded commands" $ \_ ->
testForwardedAuthErrorDelay
describe "deliver message via SMP proxy" $ do
let srv1 = SMPServer testHost testPort testKeyHash
srv2 = SMPServer testHost2 testPort2 testKeyHash
@@ -472,10 +476,35 @@ testChangedFwdVersion =
nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g
let v = currentClientSMPRelayVersion
et <- either (fail . show) (pure . SMP.EncTransmission) $ C.cbEncrypt (C.dh' peerServerPubKey cmdPrivKey) (SMP.encTransmissionNonce v nonce) "" SMP.paddedProxiedTLength
let forward fwdVersion = forwardSMPTransmission rc (SMP.CorrId corrId) fwdVersion cmdPubKey et
let forward fwdVersion = forwardSMPTransmission rc (either error id $ C.corrCbNonce corrId) fwdVersion cmdPubKey et
_ <- runExceptT' $ forward v
runExceptT (forward $ prevVersion v) `shouldReturn` Left (PCEProtocolError SMP.CRYPTO)
testFwdCorrIdSize :: IO ()
testFwdCorrIdSize =
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ ->
withSmpServerConfigOn (transport @TLS) cfgJ2 testPort2 $ \_ ->
testSMPClient_ "localhost" testPort supportedServerSMPRelayVRange Nothing $ \(th :: THandleSMP TLS 'TClient) -> do
(_, _, Right (SMP.PKEY sessId _ _)) <- sendRecv th (Nothing, "1", NoEntity, SMP.PRXY testSMPServer2 Nothing)
g <- C.newRandom
(cmdPubKey, _) <- atomically $ C.generateKeyPair g
forM_ (["", "2", "3333333333333333333333333"] :: [ByteString]) $ \corrId -> do
(_, _, reply) <- sendRecv th (Nothing, corrId, SMP.EntityId sessId, SMP.PFWD currentClientSMPRelayVersion cmdPubKey (SMP.EncTransmission ""))
reply `shouldBe` Right (SMP.ERR $ SMP.CMD SMP.SYNTAX)
testForwardedAuthErrorDelay :: IO ()
testForwardedAuthErrorDelay =
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ ->
withSmpServerConfigOn (transport @TLS) (updateCfg cfgJ2 $ \cfg_ -> cfg_ {authErrorDelay = 500000}) testPort2 $ \_ -> do
g <- C.newRandom
ts <- getCurrentTime
pc <- either (fail . show) pure =<< getProtocolClient g NRMInteractive (1, testSMPServer, Nothing) defaultSMPClientConfig [] Nothing ts (\_ -> pure ())
sess <- runExceptT' $ connectSMPProxiedRelay pc NRMInteractive testSMPServer2 Nothing
(sendTime, Left (PCEProtocolError SMP.AUTH)) <- responseTime $ runExceptT $ proxySMPMessage pc NRMInteractive sess Nothing (SMP.EntityId "1234") noMsgFlags "hello"
(getLinkTime, Left (PCEProtocolError SMP.AUTH)) <- responseTime $ runExceptT $ proxyGetSMPQueueLink pc NRMInteractive sess (SMP.EntityId "1234")
sendTime `shouldSatisfy` (>= 0.5)
getLinkTime `shouldSatisfy` (>= 0.5)
-- Shared "phase 2" of the reconnection tests: start a healthy relay, confirm it is reachable
-- directly (PING, not via the proxy) so a proxy failure can only mean the proxy didn't reconnect,
-- let any stored connection error expire, then require the proxy to establish the session (PKEY).
+66 -38
View File
@@ -36,6 +36,7 @@ import Data.List.NonEmpty (NonEmpty)
import Data.List (isPrefixOf)
import Data.Maybe (catMaybes)
import Data.String (IsString (..))
import Data.Time.Clock (NominalDiffTime, diffUTCTime, getCurrentTime)
import Text.Read (readMaybe)
import Data.Type.Equality
import qualified Data.X509.Validation as XV
@@ -58,7 +59,6 @@ import Simplex.Messaging.Transport.Credentials
import Simplex.Messaging.Util (whenM)
import System.Directory (doesDirectoryExist, doesFileExist, removeDirectoryRecursive, removeFile)
import System.IO (IOMode (..), withFile)
import System.TimeIt (timeItT)
import System.Timeout
import Test.HUnit
import Test.Hspec hiding (fit, it)
@@ -97,6 +97,7 @@ serverTests = do
describe "Restore messages (old / v2)" testRestoreExpireMessages
describe "Save prometheus metrics" testPrometheusMetrics
describe "Timing of AUTH error" testTiming
describe "Delay of AUTH error" testAuthErrorDelay
describe "Message notifications" $ do
testMessageNotifications
testMessageServiceNotifications
@@ -166,7 +167,7 @@ signSend_ h@THandle {params} (C.APrivateAuthKey a pk) serviceKey_ (corrId, qId,
authorize t = (,(`C.sign'` t) <$> serviceKey_) <$> case a of
C.SEd25519 -> Just . TASignature . C.ASignature C.SEd25519 $ C.sign' pk t'
C.SEd448 -> Just . TASignature . C.ASignature C.SEd448 $ C.sign' pk t'
C.SX25519 -> (\THAuthClient {peerServerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t') <$> thAuth params
C.SX25519 -> (\THAuthClient {peerServerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.unsafeCbNonce corrId) t') <$> thAuth params
#if !MIN_VERSION_base(4,18,0)
_sx448 -> undefined -- ghc8107 fails to the branch excluded by types
#endif
@@ -194,12 +195,12 @@ _SEND = SEND noMsgFlags
_SEND' :: MsgBody -> Command 'Sender
_SEND' = SEND MsgFlags {notification = True}
decryptMsgV2 :: C.DhSecret 'C.X25519 -> ByteString -> ByteString -> Either C.CryptoError ByteString
decryptMsgV2 dhShared = C.cbDecrypt dhShared . C.cbNonce
decryptMsgV2 :: C.DhSecret 'C.X25519 -> MsgId -> ByteString -> Either C.CryptoError ByteString
decryptMsgV2 = C.cbDecrypt
decryptMsgV3 :: C.DhSecret 'C.X25519 -> ByteString -> ByteString -> Either String MsgBody
decryptMsgV3 :: C.DhSecret 'C.X25519 -> MsgId -> ByteString -> Either String MsgBody
decryptMsgV3 dhShared nonce body =
case parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt dhShared (C.cbNonce nonce) body) of
case parseAll clientRcvMsgBodyP =<< first show (C.cbDecrypt dhShared nonce body) of
Right ClientRcvMsgBody {msgBody} -> Right msgBody
Right ClientRcvMsgQuota {} -> Left "ClientRcvMsgQuota"
Left e -> Left e
@@ -425,7 +426,7 @@ testCreateDelete =
Resp "dabc" _ err8 <- sendRecv sh ("", "dabc", sId, _SEND "hello")
(err8, ERR AUTH) #== "rejects unsigned SEND too when deleted"
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, ACK "")
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, ACK mId2)
(err11, ERR AUTH) #== "rejects ACK when conn deleted - the second message is deleted"
Resp "bcda" _ err9 <- signSendRecv rh rKey ("bcda", rId, OFF)
@@ -609,7 +610,7 @@ testGetSubCommands =
Resp "4" _ (Msg mId2 msg2) <- signSendRecv rh1 rKey ("4", rId, ACK mId1)
(dec mId2 msg2, Right "hello 2") #== "received from queue via SUB"
-- bad msgId returns error
Resp "5" _ (ERR NO_MSG) <- signSendRecv rh2 rKey ("5", rId, ACK "1234")
Resp "5" _ (ERR NO_MSG) <- signSendRecv rh2 rKey ("5", rId, ACK mId2)
-- already ACK'd by subscriber, but still returns OK when msgId matches
Resp "5a" _ OK <- signSendRecv rh2 rKey ("5a", rId, ACK mId1)
-- msg2 is not lost - even if subscriber does not ACK it, it is delivered to getter
@@ -904,7 +905,7 @@ testServiceSubsTotalCount =
_ -> pure Nothing
]
threadDelay 1500000
readFile testPrometheusMetricsFile >>= \m -> readServiceSubsMetric m `shouldBe` Just 2
B.readFile testPrometheusMetricsFile >>= \m -> readServiceSubsMetric (B.unpack m) `shouldBe` Just 2
-- Phase 2: associate 1 more queue via SUB, reconnect with SUBS 3, check metric = 3
(rPub3, rKey3) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
@@ -934,7 +935,7 @@ testServiceSubsTotalCount =
_ -> pure Nothing
]
threadDelay 1500000
readFile testPrometheusMetricsFile >>= \m -> readServiceSubsMetric m `shouldBe` Just 3
B.readFile testPrometheusMetricsFile >>= \m -> readServiceSubsMetric (B.unpack m) `shouldBe` Just 3
readServiceSubsMetric :: String -> Maybe Int
readServiceSubsMetric content =
@@ -1274,21 +1275,21 @@ testTiming =
smpTest2Cfg (cfgMS msType) supportedServerSMPRelayVRange t $ \rh sh ->
testSameTiming rh sh tst msType
where
testName :: (C.AuthAlg, C.AuthAlg, Int) -> String
testName (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, _) = unwords ["queue key:", show goodKeyAlg, "/ used key:", show badKeyAlg]
timingTests :: [(C.AuthAlg, C.AuthAlg, Int)]
testName :: (C.AuthAlg, C.AuthAlg) -> String
testName (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg) = unwords ["queue key:", show goodKeyAlg, "/ used key:", show badKeyAlg]
timingTests :: [(C.AuthAlg, C.AuthAlg)]
timingTests =
[ (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd25519, 200), -- correct key type
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150),
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200),
(C.AuthAlg C.SEd448, C.AuthAlg C.SEd25519, 200),
(C.AuthAlg C.SEd448, C.AuthAlg C.SEd448, 150), -- correct key type
(C.AuthAlg C.SEd448, C.AuthAlg C.SX25519, 200),
(C.AuthAlg C.SX25519, C.AuthAlg C.SEd25519, 200),
(C.AuthAlg C.SX25519, C.AuthAlg C.SEd448, 150),
(C.AuthAlg C.SX25519, C.AuthAlg C.SX25519, 200) -- correct key type
[ (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd25519), -- correct key type
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448),
-- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519),
(C.AuthAlg C.SEd448, C.AuthAlg C.SEd25519),
(C.AuthAlg C.SEd448, C.AuthAlg C.SEd448), -- correct key type
(C.AuthAlg C.SEd448, C.AuthAlg C.SX25519),
(C.AuthAlg C.SX25519, C.AuthAlg C.SEd25519),
(C.AuthAlg C.SX25519, C.AuthAlg C.SEd448),
(C.AuthAlg C.SX25519, C.AuthAlg C.SX25519) -- correct key type
]
timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const
timeRepeat = fmap fst . responseTime . replicateM_ 5
similarTime t1 t2 msType
| t1 <= t2 = abs (1 - t1 / t2) < diff
| otherwise = similarTime t2 t1 msType
@@ -1297,14 +1298,14 @@ testTiming =
diff = case msType of
ASType SQSPostgres _ -> 0.45
_ -> 0.3
testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg, Int) -> AStoreType -> Expectation
testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, n) msType = do
testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg) -> AStoreType -> Expectation
testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg) msType = do
g <- C.newRandom
(rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
Resp "abcd" NoEntity (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", NoEntity, New rPub dhPub)
Resp "abcdabcdabcdabcdabcdabcd" NoEntity (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcdabcdabcdabcdabcdabcd", NoEntity, New rPub dhPub)
let dec = decryptMsgV3 $ C.dh' srvDh dhPriv
Resp "cdab" _ resp <- signSendRecv rh rKey ("cdab", rId, SUB)
Resp "cdabcdabcdabcdabcdabcdab" _ resp <- signSendRecv rh rKey ("cdabcdabcdabcdabcdabcdab", rId, SUB)
case resp of
OK -> pure ()
SOK Nothing -> pure ()
@@ -1314,9 +1315,9 @@ testTiming =
runTimingTest rh badKey rId SUB
(sPub, sKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g
Resp "dabc" _ OK <- signSendRecv rh rKey ("dabc", rId, KEY sPub)
Resp "dabcdabcdabcdabcdabcdabc" _ OK <- signSendRecv rh rKey ("dabcdabcdabcdabcdabcdabc", rId, KEY sPub)
Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, _SEND "hello")
Resp "bcdabcdabcdabcdabcdabcda" _ OK <- signSendRecv sh sKey ("bcdabcdabcdabcdabcdabcda", sId, _SEND "hello")
Resp "" _ (Msg mId msg) <- tGet1 rh
(dec mId msg, Right "hello") #== "delivered from queue"
@@ -1325,17 +1326,17 @@ testTiming =
runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> EntityId -> Command p -> IO ()
runTimingTest h badKey qId cmd = do
threadDelay 100000
_ <- timeRepeat n $ do
_ <- timeRepeat $ do
-- "warm up" the server
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", EntityId "1234", cmd)
Resp "dabcdabcdabcdabcdabcdabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabcdabcdabcdabcdabcdabc", EntityId "1234", cmd)
return ()
threadDelay 100000
timeWrongKey <- timeRepeat n $ do
Resp "cdab" _ (ERR AUTH) <- signSendRecv h badKey ("cdab", qId, cmd)
timeWrongKey <- timeRepeat $ do
Resp "cdabcdabcdabcdabcdabcdab" _ (ERR AUTH) <- signSendRecv h badKey ("cdabcdabcdabcdabcdabcdab", qId, cmd)
return ()
threadDelay 100000
timeNoQueue <- timeRepeat n $ do
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", EntityId "1234", cmd)
timeNoQueue <- timeRepeat $ do
Resp "dabcdabcdabcdabcdabcdabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabcdabcdabcdabcdabcdabc", EntityId "1234", cmd)
return ()
let ok = similarTime timeNoQueue timeWrongKey msType
unless ok . putStrLn . unwords $
@@ -1347,6 +1348,33 @@ testTiming =
]
ok `shouldBe` True
testAuthErrorDelay :: SpecWith (ASrvTransport, AStoreType)
testAuthErrorDelay =
it "should send AUTH errors after the delay and other responses without it" $ \(ATransport t, msType) ->
smpTest2Cfg (updateCfg (cfgMS msType) $ \cfg' -> cfg' {authErrorDelay = 500000}) supportedClientSMPRelayVRange t $ \rh sh -> do
g <- C.newRandom
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
(dhPub, _ :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
Resp "1" NoEntity (Ids rId sId _) <- signSendRecv rh rKey ("1", NoEntity, New rPub dhPub)
(_, badKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
(wrongKeyTime, Resp "2" _ (ERR AUTH)) <- responseTime $ signSendRecv rh badKey ("2", rId, SUB)
(noQueueTime, Resp "3" _ (ERR AUTH)) <- responseTime $ signSendRecv rh badKey ("3", EntityId "1234", SUB)
(noLinkTime, Resp "4" _ (ERR AUTH)) <- responseTime $ sendRecv sh ("", "4", EntityId "1234", LGET)
(subTime, Resp "5" _ (SOK Nothing)) <- responseTime $ signSendRecv rh rKey ("5", rId, SUB)
Resp "6" _ OK <- signSendRecv rh rKey ("6", rId, OFF)
(suspendedTime, Resp "7" _ (ERR AUTH)) <- responseTime $ sendRecv sh ("", "7", sId, _SEND "hello")
wrongKeyTime `shouldSatisfy` (>= 0.5)
noQueueTime `shouldSatisfy` (>= 0.5)
suspendedTime `shouldSatisfy` (>= 0.5)
noLinkTime `shouldSatisfy` (>= 0.5)
subTime `shouldSatisfy` (< 0.5)
responseTime :: IO a -> IO (NominalDiffTime, a)
responseTime action = do
t <- getCurrentTime
r <- action
(,r) . (`diffUTCTime` t) <$> getCurrentTime
testMessageNotifications :: SpecWith (ASrvTransport, AStoreType)
testMessageNotifications =
it "should create simplex connection, subscribe notifier and deliver notifications" $ \(ATransport t, msType) -> do
@@ -1814,10 +1842,10 @@ serverSyntaxTests (ATransport t) = do
it "no parameters" $ (sampleSig, "abcd", "12345678", SEND_) >#> ("", "abcd", "12345678", ERR $ CMD SYNTAX)
it "no queue ID" $ (sampleSig, "bcda", "", (SEND_, ' ', noMsgFlags, ' ', "hello" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_ENTITY)
describe "ACK" $ do
it "valid syntax" $ (sampleSig, "cdab", "12345678", (ACK_, ' ', "1234" :: ByteString)) >#> ("", "cdab", "12345678", ERR AUTH)
it "valid syntax" $ (sampleSig, "cdab", "12345678", (ACK_, ' ', "123456789012345678901234" :: ByteString)) >#> ("", "cdab", "12345678", ERR AUTH)
it "no parameters" $ (sampleSig, "abcd", "12345678", ACK_) >#> ("", "abcd", "12345678", ERR $ CMD SYNTAX)
it "no queue ID" $ (sampleSig, "bcda", "", (ACK_, ' ', "1234" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_AUTH)
it "no signature" $ ("", "cdab", "12345678", (ACK_, ' ', "1234" :: ByteString)) >#> ("", "cdab", "12345678", ERR $ CMD NO_AUTH)
it "no queue ID" $ (sampleSig, "bcda", "", (ACK_, ' ', "123456789012345678901234" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_AUTH)
it "no signature" $ ("", "cdab", "12345678", (ACK_, ' ', "123456789012345678901234" :: ByteString)) >#> ("", "cdab", "12345678", ERR $ CMD NO_AUTH)
describe "PING" $ do
it "valid syntax" $ ("", "abcd", "", PING_) >#> ("", "abcd", "", PONG)
describe "broker response not allowed" $ do
+2
View File
@@ -11,6 +11,7 @@ import CoreTests.BatchingTests
import CoreTests.CryptoFileTests
import CoreTests.CryptoTests
import CoreTests.EncodingTests
import CoreTests.EthCryptoTests
import CoreTests.MsgStoreTests
import CoreTests.RetryIntervalTests
import CoreTests.SOCKSSettings
@@ -87,6 +88,7 @@ main = do
describe "Encoding tests" encodingTests
describe "Version range" versionRangeTests
describe "Encryption tests" cryptoTests
describe "Ethereum crypto tests" ethCryptoTests
describe "Encrypted files tests" cryptoFileTests
describe "Message store tests" msgStoreTests
describe "Retry interval tests" retryIntervalTests
+1 -1
View File
@@ -56,7 +56,7 @@ instance Example a => Example (TestWrapper a) where
ci <- envCI
runTest `E.catches` [E.Handler (onTestFailure ci), E.Handler (onTestException ci)]
where
tt = 120
tt = 300
runTest =
timeout (tt * 1000000) (evaluateExample action params hooks state) `finally` callCommand "sync" >>= \case
Just r -> pure r
+17 -17
View File
@@ -768,7 +768,7 @@ tsSecretboxTests :: Spec
tsSecretboxTests = describe "crypto/secretbox" $ do
let key32 = B.pack [1 .. 32]
nonce24 = B.pack [1 .. 24]
cbNonceVal = C.cbNonce nonce24
cbNonceVal = C.unsafeCbNonce nonce24
sbKeyVal = C.unsafeSbKey key32
describe "NaCl secretbox (tag prepended)" $ do
@@ -979,7 +979,7 @@ tsFileCryptoTests :: Spec
tsFileCryptoTests = describe "crypto/file" $ do
let key32 = B.pack [1 .. 32]
nonce24 = B.pack [1 .. 24]
cbNonceVal = C.cbNonce nonce24
cbNonceVal = C.unsafeCbNonce nonce24
sbKeyVal = C.unsafeSbKey key32
describe "FileHeader encoding" $ do
@@ -2329,7 +2329,7 @@ tsClientTests = describe "protocol/client" $ do
C.cbAuthenticate
(C.PublicKeyX25519 pubA)
(C.PrivateKeyX25519 privB)
(C.cbNonce nonce24)
(C.unsafeCbNonce nonce24)
msg
tsResult <-
callNode $
@@ -2352,7 +2352,7 @@ tsClientTests = describe "protocol/client" $ do
C.cbAuthenticate
(C.PublicKeyX25519 pubA)
(C.PrivateKeyX25519 privB)
(C.cbNonce nonce24)
(C.unsafeCbNonce nonce24)
msg
B.length expected `shouldBe` 80
@@ -2363,7 +2363,7 @@ tsClientTests = describe "protocol/client" $ do
C.cbAuthenticate
(C.PublicKeyX25519 pubA)
(C.PrivateKeyX25519 privB)
(C.cbNonce nonce24)
(C.unsafeCbNonce nonce24)
msg
tsResult <-
callNode $
@@ -2389,7 +2389,7 @@ tsClientTests = describe "protocol/client" $ do
C.cbAuthenticate
(C.PublicKeyX25519 pubA)
(C.PrivateKeyX25519 privB)
(C.cbNonce nonce24)
(C.unsafeCbNonce nonce24)
msg
tsResult <-
callNode $
@@ -2427,7 +2427,7 @@ tsClientTests = describe "protocol/client" $ do
C.cbVerify
(C.PublicKeyX25519 pubB)
(C.PrivateKeyX25519 privA)
(C.cbNonce nonce24)
(C.unsafeCbNonce nonce24)
(C.CbAuthenticator tsAuth)
msg
hsValid `shouldBe` True
@@ -2438,7 +2438,7 @@ tsClientTests = describe "protocol/client" $ do
it "encryptTransportChunk matches Haskell" $ do
let plaintext = B.pack [100 .. 199]
state0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
state0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(cipher, state1) = LC.sbEncryptChunk state0 plaintext
tag = BA.convert $ LC.sbAuth state1 :: B.ByteString
expected = cipher <> tag
@@ -2457,7 +2457,7 @@ tsClientTests = describe "protocol/client" $ do
it "decryptTransportChunk decrypts Haskell-encrypted data" $ do
let plaintext = B.pack ([200 .. 255] <> [0 .. 99])
state0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
state0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(cipher, state1) = LC.sbEncryptChunk state0 plaintext
tag = BA.convert $ LC.sbAuth state1 :: B.ByteString
encData = cipher <> tag
@@ -2542,7 +2542,7 @@ tsDownloadTests = describe "download" $ do
-- File-level key/nonce (different from transport)
fileKey32 = B.pack [1 .. 32]
fileNonce24 = B.pack [1 .. 24]
fileCbNonce = C.cbNonce fileNonce24
fileCbNonce = C.unsafeCbNonce fileNonce24
fileSbKey = C.unsafeSbKey fileKey32
describe "processFileResponse" $ do
@@ -2568,7 +2568,7 @@ tsDownloadTests = describe "download" $ do
dhSecretBytes = case dhSecret of C.DhSecretX25519 k -> BA.convert k :: B.ByteString
chunkData = B.pack [50 .. 149]
chunkDigest = C.sha256Hash chunkData
state0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
state0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(cipher, state1) = LC.sbEncryptChunk state0 chunkData
tag = BA.convert (LC.sbAuth state1) :: B.ByteString
encData = cipher <> tag
@@ -2592,7 +2592,7 @@ tsDownloadTests = describe "download" $ do
dhSecretBytes = case dhSecret of C.DhSecretX25519 k -> BA.convert k :: B.ByteString
chunkData = B.pack [50 .. 149]
wrongDigest = B.replicate 32 0xff
state0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
state0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(cipher, state1) = LC.sbEncryptChunk state0 chunkData
tag = BA.convert (LC.sbAuth state1) :: B.ByteString
encData = cipher <> tag
@@ -2615,7 +2615,7 @@ tsDownloadTests = describe "download" $ do
let dhSecret = C.dh' (C.PublicKeyX25519 pubA) (C.PrivateKeyX25519 privB)
dhSecretBytes = case dhSecret of C.DhSecretX25519 k -> BA.convert k :: B.ByteString
chunkData = B.pack [10 .. 50]
state0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
state0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(cipher, state1) = LC.sbEncryptChunk state0 chunkData
tag = BA.convert (LC.sbAuth state1) :: B.ByteString
encData = cipher <> tag
@@ -2652,7 +2652,7 @@ tsDownloadTests = describe "download" $ do
-- Step 2: transport-level encryption (simulates server sending chunk)
let dhSecret = C.dh' (C.PublicKeyX25519 pubA) (C.PrivateKeyX25519 privB)
dhSecretBytes = case dhSecret of C.DhSecretX25519 k -> BA.convert k :: B.ByteString
ts0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
ts0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(transportCipher, ts1) = LC.sbEncryptChunk ts0 fileEncrypted
transportTag = BA.convert (LC.sbAuth ts1) :: B.ByteString
transportEncData = transportCipher <> transportTag
@@ -2702,7 +2702,7 @@ tsDownloadTests = describe "download" $ do
dhSecret1 = C.dh' (C.PublicKeyX25519 pubA) (C.PrivateKeyX25519 privB)
dhSecret1Bytes = case dhSecret1 of C.DhSecretX25519 k -> BA.convert k :: B.ByteString
nonce1 = nonce24
t1s0 = either (error . show) id $ LC.cbInit dhSecret1 (C.cbNonce nonce1)
t1s0 = either (error . show) id $ LC.cbInit dhSecret1 (C.unsafeCbNonce nonce1)
(t1cipher, t1s1) = LC.sbEncryptChunk t1s0 fileChunk1
t1tag = BA.convert (LC.sbAuth t1s1) :: B.ByteString
transportEnc1 = t1cipher <> t1tag
@@ -2710,7 +2710,7 @@ tsDownloadTests = describe "download" $ do
nonce2 = B.pack [24 .. 47]
dhSecret2 = C.dh' (C.PublicKeyX25519 pubB) (C.PrivateKeyX25519 privA)
dhSecret2Bytes = case dhSecret2 of C.DhSecretX25519 k -> BA.convert k :: B.ByteString
t2s0 = either (error . show) id $ LC.cbInit dhSecret2 (C.cbNonce nonce2)
t2s0 = either (error . show) id $ LC.cbInit dhSecret2 (C.unsafeCbNonce nonce2)
(t2cipher, t2s1) = LC.sbEncryptChunk t2s0 fileChunk2
t2tag = BA.convert (LC.sbAuth t2s1) :: B.ByteString
transportEnc2 = t2cipher <> t2tag
@@ -2748,7 +2748,7 @@ tsDownloadTests = describe "download" $ do
-- Client side: generate FGET command
let dhSecret = C.dh' (C.PublicKeyX25519 pubA) (C.PrivateKeyX25519 privB)
chunkData = "FGET round-trip test data" :: B.ByteString
state0 = either (error . show) id $ LC.cbInit dhSecret (C.cbNonce nonce24)
state0 = either (error . show) id $ LC.cbInit dhSecret (C.unsafeCbNonce nonce24)
(cipher, state1) = LC.sbEncryptChunk state0 chunkData
tag = BA.convert (LC.sbAuth state1) :: B.ByteString
encData = cipher <> tag
+315
View File
@@ -0,0 +1,315 @@
RECENT CHANGES:
* (16 Apr 2013) Added private derivation for i ≥ 0x80000000 (less risk of parent private key leakage)
* (30 Apr 2013) Switched from multiplication by I<sub>L</sub> to addition of I<sub>L</sub> (faster, easier implementation)
* (25 May 2013) Added test vectors
* (15 Jan 2014) Rename keys with index ≥ 0x80000000 to hardened keys, and add explicit conversion functions.
* (24 Feb 2017) Added test vectors for hardened derivation with leading zeros
* (4 Nov 2020) Added new test vectors for hardened derivation with leading zeros
<pre>
BIP: 32
Layer: Applications
Title: Hierarchical Deterministic Wallets
Authors: Pieter Wuille <[email protected]>
Status: Deployed
Type: Informational
Assigned: 2012-02-11
License: BSD-2-Clause
</pre>
==Abstract==
This document describes hierarchical deterministic wallets (or "HD Wallets"): wallets which can be shared partially or entirely with different systems, each with or without the ability to spend coins.
The specification is intended to set a standard for deterministic wallets that can be interchanged between different clients. Although the wallets described here have many features, not all are required by supporting clients.
The specification consists of two parts. In the first part, a system for deriving a tree of keypairs from a single seed is presented. The second part demonstrates how to build a wallet structure on top of such a tree.
==Copyright==
This BIP is licensed under the 2-clause BSD license.
==Motivation==
The Bitcoin reference client uses randomly generated keys. In order to avoid the necessity for a backup after every transaction, (by default) 100 keys are cached in a pool of reserve keys. Still, these wallets are not intended to be shared and used on several systems simultaneously. They support hiding their private keys by using the wallet encrypt feature and not sharing the password, but such "neutered" wallets lose the power to generate public keys as well.
Deterministic wallets do not require such frequent backups, and elliptic curve mathematics permit schemes where one can calculate the public keys without revealing the private keys. This permits for example a webshop business to let its webserver generate fresh addresses (public key hashes) for each order or for each customer, without giving the webserver access to the corresponding private keys (which are required for spending the received funds).
However, deterministic wallets typically consist of a single "chain" of keypairs. The fact that there is only one chain means that sharing a wallet happens on an all-or-nothing basis. However, in some cases one only wants some (public) keys to be shared and recoverable. In the example of a webshop, the webserver does not need access to all public keys of the merchant's wallet; only to those addresses which are used to receive customers' payments, and not for example the change addresses that are generated when the merchant spends money. Hierarchical deterministic wallets allow such selective sharing by supporting multiple keypair chains, derived from a single root.
==Specification: Key derivation==
===Conventions===
In the rest of this text we will assume the public key cryptography used in Bitcoin, namely elliptic curve cryptography using the field and curve parameters defined by secp256k1 (http://www.secg.org/sec2-v2.pdf). Variables below are either:
* Integers modulo the order of the curve (referred to as n).
* Coordinates of points on the curve.
* Byte sequences.
Addition (+) of two coordinate pair is defined as application of the EC group operation.
Concatenation (||) is the operation of appending one byte sequence onto another.
As standard conversion functions, we assume:
* point(p): returns the coordinate pair resulting from EC point multiplication (repeated application of the EC group operation) of the secp256k1 base point with the integer p (i.e., the operation used to compute a public key from a private key).
* ser<sub>32</sub>(i): serialize a 32-bit unsigned integer i as a 4-byte sequence, most significant byte first.
* ser<sub>256</sub>(p): serializes the integer p as a 32-byte sequence, most significant byte first.
* ser<sub>P</sub>(P): serializes the coordinate pair P = (x,y) (i.e., the public key) as a byte sequence using [https://www.secg.org/sec1-v2.pdf SEC1]'s compressed form: (0x02 or 0x03) || ser<sub>256</sub>(x), where the header byte depends on the parity of the omitted y coordinate.
* parse<sub>256</sub>(p): interprets a 32-byte sequence as a 256-bit number, most significant byte first.
===Extended keys===
In what follows, we will define a function that derives a number of child keys from a parent key. In order to prevent these from depending solely on the key itself, we extend both private and public keys first with an extra 256 bits of entropy. This extension, called the chain code, is identical for corresponding private and public keys, and consists of 32 bytes.
We represent an extended private key as (k, c), with k the normal private key, and c the chain code. An extended public key is represented as (K, c), with K = point(k) and c the chain code.
Each extended key has 2<sup>31</sup> normal child keys, and 2<sup>31</sup> hardened child keys. Each of these child keys has an index. The normal child keys use indices 0 through 2<sup>31</sup>-1. The hardened child keys use indices 2<sup>31</sup> through 2<sup>32</sup>-1. To ease notation for hardened key indices, a number i<sub>H</sub> represents i+2<sup>31</sup>.
===Child key derivation (CKD) functions===
Given a parent extended key and an index i, it is possible to compute the corresponding child extended key. The algorithm to do so depends on whether the child is a hardened key or not (or, equivalently, whether i ≥ 2<sup>31</sup>), and whether we're talking about private or public keys.
====Private parent key &rarr; private child key====
The function CKDpriv((k<sub>par</sub>, c<sub>par</sub>), i) &rarr; (k<sub>i</sub>, c<sub>i</sub>) computes a child extended private key from the parent extended private key:
* Check whether i ≥ 2<sup>31</sup> (whether the child is a hardened key).
** If so (hardened child): let I = HMAC-SHA512(Key = c<sub>par</sub>, Data = 0x00 || ser<sub>256</sub>(k<sub>par</sub>) || ser<sub>32</sub>(i)). (Note: The 0x00 pads the private key to make it 33 bytes long.)
** If not (normal child): let I = HMAC-SHA512(Key = c<sub>par</sub>, Data = ser<sub>P</sub>(point(k<sub>par</sub>)) || ser<sub>32</sub>(i)).
* Split I into two 32-byte sequences, I<sub>L</sub> and I<sub>R</sub>.
* The returned child key k<sub>i</sub> is parse<sub>256</sub>(I<sub>L</sub>) + k<sub>par</sub> (mod n).
* The returned chain code c<sub>i</sub> is I<sub>R</sub>.
* In case parse<sub>256</sub>(I<sub>L</sub>) ≥ n or k<sub>i</sub> = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2<sup>127</sup>.)
The HMAC-SHA512 function is specified in [http://tools.ietf.org/html/rfc4231 RFC 4231].
====Public parent key &rarr; public child key====
The function CKDpub((K<sub>par</sub>, c<sub>par</sub>), i) &rarr; (K<sub>i</sub>, c<sub>i</sub>) computes a child extended public key from the parent extended public key. It is only defined for non-hardened child keys.
* Check whether i ≥ 2<sup>31</sup> (whether the child is a hardened key).
** If so (hardened child): return failure
** If not (normal child): let I = HMAC-SHA512(Key = c<sub>par</sub>, Data = ser<sub>P</sub>(K<sub>par</sub>) || ser<sub>32</sub>(i)).
* Split I into two 32-byte sequences, I<sub>L</sub> and I<sub>R</sub>.
* The returned child key K<sub>i</sub> is point(parse<sub>256</sub>(I<sub>L</sub>)) + K<sub>par</sub>.
* The returned chain code c<sub>i</sub> is I<sub>R</sub>.
* In case parse<sub>256</sub>(I<sub>L</sub>) ≥ n or K<sub>i</sub> is the point at infinity, the resulting key is invalid, and one should proceed with the next value for i.
====Private parent key &rarr; public child key====
The function N((k, c)) &rarr; (K, c) computes the extended public key corresponding to an extended private key (the "neutered" version, as it removes the ability to sign transactions).
* The returned key K is point(k).
* The returned chain code c is just the passed chain code.
To compute the public child key of a parent private key:
* N(CKDpriv((k<sub>par</sub>, c<sub>par</sub>), i)) (works always).
* CKDpub(N(k<sub>par</sub>, c<sub>par</sub>), i) (works only for non-hardened child keys).
The fact that they are equivalent is what makes non-hardened keys useful (one can derive child public keys of a given parent key without knowing any private key), and also what distinguishes them from hardened keys. The reason for not always using non-hardened keys (which are more useful) is security; see further below for more information.
====Public parent key &rarr; private child key====
This is not possible.
===The key tree===
The next step is cascading several CKD constructions to build a tree. We start with one root, the master extended key m. By evaluating CKDpriv(m,i) for several values of i, we get a number of level-1 derived nodes. As each of these is again an extended key, CKDpriv can be applied to those as well.
To shorten notation, we will write CKDpriv(CKDpriv(CKDpriv(m,3<sub>H</sub>),2),5) as m/3<sub>H</sub>/2/5. Equivalently for public keys, we write CKDpub(CKDpub(CKDpub(M,3),2),5) as M/3/2/5. This results in the following identities:
* N(m/a/b/c) = N(m/a/b)/c = N(m/a)/b/c = N(m)/a/b/c = M/a/b/c.
* N(m/a<sub>H</sub>/b/c) = N(m/a<sub>H</sub>/b)/c = N(m/a<sub>H</sub>)/b/c.
However, N(m/a<sub>H</sub>) cannot be rewritten as N(m)/a<sub>H</sub>, as the latter is not possible.
Each leaf node in the tree corresponds to an actual key, while the internal nodes correspond to the collections of keys that descend from them. The chain codes of the leaf nodes are ignored, and only their embedded private or public key is relevant. Because of this construction, knowing an extended private key allows reconstruction of all descendant private keys and public keys, and knowing an extended public key allows reconstruction of all descendant non-hardened public keys.
===Key identifiers===
Extended keys can be identified by the Hash160 (RIPEMD160 after SHA256) of the serialized ECDSA public key K, ignoring the chain code. This corresponds exactly to the data used in traditional Bitcoin addresses. It is not advised to represent this data in base58 format though, as it may be interpreted as an address that way (and wallet software is not required to accept payment to the chain key itself).
The first 32 bits of the identifier are called the key fingerprint.
===Serialization format===
Extended public and private keys are serialized as follows:
* 4 bytes: version bytes (mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private)
* 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 derived keys, ....
* 4 bytes: the fingerprint of the parent's key (0x00000000 if master key)
* 4 bytes: child number. This is ser<sub>32</sub>(i) for i in x<sub>i</sub> = x<sub>par</sub>/i, with x<sub>i</sub> the key being serialized. (0x00000000 if master key)
* 32 bytes: the chain code
* 33 bytes: the public key or private key data (ser<sub>P</sub>(K) for public keys, 0x00 || ser<sub>256</sub>(k) for private keys)
This 78 byte structure can be encoded like other Bitcoin data in Base58, by first adding 32 checksum bits (derived from the double SHA-256 checksum), and then converting to the Base58 representation. This results in a Base58-encoded string of exactly 111 characters. Because of the choice of the version bytes, the Base58 representation will start with "xprv" or "xpub" on mainnet, "tprv" or "tpub" on testnet.
Note that the fingerprint of the parent only serves as a fast way to detect parent and child nodes in software, and software must be willing to deal with collisions. Internally, the full 160-bit identifier could be used.
When importing a serialized extended public key, implementations must verify whether the X coordinate in the public key data corresponds to a point on the curve. If not, the extended public key is invalid.
===Master key generation===
The total number of possible extended keypairs is almost 2<sup>512</sup>, but the produced keys are only 256 bits long, and offer about half of that in terms of security. Therefore, master keys are not generated directly, but instead from a potentially short seed value.
* Generate a seed byte sequence S of a chosen length (between 128 and 512 bits; 256 bits is advised) from a (P)RNG.
* Calculate I = HMAC-SHA512(Key = "Bitcoin seed", Data = S)
* Split I into two 32-byte sequences, I<sub>L</sub> and I<sub>R</sub>.
* Use parse<sub>256</sub>(I<sub>L</sub>) as master secret key, and I<sub>R</sub> as master chain code.
In case parse<sub>256</sub>(I<sub>L</sub>) is 0 or parse<sub>256</sub>(I<sub>L</sub>) ≥ n, the master key is invalid.
<img src=bip-0032/derivation.png></img>
==Specification: Wallet structure==
The previous sections specified key trees and their nodes. The next step is imposing a wallet structure on this tree. The layout defined in this section is a default only, though clients are encouraged to mimic it for compatibility, even if not all features are supported.
===The default wallet layout===
An HDW is organized as several 'accounts'. Accounts are numbered, the default account ("") being number 0. Clients are not required to support more than one account - if not, they only use the default account.
Each account is composed of two keypair chains: an internal and an external one. The external keychain is used to generate new public addresses, while the internal keychain is used for all other operations (change addresses, generation addresses, ..., anything that doesn't need to be communicated). Clients that do not support separate keychains for these should use the external one for everything.
* m/i<sub>H</sub>/0/k corresponds to the k'th keypair of the external chain of account number i of the HDW derived from master m.
* m/i<sub>H</sub>/1/k corresponds to the k'th keypair of the internal chain of account number i of the HDW derived from master m.
===Use cases===
====Full wallet sharing: m====
In cases where two systems need to access a single shared wallet, and both need to be able to perform spendings, one needs to share the master private extended key. Nodes can keep a pool of N look-ahead keys cached for external chains, to watch for incoming payments. The look-ahead for internal chains can be very small, as no gaps are to be expected here. An extra look-ahead could be active for the first unused account's chains - triggering the creation of a new account when used. Note that the name of the account will still need to be entered manually and cannot be synchronized via the block chain.
====Audits: N(m/*)====
In case an auditor needs full access to the list of incoming and outgoing payments, one can share all account public extended keys. This will allow the auditor to see all transactions from and to the wallet, in all accounts, but not a single secret key.
====Per-office balances: m/i<sub>H</sub>====
When a business has several independent offices, they can all use wallets derived from a single master. This will allow the headquarters to maintain a super-wallet that sees all incoming and outgoing transactions of all offices, and even permit moving money between the offices.
====Recurrent business-to-business transactions: N(m/i<sub>H</sub>/0)====
In case two business partners often transfer money, one can use the extended public key for the external chain of a specific account (M/i h/0) as a sort of "super address", allowing frequent transactions that cannot (easily) be associated, but without needing to request a new address for each payment.
Such a mechanism could also be used by mining pool operators as a variable payout address.
====Unsecure money receiver: N(m/i<sub>H</sub>/0)====
When an unsecure webserver is used to run an e-commerce site, it needs to know public addresses that are used to receive payments. The webserver only needs to know the public extended key of the external chain of a single account. This means someone illegally obtaining access to the webserver can at most see all incoming payments but will not be able to steal the money, will not (trivially) be able to distinguish outgoing transactions, nor be able to see payments received by other webservers if there are several.
==Compatibility==
To comply with this standard, a client must at least be able to import an extended public or private key, to give access to its direct descendants as wallet keys. The wallet structure (master/account/chain/subchain) presented in the second part of the specification is advisory only, but is suggested as a minimal structure for easy compatibility - even when no separate accounts or distinction between internal and external chains is made. However, implementations may deviate from it for specific needs; more complex applications may call for a more complex tree structure.
==Security==
In addition to the expectations from the EC public-key cryptography itself:
* Given a public key K, an attacker cannot find the corresponding private key more efficiently than by solving the EC discrete logarithm problem (assumed to require 2<sup>128</sup> group operations).
the intended security properties of this standard are:
* Given a child extended private key (k<sub>i</sub>,c<sub>i</sub>) and the integer i, an attacker cannot find the parent private key k<sub>par</sub> more efficiently than a 2<sup>256</sup> brute force of HMAC-SHA512.
* Given any number (2 ≤ N ≤ 2<sup>32</sup>-1) of (index, extended private key) tuples (i<sub>j</sub>,(k<sub>i<sub>j</sub></sub>,c<sub>i<sub>j</sub></sub>)), with distinct i<sub>j</sub>'s, determining whether they are derived from a common parent extended private key (i.e., whether there exists a (k<sub>par</sub>,c<sub>par</sub>) such that for each j in (0..N-1) CKDpriv((k<sub>par</sub>,c<sub>par</sub>),i<sub>j</sub>)=(k<sub>i<sub>j</sub></sub>,c<sub>i<sub>j</sub></sub>)), cannot be done more efficiently than a 2<sup>256</sup> brute force of HMAC-SHA512.
Note however that the following properties do not exist:
* Given a parent extended public key (K<sub>par</sub>,c<sub>par</sub>) and a child public key (K<sub>i</sub>), it is hard to find i.
* Given a parent extended public key (K<sub>par</sub>,c<sub>par</sub>) and a non-hardened child private key (k<sub>i</sub>), it is hard to find k<sub>par</sub>.
===Implications===
Private and public keys must be kept safe as usual. Leaking a private key means access to coins - leaking a public key can mean loss of privacy.
Somewhat more care must be taken regarding extended keys, as these correspond to an entire (sub)tree of keys.
One weakness that may not be immediately obvious, is that knowledge of a parent extended public key plus any non-hardened private key descending from it is equivalent to knowing the parent extended private key (and thus every private and public key descending from it). This means that extended public keys must be treated more carefully than regular public keys.
It is also the reason for the existence of hardened keys, and why they are used for the account level in the tree. This way, a leak of account-specific (or below) private keys never risks compromising the master or other accounts.
==Test Vectors==
===Test vector 1===
Seed (hex): 000102030405060708090a0b0c0d0e0f
* Chain m
** ext pub: xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8
** ext prv: xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi
* Chain m/0<sub>H</sub>
** ext pub: xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw
** ext prv: xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7
* Chain m/0<sub>H</sub>/1
** ext pub: xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ
** ext prv: xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs
* Chain m/0<sub>H</sub>/1/2<sub>H</sub>
** ext pub: xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5
** ext prv: xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM
* Chain m/0<sub>H</sub>/1/2<sub>H</sub>/2
** ext pub: xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV
** ext prv: xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334
* Chain m/0<sub>H</sub>/1/2<sub>H</sub>/2/1000000000
** ext pub: xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy
** ext prv: xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76
===Test vector 2===
Seed (hex): fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542
* Chain m
** ext pub: xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB
** ext prv: xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U
* Chain m/0
** ext pub: xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH
** ext prv: xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt
* Chain m/0/2147483647<sub>H</sub>
** ext pub: xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a
** ext prv: xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9
* Chain m/0/2147483647<sub>H</sub>/1
** ext pub: xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon
** ext prv: xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef
* Chain m/0/2147483647<sub>H</sub>/1/2147483646<sub>H</sub>
** ext pub: xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL
** ext prv: xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc
* Chain m/0/2147483647<sub>H</sub>/1/2147483646<sub>H</sub>/2
** ext pub: xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt
** ext prv: xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j
===Test vector 3===
These vectors test for the retention of leading zeros. See [https://github.com/bitpay/bitcore-lib/issues/47 bitpay/bitcore-lib#47] and [https://github.com/iancoleman/bip39/issues/58 iancoleman/bip39#58] for more information.
Seed (hex): 4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be
* Chain m
** ext pub: xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13
** ext prv: xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6
* Chain m/0<sub>H</sub>
** ext pub: xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y
** ext prv: xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L
===Test vector 4===
These vectors test for the retention of leading zeros. See [https://github.com/btcsuite/btcutil/issues/172 btcsuite/btcutil#172] for more information.
Seed (hex): 3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678
* Chain m
** ext pub: xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa
** ext prv: xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv
* Chain m/0<sub>H</sub>
** ext pub: xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m
** ext prv: xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G
* Chain m/0<sub>H</sub>/1<sub>H</sub>
** ext pub: xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt
** ext prv: xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1
===Test vector 5===
These vectors test that invalid extended keys are recognized as invalid.
* xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm (pubkey version / prvkey mismatch)
* xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH (prvkey version / pubkey mismatch)
* xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn (invalid pubkey prefix 04)
* xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ (invalid prvkey prefix 04)
* xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4 (invalid pubkey prefix 01)
* xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J (invalid prvkey prefix 01)
* xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv (zero depth with non-zero parent fingerprint)
* xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ (zero depth with non-zero parent fingerprint)
* xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN (zero depth with non-zero index)
* xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8 (zero depth with non-zero index)
* DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4 (unknown extended key version)
* DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9 (unknown extended key version)
* xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx (private key 0 not in 1..n-1)
* xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G (private key n not in 1..n-1)
* xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY (invalid pubkey 020000000000000000000000000000000000000000000000000000000000000007)
* xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL (invalid checksum)
==Acknowledgements==
* Gregory Maxwell for the original idea of type-2 deterministic wallets, and many discussions about it.
* Alan Reiner for the implementation of this scheme in Armory, and the suggestions that followed from that.
* Eric Lombrozo for reviewing and revising this BIP.
* Mike Caldwell for the version bytes to obtain human-recognizable Base58 strings.
+1754
View File
File diff suppressed because it is too large Load Diff