mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-12 02:22:45 +00:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6275e7ee5e | ||
|
|
c1e062ba12 | ||
|
|
21164fce3c | ||
|
|
b974dbd8c8 | ||
|
|
78ebf1cb2e | ||
|
|
4c8dc9f7d8 | ||
|
|
1d82c0815d | ||
|
|
861e9890c6 | ||
|
|
adcdb54c02 | ||
|
|
2ffec2aad6 | ||
|
|
3ae71f06b4 | ||
|
|
f53a6f3364 | ||
|
|
5dc64f1cf4 | ||
|
|
0d19ff3108 | ||
|
|
7dcd89d30b | ||
|
|
31f0a74310 | ||
|
|
d2128507cc | ||
|
|
33e8ef018e | ||
|
|
86ac7fdad0 | ||
|
|
ae7a312af7 | ||
|
|
e6597f28b0 | ||
|
|
2298704687 | ||
|
|
a23da20fb5 | ||
|
|
088fc4ea81 | ||
|
|
b23d3a3265 | ||
|
|
2bbc55619f | ||
|
|
3ab14f5a93 | ||
|
|
d9953b30da | ||
|
|
2d4c060c6d |
@@ -9,10 +9,6 @@ on:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
jobs:
|
||||
|
||||
# =============================
|
||||
@@ -320,14 +316,15 @@ jobs:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up uv
|
||||
# uv takes Python from .python-version, the version of the Dockerfile's runtime stage.
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
- name: Set up Python
|
||||
# Matches the runtime stage of scripts/resolver/service/Dockerfile.
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Lint
|
||||
working-directory: scripts/resolver/service
|
||||
run: uv run --frozen ruff check .
|
||||
- name: Install resolver dependencies
|
||||
# Must match scripts/resolver/service/pyproject.toml.
|
||||
run: python -m pip install "eth-hash[pycryptodome]>=0.7"
|
||||
|
||||
- name: Test
|
||||
working-directory: scripts/resolver/service
|
||||
run: uv run --frozen pytest -v
|
||||
run: python -m unittest discover -s scripts/resolver/service -v
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
*.lock
|
||||
# the resolver's Docker image and CI install exactly these
|
||||
!/scripts/resolver/service/uv.lock
|
||||
# local overrides of scripts/resolver/.env.example
|
||||
/scripts/resolver/.env
|
||||
*.db
|
||||
*.db.bak
|
||||
*.session.sql
|
||||
|
||||
@@ -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,3 +1,17 @@
|
||||
# Unreleased
|
||||
|
||||
Crypto:
|
||||
- Ethereum primitives for SimpleX names: secp256k1 keys (vendored
|
||||
libsecp256k1), BIP-39 mnemonics, BIP-32 key derivation, Keccak-256 and EIP-55
|
||||
addresses. Key derivation only - no signing, no transaction construction and
|
||||
no chain writes; the resolver path remains read-only. See
|
||||
`plans/2026-08-05-eth-crypto-bindings.md`.
|
||||
|
||||
SMP protocol:
|
||||
- `ROWN` command (v23): the names an address owns, and whether the account has
|
||||
been used at all, so a recovered wallet seed can be scanned for the accounts
|
||||
already in use. See `plans/2026-09-22-names-owned-by.md`.
|
||||
|
||||
# 6.5.1
|
||||
|
||||
Version 6.5.1.0
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ source-repository-package
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/direct-sqlcipher.git
|
||||
tag: 2330df3dc8c4b660674d3c02d76bb86f66c2abee
|
||||
tag: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
Submodule
+1
Submodule cbits/libsecp256k1 added at 6e2c8bc4ec
+6
-20
@@ -719,32 +719,23 @@ Small_random (small * out, void *random_ctx, sntrup761_random_func * random)
|
||||
|
||||
/* ----- Streamlined NTRU Prime Core */
|
||||
|
||||
/* x^p-x-1 has a degree-19 factor mod 3, so a random g is not invertible in R3
|
||||
with probability about 3^-19; KeyGen_attempts failures in a row mean a broken RNG */
|
||||
#define KeyGen_attempts 10
|
||||
|
||||
/* h,(f,ginv) = KeyGen() */
|
||||
/* returns 0 if KeyGen succeeded; else -1 */
|
||||
static int
|
||||
static void
|
||||
KeyGen (Fq * h, small * f, small * ginv, void *random_ctx,
|
||||
sntrup761_random_func * random)
|
||||
{
|
||||
small g[p];
|
||||
Fq finv[p];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < KeyGen_attempts; ++i)
|
||||
for (;;)
|
||||
{
|
||||
Small_random (g, random_ctx, random);
|
||||
if (R3_recip (ginv, g) == 0)
|
||||
break;
|
||||
}
|
||||
if (i == KeyGen_attempts)
|
||||
return -1;
|
||||
Short_random (f, random_ctx, random);
|
||||
Rq_recip3 (finv, f); /* always works */
|
||||
Rq_mult_small (h, finv, g);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* c = Encrypt(r,h) */
|
||||
@@ -893,21 +884,18 @@ typedef small Inputs[p]; /* passed by reference */
|
||||
#define PublicKeys_bytes Rq_bytes
|
||||
|
||||
/* pk,sk = ZKeyGen() */
|
||||
/* returns 0 if KeyGen succeeded; else -1 */
|
||||
static int
|
||||
static void
|
||||
ZKeyGen (unsigned char *pk, unsigned char *sk, void *random_ctx,
|
||||
sntrup761_random_func * random)
|
||||
{
|
||||
Fq h[p];
|
||||
small f[p], v[p];
|
||||
|
||||
if (KeyGen (h, f, v, random_ctx, random) != 0)
|
||||
return -1;
|
||||
KeyGen (h, f, v, random_ctx, random);
|
||||
Rq_encode (pk, h);
|
||||
Small_encode (sk, f);
|
||||
sk += Small_bytes;
|
||||
Small_encode (sk, v);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* C = ZEncrypt(r,pk) */
|
||||
@@ -972,21 +960,19 @@ HashSession (unsigned char *k, int b, const unsigned char *y,
|
||||
/* ----- Streamlined NTRU Prime */
|
||||
|
||||
/* pk,sk = KEM_KeyGen() */
|
||||
int
|
||||
void
|
||||
sntrup761_keypair (unsigned char *pk, unsigned char *sk, void *random_ctx,
|
||||
sntrup761_random_func * random)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (ZKeyGen (pk, sk, random_ctx, random) != 0)
|
||||
return -1;
|
||||
ZKeyGen (pk, sk, random_ctx, random);
|
||||
sk += SecretKeys_bytes;
|
||||
for (i = 0; i < PublicKeys_bytes; ++i)
|
||||
*sk++ = pk[i];
|
||||
random (random_ctx, Inputs_bytes, sk);
|
||||
sk += Inputs_bytes;
|
||||
Hash_prefix (sk, 4, pk, PublicKeys_bytes);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* c,r_enc = Hide(r,pk,cache); cache is Hash4(pk) */
|
||||
|
||||
+1
-2
@@ -19,8 +19,7 @@
|
||||
|
||||
typedef void sntrup761_random_func (void *ctx, size_t length, uint8_t *dst);
|
||||
|
||||
/* returns 0 on success, -1 if the RNG never produced an invertible polynomial */
|
||||
int
|
||||
void
|
||||
sntrup761_keypair (uint8_t *pk, uint8_t *sk,
|
||||
void *random_ctx, sntrup761_random_func *random);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ This file provides guidance on coding style and approaches and on building the c
|
||||
When designing code and planning implementations:
|
||||
- Apply adversarial thinking, and consider what may happen if one of the communicating parties is malicious.
|
||||
- Formulate an explicit threat model for each change - who can do which undesirable things and under which circumstances.
|
||||
- Use the default cryptographic primitive for each purpose from the [primitive registry](../protocol/crypto-registry.md); any exception requires review, and the registry must be updated with every new primitive use or domain-separation string.
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# 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 know nothing
|
||||
about 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. Signing with those keys (EIP-712 typed data) and stealth addresses
|
||||
land in later changes, with the code that needs them.
|
||||
|
||||
## What is deliberately absent
|
||||
|
||||
- **No RLP encoder, and no transaction building.** RLP is only needed to
|
||||
construct raw transactions or EIP-7702 authorizations. The client does
|
||||
neither, so the `RSLV` resolver path in this repo stays strictly read-only.
|
||||
- **No signing.** This change derives keys and addresses. Signing, and the
|
||||
EIP-712 typed-data hashing it needs, land with the first code that signs.
|
||||
- **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.** Every English BIP-39 word is ASCII, so the NFKD
|
||||
normalization BIP-39 mandates is a no-op on the mnemonic side and no
|
||||
normalization dependency is needed.
|
||||
|
||||
## Modules
|
||||
|
||||
```
|
||||
Simplex.Messaging.Crypto.Secp256k1 FFI to libsecp256k1
|
||||
Simplex.Messaging.Crypto.BIP39 mnemonics
|
||||
Simplex.Messaging.Crypto.BIP39.English generated 2048-word list
|
||||
Simplex.Messaging.Crypto.BIP32 HD derivation
|
||||
Simplex.Messaging.Eth.Keccak Keccak-256
|
||||
Simplex.Messaging.Eth.Address addresses, EIP-55
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
```haskell
|
||||
newtype Secp256k1PrivateKey -- 32 bytes, validated in [1, n-1]
|
||||
newtype Secp256k1PublicKey -- libsecp256k1's opaque 64-byte form
|
||||
data PubKeyFormat = Compressed | Uncompressed
|
||||
|
||||
data Mnemonic -- validated indexes + words, always consistent
|
||||
data MnemonicStrength = MS128 | MS160 | MS192 | MS224 | MS256
|
||||
|
||||
data ExtendedKey = ExtendedKey {xkKey :: PrivateKey, xkChainCode :: ByteString}
|
||||
|
||||
newtype Address -- 20 bytes; Show renders the EIP-55 form
|
||||
```
|
||||
|
||||
`PrivateKey`, `Mnemonic` and `ExtendedKey` have **redacting `Show` instances**,
|
||||
and `PrivateKey` compares with `constEq`. These keys authorise transfers of
|
||||
assets with monetary value: a derived `Show` would put one in a log the first
|
||||
time anything is traced. A chain code is secret too — it plus one child key
|
||||
derives siblings.
|
||||
|
||||
## Functions
|
||||
|
||||
```haskell
|
||||
-- Secp256k1
|
||||
mkPrivateKey :: ByteString -> Either String Secp256k1PrivateKey
|
||||
unPrivateKey :: Secp256k1PrivateKey -> ByteString
|
||||
secp256k1PublicKey :: Secp256k1PrivateKey -> Secp256k1PublicKey -- total: key is validated
|
||||
serializePublicKey :: PubKeyFormat -> Secp256k1PublicKey -> ByteString
|
||||
privateKeyTweakAdd :: Secp256k1PrivateKey -> ByteString -> Maybe Secp256k1PrivateKey
|
||||
|
||||
-- BIP39
|
||||
entropyToMnemonic :: ByteString -> Either String Mnemonic
|
||||
mnemonicToEntropy :: Mnemonic -> ByteString -- total
|
||||
parseMnemonic :: ByteString -> Either String Mnemonic
|
||||
mnemonicToSeed :: Mnemonic -> ByteString -> ByteString
|
||||
randomMnemonic :: MnemonicStrength -> TVar ChaChaDRG -> STM Mnemonic
|
||||
|
||||
-- BIP32
|
||||
masterKey :: ByteString -> Either String ExtendedKey
|
||||
derivePath :: ExtendedKey -> [Word32] -> Either String ExtendedKey
|
||||
renderPath :: [Word32] -> ByteString
|
||||
|
||||
-- Eth
|
||||
keccak256 :: ByteString -> ByteString
|
||||
addressFromPrivateKey :: Secp256k1PrivateKey -> Address
|
||||
ethereumPath :: Word32 -> Word32 -> [Word32] -- m/44'/60'/account'/0/address
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
`randomMnemonic` is shaped like `Simplex.Messaging.Crypto.randomBytes` so it
|
||||
composes with the agent's DRG instead of reaching for system entropy.
|
||||
|
||||
`parseMnemonic` lower-cases and splits on any whitespace, so a user retyping
|
||||
their recovery key is not rejected for capitalising a word. This does not change
|
||||
the derived seed: `mnemonicPhrase` always rebuilds the canonical lowercase
|
||||
sentence from the wordlist, and that is what `mnemonicToSeed` hashes.
|
||||
|
||||
## How applications use it
|
||||
|
||||
An application defines the derivation path. For SimpleX names, one seed per
|
||||
device and one account per name — see `Simplex.Chat.Wallet` in simplex-chat:
|
||||
|
||||
```haskell
|
||||
m <- either fail pure $ parseMnemonic phrase
|
||||
mk <- either fail pure $ masterKey (mnemonicToSeed m "")
|
||||
xk <- either fail pure $ derivePath mk (ethereumPath account 0)
|
||||
let addr = addressFromPrivateKey (xkKey xk)
|
||||
```
|
||||
|
||||
## libsecp256k1 C API mapping
|
||||
|
||||
```c
|
||||
secp256k1_context_create(SECP256K1_CONTEXT_NONE) /* once, then _randomize */
|
||||
secp256k1_ec_seckey_verify(ctx, seckey)
|
||||
secp256k1_ec_pubkey_create(ctx, pubkey, seckey)
|
||||
secp256k1_ec_pubkey_serialize(ctx, output, outputlen, pubkey, flags)
|
||||
secp256k1_ec_seckey_tweak_add(ctx, seckey, tweak)
|
||||
```
|
||||
|
||||
Every call is a pure function of its arguments, which is why the module exposes
|
||||
a pure API over `unsafePerformIO`. The context is created and blinded once at
|
||||
first use; randomization is a side-channel countermeasure that affects no
|
||||
output and mutates nothing afterwards, so one shared context is safe across
|
||||
threads.
|
||||
|
||||
`secp256k1_ec_seckey_tweak_add` returns 0 exactly when BIP-32 says "proceed with
|
||||
the next index" (tweak out of range, or a zero result), which is why
|
||||
`privateKeyTweakAdd` returns `Maybe` and `derivePath` can surface it.
|
||||
|
||||
libsecp256k1 never reads OS entropy — the context blinding seed is supplied by
|
||||
the caller. So unlike
|
||||
libbbs it raises no `getentropy` / ITMS-90338 concern on iOS, and needs 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
|
||||
include-dirs: cbits/libsecp256k1{,/include,/src}
|
||||
```
|
||||
|
||||
Built **without** its autotools config header, and with no `-D` of our own.
|
||||
Every knob has an `#ifndef` default in the headers, and the checked-in
|
||||
precomputed tables are generated for those defaults. `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` with the 10x26 field
|
||||
and 8x32 scalar backends when `__SIZEOF_INT128__` is absent.
|
||||
|
||||
`include-dirs` order matters: libsecp256k1's directories come last, after
|
||||
libbbs and blst. There are no filename collisions between the three (checked),
|
||||
and C quoted includes prefer the including file's own directory anyway, but the
|
||||
ordering keeps it that way if any library later adds a generically-named header.
|
||||
|
||||
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` need no nix entry, which is why blst and libbbs have none either.
|
||||
|
||||
### Cross-compilation status
|
||||
|
||||
Verified by building simplex-chat through its flake:
|
||||
|
||||
| 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 (needs a darwin host) |
|
||||
|
||||
`aarch64-android` is the meaningful pass: it proves the C both cross-compiles
|
||||
and links into the artifact the app actually ships.
|
||||
|
||||
`armv7a-android` gets far enough to prove the 32-bit path compiles — that is,
|
||||
libsecp256k1's `SECP256K1_WIDEMUL_INT64` fallback builds under the NDK — but the
|
||||
build then dies 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` out of `libHSdirectory` and segfaults. It is
|
||||
unrelated to this work: none of these modules use Template Haskell, and
|
||||
simplexmq (which does) builds for armv7a fine. So 32-bit *linking* remains
|
||||
unproven, though there is no plausible mechanism by which it would fail given
|
||||
aarch64 links and the 32-bit objects compile.
|
||||
|
||||
`x86_64-windows` fails while bootstrapping the mingw cross-GHC, long before any
|
||||
of our code is considered: 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 program that calls into both the core and
|
||||
the recovery module — that isolates the C question from the Haskell build
|
||||
entirely.
|
||||
|
||||
## Tests
|
||||
|
||||
`tests/CoreTests/EthCryptoTests.hs`, 75 examples. Everything is checked against
|
||||
published vectors rather than our own output:
|
||||
|
||||
- **BIP-39** — all 24 official English vectors from
|
||||
`trezor/python-mnemonic/vectors.json`, entropy → mnemonic → entropy and
|
||||
mnemonic → seed with the `TREZOR` passphrase.
|
||||
- **BIP-32** — spec test vectors 1 (all six chains) and 2. Expected private keys
|
||||
and chain codes were decoded from the published `xprv` base58 strings, since
|
||||
we do not implement xprv serialization.
|
||||
- **EIP-55** — the four 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.
|
||||
- Keccak-256 against SHA3-256, so the padding-byte confusion cannot pass.
|
||||
- Negative cases: zero, short and out-of-range private keys, bad BIP-39
|
||||
checksums and word counts, out-of-range seeds, 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 are not just
|
||||
our implementation agreeing with itself.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Owned-by: the names an address owns
|
||||
|
||||
A recovered seed does not say how many of its accounts have been used. Until it
|
||||
does, a restored device cannot take a fresh account without risking one its
|
||||
owner already has, and cannot find the names it already holds. This adds the
|
||||
question the protocol could not ask: what does this address own.
|
||||
|
||||
This is Workstream B of the SimpleX names v2 plan, after the Ethereum crypto
|
||||
primitives. The client consumer is the wallet recovery scan in simplex-chat.
|
||||
|
||||
## What this delivers
|
||||
|
||||
```
|
||||
GET /v2/owned-by/<address>?offset=N resolver, JSON OwnedNames
|
||||
ROWN <address> <offset> -> ROWND SMP v23 (nameOwnedSMPVersion)
|
||||
ownedSimplexNames agent, over ownedNames in the server
|
||||
```
|
||||
|
||||
The agent returns the relay it used alongside the answer, so a caller can pass
|
||||
it back as used and ask the next account elsewhere. A relay that cannot answer
|
||||
at all - too old for ROWN, unreachable, or with no resolver of its own behind it
|
||||
- is not the end of the lookup: it asks another, so a scan does not fail
|
||||
wholesale on the one relay it happened to draw during a version rollout. Sending every account of a
|
||||
seed to one relay would tell that relay the accounts belong to one wallet; the
|
||||
scan therefore asks one account per relay, and `getNextNameServer` avoids the
|
||||
hosts already used where the configured set allows. `proxiedSMPRelayVersion` is
|
||||
pinned to the current version so ROWN can be proxied at all — a scan that falls
|
||||
back to a direct session hands the relay its address with its IP. The pin is
|
||||
necessary but not sufficient: under the default `SPMUnknown` proxy mode a
|
||||
configured names server is a known host, so the scan reaches it directly, as
|
||||
RSLV already does.
|
||||
|
||||
Enumeration is read off the ERC-721 registrar (`balanceOf`,
|
||||
`tokenOfOwnerByIndex`, `labelOf`), not from registration logs: the token is the
|
||||
name, so a name acquired by transfer counts. Each name is then answered with the
|
||||
`NameResponse` resolving it gives, so the client that scans can list the names
|
||||
and act on them without asking again — the reason the per-name cost is worth
|
||||
paying. The registrar does not maintain enumeration on expiry, so a name past
|
||||
its grace is still enumerated; it answers as available, naming nothing the
|
||||
account holds, so it is left out while still counting towards `inUse`.
|
||||
|
||||
## What `inUse` actually answers
|
||||
|
||||
`inUse` is `holds a name now, or nonce > 0, or balance > 0`, on the one chain
|
||||
the resolver is configured with. Holding a name is only one way for an account
|
||||
to be in use: an account that was funded, or ever sent a transaction, is in use
|
||||
with no name, and a scan that reads names alone hands out an account its owner
|
||||
is already using. Within that scope the signal is sound — an EOA's balance can
|
||||
only fall through its own transaction, which bumps the nonce, so "funded then
|
||||
drained" still reads as used, and an EIP-7702 delegation leaves the authority's
|
||||
nonce incremented, so it needs no separate code check.
|
||||
|
||||
It does not see:
|
||||
|
||||
- **Accounts holding only other tokens.** An EOA can hold ERC-20s or NFTs with
|
||||
nonce 0 and no ether. ERC-20 has no reverse index, so answering this needs
|
||||
`eth_getLogs` over Transfer topics for all history, which providers cap, or an
|
||||
external indexer. Out of scope here.
|
||||
- **Accounts used on another chain.** The same key is the same address on every
|
||||
L2 and sidechain; this chain's nonce and balance stay 0.
|
||||
- **History, as opposed to current state.** `balanceOf` is present ownership. A
|
||||
name registered for an account by a controller — which leaves the owner at
|
||||
nonce 0 — and later transferred away, or lapsed and re-registered by someone
|
||||
else, leaves no trace.
|
||||
|
||||
The cost is bounded. A name-holding account is always found, so no account that
|
||||
holds a name is ever mislabelled. A missed account matters only as a gap-limit
|
||||
reset: the scan can stop up to `scanGapLimit` indexes early and not reach a name
|
||||
beyond that. That is recovery incompleteness, recoverable by scanning again on a
|
||||
better signal, not loss. In the other direction a dust transfer marks an account
|
||||
used; the indexes come from hardened derivation with no published xpub, so they
|
||||
cannot be enumerated to be dusted.
|
||||
|
||||
## What is deliberately absent
|
||||
|
||||
- **No token or indexer integration**, per the limits above.
|
||||
- **Nonce and balance are not carried over SMP.** The resolver reports them, and
|
||||
`OwnedNames` keeps only the names, the flag and the cursor, so a client cannot
|
||||
yet see why an account is in use.
|
||||
- **No caching, and no batching.** One owned-by is a `balanceOf` per configured
|
||||
TLD, then for each name the same reads resolving it takes - roughly fifteen
|
||||
`eth_call`s - plus the nonce and balance, all issued one at a time, so it is
|
||||
forked on
|
||||
the server the same way RSLV is and nothing is memoised. The page size trades
|
||||
against both the relay's response cap and its timeout, and it bounds names per
|
||||
registrar rather than per page, so a second TLD doubles a full page.
|
||||
- **Enumeration is not atomic.** `balanceOf` and each `tokenOfOwnerByIndex` are
|
||||
separate calls at `latest`, so a transfer between them can duplicate or skip an
|
||||
entry, and successive pages can straddle blocks. Pinning every call to one
|
||||
block number is the fix if it ever matters for a recovery scan.
|
||||
@@ -1,58 +0,0 @@
|
||||
# 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`.
|
||||
|
||||
## 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.
|
||||
@@ -85,7 +85,7 @@ SMP agent protocol has 2 main parts:
|
||||
|
||||

|
||||
|
||||
The procedure of establishing a duplex connection is explained on the example of Alice (the initiating party) and Bob (the joining party) creating a bi-directional connection consisting of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP routers (which could be the same router). It is shown on the diagram above and has these steps:
|
||||
The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection consisting of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP routers (which could be the same router). It is shown on the diagram above and has these steps:
|
||||
|
||||
1. Alice requests the new connection from the SMP agent A using agent `createConnection` api function.
|
||||
2. Agent A creates an SMP queue on the router (using [SMP protocol](./simplex-messaging.md) `NEW` command) and responds to Alice with the invitation that contains queue information and the encryption keys Bob's agent B should use. The invitation format is described in [Connection link](connection-link-1-time-invitation-and-contact-address).
|
||||
@@ -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. A key sent in reply to another key includes the hash of that key; agents do not reply to such keys.
|
||||
- `agentRatchetInfo` - used to pass additional information when renegotiating double ratchet encryption - sent in `agentRatchetKey` envelope.
|
||||
- `agentMessage` - all other agent messages.
|
||||
|
||||
`agentMessage` contains these parts:
|
||||
@@ -233,8 +233,7 @@ connInfo = *OCTET
|
||||
agentConnInfoReply = %s"D" smpQueues connInfo
|
||||
smpQueues = length 1*newQueueInfo ; NonEmpty list of reply queues
|
||||
agentRatchetInfo = %s"R" ratchetInfo
|
||||
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
|
||||
ratchetInfo = *OCTET
|
||||
|
||||
agentMessage = %s"M" agentMsgHeader aMessage
|
||||
agentMsgHeader = agentMsgId prevMsgHash
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
Revision 1, 2026-10-01
|
||||
|
||||
# SimpleX Network: cryptographic primitive registry
|
||||
|
||||
The cryptographic primitives, constructions and domain-separation strings used by this repository, and which of them new code may use. For the threat model see [Security](./security.md).
|
||||
|
||||
Module aliases follow the codebase: `C` = [Simplex.Messaging.Crypto](../src/Simplex/Messaging/Crypto.hs), `LC` = [Crypto.Lazy](../src/Simplex/Messaging/Crypto/Lazy.hs), `CR` = [Crypto.Ratchet](../src/Simplex/Messaging/Crypto/Ratchet.hs), `SL` = [Crypto.ShortLink](../src/Simplex/Messaging/Crypto/ShortLink.hs), `KEM` = [Crypto.SNTRUP761](../src/Simplex/Messaging/Crypto/SNTRUP761.hs) and [its bindings](../src/Simplex/Messaging/Crypto/SNTRUP761/Bindings.hs), `BBS` = [Crypto.BBS](../src/Simplex/Messaging/Crypto/BBS.hs). Lengths are in bytes.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Policy](#policy)
|
||||
- [Defaults for new code](#defaults-for-new-code)
|
||||
- [Primitives](#primitives)
|
||||
- [SimpleX box constructions](#simplex-box-constructions)
|
||||
- [Domain separation and KDF inputs](#domain-separation-and-kdf-inputs)
|
||||
- [Nonces and IVs](#nonces-and-ivs)
|
||||
- [Implementation sources](#implementation-sources)
|
||||
- [xftp-web (TypeScript)](#xftp-web-typescript)
|
||||
- [Tests](#tests)
|
||||
|
||||
## Policy
|
||||
|
||||
- New code uses the default primitive for its purpose from [Defaults for new code](#defaults-for-new-code).
|
||||
- Using any other primitive, or a primitive marked *restricted* outside its listed purpose, requires a written justification in the pull request and review by a maintainer responsible for cryptography.
|
||||
- *Legacy-only* primitives stay only for the existing wire formats and stored data listed below. Do not add call sites.
|
||||
- Every new HKDF info string or hash prefix must be unique, start with `SimpleX`, and be added to [Domain separation and KDF inputs](#domain-separation-and-kdf-inputs).
|
||||
- Changing a primitive, key length, KDF input, info string or nonce construction changes the wire format. It requires a protocol version and an update of this registry and of the affected `protocol/` specification.
|
||||
|
||||
Status values used below:
|
||||
|
||||
| Status | Meaning |
|
||||
|---|---|
|
||||
| Default | Approved and preferred for new code for this purpose |
|
||||
| Approved | Approved for new code for this purpose when the default does not fit |
|
||||
| Restricted | Approved only for the listed purpose (external interoperability or a single protocol) |
|
||||
| Legacy-only | Kept for existing wire formats or stored data; no new call sites |
|
||||
|
||||
## Defaults for new code
|
||||
|
||||
| Purpose | Default | Functions |
|
||||
|---|---|---|
|
||||
| Signature | Ed25519 | `C.sign'`, `C.verify'` with `C.SEd25519` |
|
||||
| Key agreement | X25519 | `C.generateKeyPair`, `C.dh'` |
|
||||
| Post-quantum KEM | sntrup761 | `KEM.sntrup761Keypair`, `sntrup761Enc`, `sntrup761Dec` |
|
||||
| Hybrid DH + KEM secret | HKDF-SHA512 over `dh \|\| kemSecret` with a new info string, as in `CR.rootKdf` | `C.hkdf` |
|
||||
| Authenticated encryption | XSalsa20-Poly1305 [SimpleX secretbox](#simplex-box-constructions) | `C.sbEncrypt`/`C.sbDecrypt`, `LC.sbEncryptTailTag` for streams |
|
||||
| Authenticated encryption with a DH secret | XSalsa20-Poly1305 [SimpleX crypto_box](#simplex-box-constructions) | `C.cbEncrypt`/`C.cbDecrypt` |
|
||||
| Forward-secret symmetric session | HKDF-SHA512 key chain | `C.sbcInit`, `C.sbcHkdf` |
|
||||
| KDF | HKDF-SHA512 | `C.hkdf` |
|
||||
| Hash commitment, derived identifier | SHA3-256 | `C.sha3_256` |
|
||||
| Certificate fingerprint | SHA-256 X.509 fingerprint | `C.signedFingerprint`, `C.certificateFingerprint` |
|
||||
| Randomness | ChaChaDRG seeded from system entropy | `C.newRandom`, `C.randomBytes` and the `random*` helpers |
|
||||
|
||||
## Primitives
|
||||
|
||||
| Primitive | Status | Purpose and protocol | Implementation | Lengths |
|
||||
|---|---|---|---|---|
|
||||
| Ed25519 | Default | SMP/NTF/XFTP queue and file keys (agent default `rcvAuthAlg`, `sndAuthAlg` in [Agent/Env/SQLite.hs](../src/Simplex/Messaging/Agent/Env/SQLite.hs)), short-link signatures and owner auth (`SL.encodeSign`, `SL.newOwnerAuth`), XRCP identity and session keys ([RemoteControl/Invitation.hs](../src/Simplex/RemoteControl/Invitation.hs)), agent service request signatures, client/service TLS certificates ([Transport/Credentials.hs](../src/Simplex/Messaging/Transport/Credentials.hs)) | crypton `Crypto.PubKey.Ed25519` via `C.sign'`/`C.verify'` | pub 32, priv 32, sig 64; X.509 SPKI 44 |
|
||||
| Ed448 | Approved | Server CA and online certificates (default `signAlgorithm = ED448` in [Server/CLI.hs](../src/Simplex/Messaging/Server/CLI.hs)); accepted for SMP command signatures and TLS | crypton `Crypto.PubKey.Ed448`; server key generation by the `openssl genpkey` CLI | pub 57, priv 57, sig 114; SPKI 69 |
|
||||
| X25519 | Default | Per-queue e2e and server-to-recipient `crypto_box` keys, SMP session DH, command authenticator (SMP v7+), proxy (PRXY/PFWD), notification tokens, XFTP chunk transport, XRCP hello and announcements | crypton `Crypto.PubKey.Curve25519` via `C.dh'` | pub 32, priv 32, secret 32; SPKI 44 |
|
||||
| X448 | Restricted | E2E double ratchet only: X3DH and DH ratchet (`CR.RatchetX448`, E2E v3) | crypton `Crypto.PubKey.Curve448` | pub 56, priv 56, secret 56 |
|
||||
| sntrup761 | Default | PQ KEM in double ratchet (E2E v3, agent v5+) and XRCP v1 hello | Vendored C [cbits/sntrup761.c](../cbits/sntrup761.c) via FFI; randomness from ChaChaDRG through `haskell_rng_func` ([Bindings/RNG.hs](../src/Simplex/Messaging/Crypto/SNTRUP761/Bindings/RNG.hs)) | pk 1158, sk 1763, ct 1039, shared 32 |
|
||||
| XSalsa20-Poly1305, SimpleX crypto_box | Default | SMP message bodies (server to recipient), per-queue e2e envelope, confirmations, proxy forwarding, notifications, XRCP hello, XFTP chunk transport | crypton `Crypto.Cipher.XSalsa`, `Crypto.MAC.Poly1305`; `C.cryptoBox`, `LC.cbInit` | key 32 (DH secret), nonce 24, tag 16 |
|
||||
| XSalsa20-Poly1305, SimpleX secretbox | Default | SMP transport block encryption (SMP v11+), short-link data (SMP v15+), XFTP file encryption, local file encryption ([Crypto/File.hs](../src/Simplex/Messaging/Crypto/File.hs)), XRCP session | same; `C.sbEncrypt`, `LC.sbEncryptTailTag`, `LC.sbInit` | key 32, nonce 24, tag 16 |
|
||||
| crypto_box authenticator | Approved | Deniable sender command authorization with X25519 queue keys (SMP v7+); agent currently creates Ed25519 keys | `C.cbAuthenticate`, `C.cbVerify`: `crypto_box(sha512(msg))` | 80 = 64 + 16 |
|
||||
| AES-256-GCM, 16-byte IV | Legacy-only | Double ratchet header and body (E2E v3, [pqdr.md](./pqdr.md)) | crypton `Crypto.Cipher.AES`, `Crypto.Cipher.Types`; `C.encryptAEAD`, `C.decryptAEAD`, `C.initAEAD`; J0 = GHASH(IV) as NIST SP 800-38D defines for non-96-bit IVs | key 32, IV 16, tag 16; padded header 88 (PQ off) or 2310 (PQ on), `CR.paddedHeaderLen` |
|
||||
| AES-256-GCM, 12-byte IV | Restricted | WebRTC frame encryption in simplex-chat (`Simplex.Chat.Mobile.WebRTC`), for WebCrypto interoperability | `C.encryptAESNoPad`, `C.decryptAESNoPad`, `C.initAEADGCM`, `C.GCMIV` | key 32, IV 12, tag 16 |
|
||||
| HKDF-SHA512 | Default | All KDFs listed in [Domain separation](#domain-separation-and-kdf-inputs) | crypton `Crypto.KDF.HKDF` with `SHA512`; `C.hkdf` (extract + expand) | output per use, at most 255 * 64 |
|
||||
| SHA-256 | Default for X.509 fingerprints, otherwise Restricted | X.509 fingerprints (`C.KeyHash`, server identity, XRCP CA, service certificate hash, SMP v16+), XFTP chunk digests, agent message hashes, `requestCode`, ratchet key dedup hash, security code `codeAD = sha256(rcAD)` | crypton; `C.sha256Hash`, `LC.sha256Hash`, `getFingerprint ... HashSHA256` | 32 |
|
||||
| SHA-512 | Restricted | XFTP file digests and file description hash, input of the crypto_box authenticator | crypton; `C.sha512Hash`, `LC.sha512Hash` | 64 |
|
||||
| SHA-512 (inside sntrup761) | Restricted | sntrup761 internal hash | [cbits/sha512.c](../cbits/sha512.c) calls OpenSSL `SHA512()` from the system `libcrypto` (`extra-libraries: crypto`) | 64 |
|
||||
| SHA3-256 | Default | Short-link key `sha3_256(fixedData)` (SMP v15+), XRCP hybrid secret, service request binding (agent v8) | crypton; `C.sha3_256`, `KEM.kemHybridSecret` | 32 |
|
||||
| SHA3-384 | Restricted | Client-supplied sender ID: first 24 bytes of `sha3_384(corrId)`, computed by the agent (`prepareConnectionLink'`, `newRcvConnSrv`) and the server (`createQueue`) | crypton; `C.sha3_384` | 48, truncated to 24 |
|
||||
| Keccak-256 | Restricted | Label hash for SMP name queries (`NameQuery NQHash`), must match the on-chain registry | crypton `Keccak_256`; `labelHash` in [SimplexName.hs](../src/Simplex/Messaging/SimplexName.hs) | 32 |
|
||||
| MD5 | Legacy-only, non-security | XOR-aggregated queue ID hash `IdsHash` for service subscription reconciliation (SUBS, NSUBS, SOKS, ENDS) | crypton (`C.md5Hash`, `Protocol.queueIdHash`); SQLite UDF `simplex_xor_md5_combine` ([Agent/Store/SQLite.hs](../src/Simplex/Messaging/Agent/Store/SQLite.hs)); PostgreSQL pgcrypto `digest(..., 'md5')` in server and NTF schemas | 16 |
|
||||
| ChaChaDRG | Default | Keys, nonces, correlation IDs, random IDs, sntrup761 randomness | crypton `Crypto.Random`; `C.newRandom` seeds with `drgNew` from system entropy | n/a |
|
||||
| BBS+ over BLS12-381, SHA-256 suite | Restricted | Badge entitlement credentials and proofs ([Crypto/Entitlement.hs](../src/Simplex/Messaging/Crypto/Entitlement.hs)), XFTP storage-time extension | Vendored submodules libbbs and blst (C and assembly) via FFI; randomness from libbbs `getentropy`, `SecRandomCopyBytes` with the `commoncrypto` flag, `BCryptGenRandom` on Windows ([cbits/getentropy_win.c](../cbits/getentropy_win.c)) | sk 32, pk 96, sig 80, proof 272 + 32 per undisclosed message |
|
||||
| ECDSA with SHA-256 (ES256) | Restricted | APNS provider JWT ([Push/APNS.hs](../src/Simplex/Messaging/Notifications/Server/Push/APNS.hs)) | crypton `Crypto.PubKey.ECC.ECDSA.sign`; key read by cryptostore `Crypto.Store.PKCS8`; curve taken from the key file | signature DER `SEQUENCE {r, s}`, base64url |
|
||||
| TLS 1.3 / 1.2 | Restricted | SMP, XFTP, NTF, XRCP transport: `TLS_CHACHA20_POLY1305_SHA256` (1.3), `ECDHE_ECDSA_CHACHA20POLY1305_SHA256` (1.2), groups X448 and X25519, Ed448 and Ed25519 signatures (`defaultSupportedParams`) | `tls` 1.9 | n/a |
|
||||
| TLS for browsers | Restricted | XFTP server with HTTPS credentials (`defaultSupportedParamsHTTPS`: `ciphersuite_strong`, FFDHE, P-521, ECDSA and RSA signatures); SMP server HTTPS requires RSA-4096 (`checkHTTPSCredentials`) | `tls`, `warp-tls` | n/a |
|
||||
| X.509 | Restricted | Certificate chains, fingerprints, signed session keys (`C.signX509`, `C.verifyX509`) | `crypton-x509`, `crypton-x509-validation`, `cryptostore` | n/a |
|
||||
| SQLCipher | Restricted | Agent SQLite database at rest (`PRAGMA key`); cipher parameters are SQLCipher defaults, not set in this repository | `direct-sqlcipher` (git dependency, [cabal.project](../cabal.project)) | n/a |
|
||||
|
||||
## SimpleX box constructions
|
||||
|
||||
`C.cryptoBox` and `LC.sbInit_` compute, for a 32-byte key `k` and 24-byte nonce `n`:
|
||||
|
||||
```
|
||||
k1 = HSalsa20(k, 0^16)
|
||||
subkey = HSalsa20(k1, n[0..16])
|
||||
stream = Salsa20(subkey, n[16..24])
|
||||
polyKey = stream[0..32]
|
||||
ct = msg XOR stream[32..]
|
||||
tag = Poly1305(polyKey, ct)
|
||||
```
|
||||
|
||||
| Construction | Key `k` | Equivalent libsodium call | Layout |
|
||||
|---|---|---|---|
|
||||
| SimpleX crypto_box (`C.cbEncrypt`, `LC.cbInit`) | X25519 shared secret | `crypto_box_easy` (`crypto_box_beforenm` is `HSalsa20(dh, 0^16)`) | strict: `tag \|\| ct`; lazy tail-tag: `ct \|\| tag` |
|
||||
| SimpleX secretbox (`C.sbEncrypt`, `LC.sbInit`, `KEM.kcb*`) | 32-byte symmetric key | `crypto_secretbox_easy` with key `crypto_core_hsalsa20(0^16, k)`, not with `k` | same |
|
||||
|
||||
Padding before encryption: `C.pad` prefixes a 2-byte big-endian length and fills with `#` (message at most 65533 bytes); `LC.pad` prefixes an 8-byte length. `*NoPad` variants skip padding.
|
||||
|
||||
## Domain separation and KDF inputs
|
||||
|
||||
HKDF is `C.hkdf salt ikm info len` (HKDF-SHA512).
|
||||
|
||||
| Info string | Salt | IKM | Output (split) | Function | Use |
|
||||
|---|---|---|---|---|---|
|
||||
| `"SimpleXX3DH"` | 64 zero bytes | `dh1 \|\| dh2 \|\| dh3 [\|\| kemShared]` | 96: `hk`, `nhk`, root key (32 each) | `CR.pqX3dh` | Ratchet initialization, added in E2E v2; KEM secret from E2E v3 (current minimum) |
|
||||
| `"SimpleXVerifyCode"` | 64 zero bytes | same as `SimpleXX3DH` | 32: `rcVCPQ` | `CR.pqX3dh` | Verification code covering all handshake keys, new ratchets |
|
||||
| `"SimpleXRootRatchet"` | root key | `dh [\|\| kemShared]` | 96: root key, chain key, next header key | `CR.rootKdf` | DH/PQ ratchet step |
|
||||
| `"SimpleXChainRatchet"` | empty | chain key | 96: chain key, message key (32 each), message IV (16), header IV (16) | `CR.chainKdf` | Per-message keys |
|
||||
| `"SimpleXSbChainInit"` | SMP session ID | X25519 session secret | 64: two 32-byte chain keys | `C.sbcInit` from `Transport.blockEncryption` | SMP transport block encryption, SMP v11+ |
|
||||
| `"SimpleXSbChainInit"` | empty | XRCP hybrid secret (below) | 64: two 32-byte chain keys | `C.sbcInit` in [RemoteControl/Client.hs](../src/Simplex/RemoteControl/Client.hs) | XRCP v1 session |
|
||||
| `"SimpleXSbChain"` | empty | chain key | 88: chain key (32), secretbox key (32), nonce (24) | `C.sbcHkdf` | Each SMP block and XRCP message |
|
||||
| `"SimpleXContactLink"` | empty | link key (32) | 56: link ID (24), secretbox key (32) | `SL.contactShortLinkKdf` | Contact short links, SMP v15+ |
|
||||
| `"SimpleXInvLink"` | empty | link key (32) | 32: secretbox key | `SL.invShortLinkKdf` | Invitation short links, SMP v15+ |
|
||||
|
||||
Other derivations:
|
||||
|
||||
| Value | Construction | Function | Use |
|
||||
|---|---|---|---|
|
||||
| Service request binding | `sha3_256("SimpleXService" \|\| rcAD)` | `serviceReqBinding` in [Agent.hs](../src/Simplex/Messaging/Agent.hs) | Agent RPC, agent v8 |
|
||||
| BBS header | `"SimpleX badges v1"` | `entitlementBBSHeader` | Badge credentials |
|
||||
| XRCP hybrid secret | `sha3_256(x25519Dh \|\| kemShared)` | `KEM.kemHybridSecret` | XRCP v1 |
|
||||
| Short-link key | `sha3_256(smpEncode FixedLinkData)` | `SL.encodeSignFixedData`, checked in `SL.decryptLinkData` | SMP v15+ |
|
||||
| Sender ID | `take 24 (sha3_384 corrId)` | `prepareConnectionLink'`, `newRcvConnSrv` in Agent.hs; `createQueue` in [Server.hs](../src/Simplex/Messaging/Server.hs) | Client-supplied sender ID with link data, SMP v15+; the server rejects a mismatch to prevent an ID oracle |
|
||||
| Ratchet associated data | `pubKeyBytes sk1 \|\| pubKeyBytes rk1` (raw X448, 112) | `CR.pqX3dh` | AD of every ratchet AEAD |
|
||||
| Security code | `sha256(rcAD)` | `ratchetVerifyCodes` in [AgentStore.hs](../src/Simplex/Messaging/Agent/Store/AgentStore.hs) | Connection verification |
|
||||
| Contact request code | `sha256(smpEncode (k1, k2, kem, sndId))` | `requestCode` in Agent.hs | Contact request binding |
|
||||
| Command authenticator | `crypto_box(sha512(authorized))` | `C.cbAuthenticate` | SMP v7+ |
|
||||
| Queue IDs hash | `xor` of `md5(queueId)` | `Protocol.queueIdsHash` | Service subscriptions |
|
||||
|
||||
`"SimpleXSbChainInit"` and `"SimpleXSbChain"` are shared by SMP block encryption and XRCP. Their outputs are separated only by the IKM (and the salt for `sbcInit`).
|
||||
|
||||
## Nonces and IVs
|
||||
|
||||
| Use | Construction |
|
||||
|---|---|
|
||||
| SMP command authenticator, proxied command | `C.cbNonce corrId`, where `corrId` is a random 24-byte nonce (`C.randomCbNonce`) |
|
||||
| SMP proxied responses | `C.reverseNonce` of the request nonce |
|
||||
| Server-to-recipient message body | `C.cbNonce msgId`; `msgIdBytes = 24` in [Server/Main.hs](../src/Simplex/Messaging/Server/Main.hs) |
|
||||
| Per-queue e2e envelope, short-link data, notifications, XRCP hello, XFTP chunk download | Random 24-byte nonce, sent with the ciphertext |
|
||||
| XFTP file, local encrypted file | Random key and nonce per file (`C.randomSbKey`, `C.randomCbNonce`) |
|
||||
| SMP block, XRCP session messages | Key and nonce from `C.sbcHkdf`, one per block |
|
||||
| Ratchet header, body | 16-byte IVs from `CR.chainKdf`; header IV is sent, body IV is not |
|
||||
| WebRTC frames | 12-byte `C.GCMIV` supplied by the caller in simplex-chat |
|
||||
|
||||
## Implementation sources
|
||||
|
||||
| Source | Version or pin | Provides |
|
||||
|---|---|---|
|
||||
| crypton | 0.34 | Ed25519, Ed448, X25519, X448, XSalsa20, Poly1305, AES-GCM, HKDF, SHA-2, SHA-3, Keccak, MD5, ChaChaDRG, ECDSA |
|
||||
| tls, crypton-x509, crypton-x509-validation, cryptostore | 1.9.0, 1.7.6, 1.6.12, 0.3.0.1 | TLS, X.509, PKCS#8 |
|
||||
| [cbits/sntrup761.c](../cbits/sntrup761.c) | Copy of draft-josefsson-ntruprime-streamlined-00 ([cbits/README.md](../cbits/README.md)) | sntrup761 |
|
||||
| [cbits/sha512.c](../cbits/sha512.c) and system OpenSSL `libcrypto` | system | SHA-512 for sntrup761 |
|
||||
| `cbits/libbbs` submodule ([simplex-chat/libbbs](https://github.com/simplex-chat/libbbs)) | `59a0f4bf` | BBS+ (`bbs_sha256_ciphersuite`), SHA-256, SHAKE256 |
|
||||
| `cbits/blst` submodule ([supranational/blst](https://github.com/supranational/blst)) | `db3defd0` | BLS12-381 (C and `build/assembly.S`, `-D__BLST_PORTABLE__`) |
|
||||
| direct-sqlcipher | git `f814ee68` | SQLCipher |
|
||||
| `openssl` CLI | system | Server CA and certificate key generation (`createServerX509_`) |
|
||||
|
||||
## xftp-web (TypeScript)
|
||||
|
||||
[xftp-web](../xftp-web/) reimplements a subset for the browser XFTP client. It has no AES-GCM, HKDF, SHA-3, MD5, X448 or sntrup761 code.
|
||||
|
||||
| Primitive | Implementation | Haskell counterpart |
|
||||
|---|---|---|
|
||||
| Ed25519 sign, verify, keys | libsodium-wrappers-sumo (`crypto_sign_*`), [src/crypto/keys.ts](../xftp-web/src/crypto/keys.ts) | `C.sign'`, `C.verify'` |
|
||||
| Ed448 verify | `@noble/curves/ed448`, keys.ts `verifyEd448` | `C.verify'` |
|
||||
| X25519 | libsodium `crypto_scalarmult`, `crypto_box_keypair` | `C.dh'` |
|
||||
| SimpleX crypto_box, secretbox, tail-tag streaming | Salsa20 block function written in TypeScript, libsodium `crypto_core_hsalsa20` and `crypto_onetimeauth_*`, [src/crypto/secretbox.ts](../xftp-web/src/crypto/secretbox.ts) | `C.cryptoBox`, `LC.sbInit_` |
|
||||
| crypto_box authenticator | [src/protocol/client.ts](../xftp-web/src/protocol/client.ts) `cbAuthenticate`, `cbVerify` | `C.cbAuthenticate`, `C.cbVerify` |
|
||||
| SHA-256, SHA-512 | libsodium `crypto_hash_sha256`, `crypto_hash_sha512*`, [src/crypto/digest.ts](../xftp-web/src/crypto/digest.ts) | `C.sha256Hash`, `C.sha512Hash` |
|
||||
| Random | WebCrypto `crypto.getRandomValues`; libsodium for key generation | `C.randomBytes` |
|
||||
|
||||
## Tests
|
||||
|
||||
None of the tests below compares against published reference vectors (NIST, RFC 8032, RFC 7748, RFC 5869, NaCl, the sntrup761 draft or the BBS draft). Interoperability is tested between this repository's own implementations.
|
||||
|
||||
| Area | Test module (hspec group) | Kind |
|
||||
|---|---|---|
|
||||
| Ed25519, Ed448, X25519 crypto_box, secretbox, lazy and tail-tag secretbox, AES-GCM 12-byte IV, X.509 key encoding, X.509 chains, sntrup761, BBS+, entitlements, padding | [tests/CoreTests/CryptoTests.hs](../tests/CoreTests/CryptoTests.hs) | Round-trip; fixed lengths for BBS+; fixed bytes for padding |
|
||||
| Double ratchet (X25519 and X448), PQ KEM agreement, AES-GCM 16-byte IV | [tests/AgentTests/DoubleRatchetTests.hs](../tests/AgentTests/DoubleRatchetTests.hs) | Round-trip; decoding of a stored v2 ratchet JSON |
|
||||
| Short links (HKDF info strings, SHA3-256 link key) | [tests/AgentTests/ShortLinkTests.hs](../tests/AgentTests/ShortLinkTests.hs) | Round-trip, tamper rejection |
|
||||
| File encryption | [tests/CoreTests/CryptoFileTests.hs](../tests/CoreTests/CryptoFileTests.hs) | Round-trip |
|
||||
| XRCP session (SHA3-256 hybrid, sb chain) | [tests/RemoteControl.hs](../tests/RemoteControl.hs) | End-to-end |
|
||||
| SMP authenticators, block encryption, `IdsHash` (MD5) | [tests/ServerTests.hs](../tests/ServerTests.hs) | End-to-end; with the PostgreSQL queue store this checks Haskell MD5 against pgcrypto |
|
||||
| Haskell and xftp-web: SHA-256/512, Ed25519, Ed448 identity proof, X25519, DER keys, crypto_box, secretbox, tail-tag, authenticator, file encryption | [tests/XFTPWebTests.hs](../tests/XFTPWebTests.hs) (`XFTP Web Client`) | Byte-identical cross-language output |
|
||||
@@ -17,11 +17,11 @@ note over CC, HC: TLS handshake
|
||||
par
|
||||
note over CC: validate client X509 credentials
|
||||
CC->>CI: session code from tlsUnique
|
||||
CI-->>CC: user confirmation (optional if known)
|
||||
CI-->>CC: user confirmation
|
||||
and
|
||||
note over HC: validate server X509 credentials
|
||||
HC->>HI: session code from tlsUnique
|
||||
HI-->>HC: user confirmation (optional if known)
|
||||
HI-->>HC: user confirmation
|
||||
end
|
||||
|
||||
note over CI, HI: 3. Session verification and protocol negotiation
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
+4
-8
@@ -62,16 +62,14 @@ It is possible to reduce size overhead by using only one KEM agreement and makin
|
||||
|
||||
## Double ratchet with encrypted headers augmented with double PQ KEM
|
||||
|
||||
Algorithm below assumes that in addition to shared secret from the initial key agreement, there will be an encapsulation key available from the initiating party, that sent its keys in the connection invitation to the joining party (see [agent protocol](./agent-protocol.md)). Following the double ratchet specification, the pseudo-code below names the joining party Alice and the initiating party Bob.
|
||||
|
||||
Unlike X3DH prekey bundles, these keys are not reusable keys published for any party to use. The initiating party generates two X448 key pairs and, optionally, a sntrup761 KEM key pair for each connection, sends the public keys in the invitation, and deletes the stored private keys once the ratchet is initialized; only the second X448 private key and the agreed KEM key pair remain in the ratchet state as its initial ratchet keys. The joining party generates its keys when joining and keeps only its KEM key pair, in the ratchet state. The exception is a contact address that publishes ratchet keys in its link data: all requesters use these keys until the address owner rotates them, and the owner keeps the private keys of a few recent generations.
|
||||
Algorithm below assumes that in addition to shared secret from the initial key agreement, there will be an encapsulation key available from the party that published its keys (Bob).
|
||||
|
||||
### Initialization
|
||||
|
||||
The double ratchet initialization is defined in pseudo-code. This pseudo-code is identical to Signal algorithm specification except for that parts that add post-quantum key agreement.
|
||||
|
||||
```
|
||||
// Alice (joining party) received Bob's keys in the invitation and initializes ratchet first
|
||||
// Alice obtained Bob's keys and initializes ratchet first
|
||||
def RatchetInitAlicePQ2HE(state, SK, bob_dh_public_key, shared_hka, shared_nhkb, bob_pq_kem_encapsulation_key):
|
||||
state.DHRs = GENERATE_DH()
|
||||
state.DHRr = bob_dh_public_key
|
||||
@@ -91,7 +89,7 @@ def RatchetInitAlicePQ2HE(state, SK, bob_dh_public_key, shared_hka, shared_nhkb,
|
||||
state.HKr = None
|
||||
state.NHKr = shared_nhkb
|
||||
|
||||
// Bob (initiating party) initializes ratchet second, having received Alice's first message
|
||||
// Bob initializes ratchet second, having received Alice's connection request
|
||||
def RatchetInitBobPQ2HE(state, SK, bob_dh_key_pair, shared_hka, shared_nhkb, bob_pq_kem_key_pair):
|
||||
state.DHRs = bob_dh_key_pair
|
||||
state.DHRr = None
|
||||
@@ -212,8 +210,6 @@ The outer envelope contains the encrypted header (used as associated data for bo
|
||||
|
||||
The message body is encrypted with AES-256-GCM using the message key derived from the sending chain key (`KDF_CK`). The associated data for body encryption is the concatenation of the ratchet associated data and the encoded encrypted header.
|
||||
|
||||
`KDF_CK(CK)` is HKDF-SHA512 with empty salt, `CK` as input key material and info `"SimpleXChainRatchet"`, producing 96 bytes split into the next chain key (32 bytes), the message key (32 bytes), the message body IV (16 bytes, not transmitted) and `headerIV` (16 bytes). Both IVs are used as 16-byte AES-256-GCM IVs, not the 12-byte IVs recommended by NIST SP 800-38D, so the initial counter block is J0 = GHASH(IV || 0^64 || [128]_64) as defined there for non-96-bit IVs. WebCrypto and other conforming implementations compute it when given the full 16-byte IV; truncating the IV to 12 bytes produces different ciphertext.
|
||||
|
||||
```abnf
|
||||
encRatchetMessage = versionedLength encMessageHeader msgAuthTag encMsgBody
|
||||
; encMessageHeader is used as associated data for body decryption: AD = rcAD || encMessageHeader
|
||||
@@ -278,7 +274,7 @@ As SimpleX Messaging Protocol pads messages to a fixed size, using 16kb transpor
|
||||
|
||||
Sharing the initial keys in case of SimpleX Chat it is equivalent to sharing the invitation link. As encapsulation key is large, it may be inconvenient to share it in the link in some contexts, e.g. when QR codes are used.
|
||||
|
||||
It is possible to postpone sharing the encapsulation key until the first message from the joining party (confirmation message in SMP protocol). The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm.
|
||||
It is possible to postpone sharing the encapsulation key until the first message from Alice (confirmation message in SMP protocol), the party sending connection request. The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm.
|
||||
|
||||
Another consideration is pairwise ratchets in groups. Key generation in sntrup761 is quite slow - on slow devices it can be as slow as 10-20 keys per second, so using this primitive in groups larger than 10-20 members would result in slow performance.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ This document describes the cryptographic primitives and threat model for the Si
|
||||
- [SimpleX Messaging Protocol router that proxies the messages to another SMP router](#simplex-messaging-protocol-router-that-proxies-the-messages-to-another-smp-router)
|
||||
- [An attacker who obtained Alice's (decrypted) chat database](#an-attacker-who-obtained-alices-decrypted-chat-database)
|
||||
- [A user's contact](#a-users-contact)
|
||||
- [An attacker who observes the initiating party showing an introduction message to the joining party](#an-attacker-who-observes-the-initiating-party-showing-an-introduction-message-to-the-joining-party)
|
||||
- [An attacker who observes Alice showing an introduction message to Bob](#an-attacker-who-observes-alice-showing-an-introduction-message-to-bob)
|
||||
- [An attacker with Internet access](#an-attacker-with-internet-access)
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ This document describes the cryptographic primitives and threat model for the Si
|
||||
- AES-GCM AEAD cipher,
|
||||
- SHA512-based HKDF for key derivation.
|
||||
|
||||
All primitives in use, their lengths, domain-separation strings and the policy for new code are listed in the [cryptographic primitive registry](./crypto-registry.md).
|
||||
|
||||
|
||||
## Threat Model
|
||||
|
||||
@@ -170,7 +168,7 @@ All primitives in use, their lengths, domain-separation strings and the policy f
|
||||
|
||||
*cannot:*
|
||||
|
||||
- impersonate a sender and send messages to the user whose database was stolen, unless the database contains a new rotation queue that is not secured yet. Doing so requires also compromising the router (to place the message in the queue, that is possible until the Double-Ratchet advances forward) or the user's device at a subsequent time (to place the message in the database).
|
||||
- impersonate a sender and send messages to the user whose database was stolen. Doing so requires also compromising the router (to place the message in the queue, that is possible until the Double-Ratchet advances forward) or the user's device at a subsequent time (to place the message in the database).
|
||||
|
||||
- undetectably communicate at the same time as Alice with her contacts. Doing so would result in the contact getting different messages with repeated IDs.
|
||||
|
||||
@@ -192,15 +190,15 @@ All primitives in use, their lengths, domain-separation strings and the policy f
|
||||
|
||||
- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user.
|
||||
|
||||
### An attacker who observes the initiating party showing an introduction message to the joining party
|
||||
### An attacker who observes Alice showing an introduction message to Bob
|
||||
|
||||
*can:*
|
||||
|
||||
- Impersonate the joining party to the initiating party.
|
||||
- Impersonate Bob to Alice.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Impersonate the initiating party to the joining party.
|
||||
- Impersonate Alice to Bob.
|
||||
|
||||
### An attacker with Internet access
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ Version 21, 2026-07-05
|
||||
- [Resolver commands](#resolver-commands)
|
||||
- [Resolve name command](#resolve-name-command)
|
||||
- [Name record response](#name-record-response)
|
||||
- [Names owned by an address command](#names-owned-by-an-address-command)
|
||||
- [Transport connection with the SMP router](#transport-connection-with-the-SMP-router)
|
||||
- [General transport protocol considerations](#general-transport-protocol-considerations)
|
||||
- [TLS transport encryption](#tls-transport-encryption)
|
||||
@@ -109,7 +110,7 @@ This document describes SMP protocol version 23. Versions 1-5 are discontinued.
|
||||
- v20: public namespaces resolver (RSLV command, RNAME response) — direct or forwarded via PFWD
|
||||
- v21: server public information in handshake
|
||||
- v22: `RNAME` says whether a name can be registered, not only what it resolves to
|
||||
- v23: version in nonces of forwarded commands, random nonces for forwarded responses
|
||||
- v23: the names an address owns (ROWN command, ROWND response) — direct or forwarded via PFWD
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -1121,15 +1122,13 @@ When the client receives PKEY response it MUST validate that:
|
||||
- the fingerprint of the received certificate matches fingerprint in the router address - it mitigates MITM attack by proxy.
|
||||
- the router session key is correctly signed with the received certificate.
|
||||
|
||||
`smpVersionRange` in `PKEY` is not signed.
|
||||
|
||||
The proxy router may respond with error response in case the destination router is not available or in case it has an earlier version that does not support proxied commands.
|
||||
|
||||
#### Send command via proxy
|
||||
|
||||
Sender can send `SKEY` and `SEND` commands via proxy after obtaining the session ID with `PRXY` command (see [Request proxied session](#request-proxied-session)).
|
||||
|
||||
Transmission sent to proxy router should use session ID as entity ID and use a random correlation ID of 24 bytes as a nonce for crypto_box encryption of transmission to the destination router. When `smpVersion` in `PFWD` is 23 or higher, the first 2 bytes of this nonce are XOR-ed with `smpVersion`. The random ephemeral X25519 key to encrypt transmission should be unique per command, and it should be combined with the key sent by the router in the handshake header to proxy and to the client in `PKEY` command.
|
||||
Transmission sent to proxy router should use session ID as entity ID and use a random correlation ID of 24 bytes as a nonce for crypto_box encryption of transmission to the destination router. The random ephemeral X25519 key to encrypt transmission should be unique per command, and it should be combined with the key sent by the router in the handshake header to proxy and to the client in `PKEY` command.
|
||||
|
||||
Encrypted transmission should use the received session ID from the connection between proxy router and destination router in the authorized body.
|
||||
|
||||
@@ -1141,11 +1140,10 @@ commandKey = length x509encoded
|
||||
|
||||
The proxy router will forward the encrypted transmission in `RFWD` command (see below).
|
||||
|
||||
Having received the `RRES` response from the destination router, proxy router will forward `PRES` response to the client. `PRES` response should use the same correlation ID as `PFWD` command. The destination router will use this correlation ID increased by 1 as a nonce for encryption of the response. When `smpVersion` in `PFWD` is 23 or higher, the destination router uses a random nonce instead, and sends it before the encrypted response.
|
||||
Having received the `RRES` response from the destination router, proxy router will forward `PRES` response to the client. `PRES` response should use the same correlation ID as `PFWD` command. The destination router will use this correlation ID increased by 1 as a nonce for encryption of the response.
|
||||
|
||||
```abnf
|
||||
proxyResponse = %s"PRES" SP [responseNonce] <encrypted padded(forwardedResponse, 16226)>
|
||||
responseNonce = %s"0" / (%s"1" 24*24 OCTET) ; from v23
|
||||
proxyResponse = %s"PRES" SP <encrypted padded(forwardedResponse, 16226)>
|
||||
forwardedResponse = *OCTET ; client-encrypted SMP response, decrypted by client using per-command DH secret
|
||||
```
|
||||
|
||||
@@ -1172,7 +1170,7 @@ The shared secret for encrypting transmission bodies between proxy router and de
|
||||
|
||||
|
||||
```abnf
|
||||
relayResponse = %s"RRES" SP [responseNonce] <encrypted(responseTransmission)>
|
||||
relayResponse = %s"RRES" SP <encrypted(responseTransmission)>
|
||||
responseTransmission = fwdCorrId forwardedResponse
|
||||
; fwdCorrId and forwardedResponse defined above in RFWD section
|
||||
```
|
||||
@@ -1457,7 +1455,8 @@ companion REST resolver process (`scripts/resolver/snrc-resolve.py`) that
|
||||
queries the SNRC contract on Ethereum; alternative backings (different chains,
|
||||
DHT, etc.) are valid as long as they expose the documented HTTP shape (`GET
|
||||
/v2/resolve/<query>` returning a `NameRegistration` on 200 for every
|
||||
registration shape, 400 for unknown TLDs, 502 for upstream failures) or
|
||||
registration shape, 400 for unknown TLDs, 502 for upstream failures, and from
|
||||
SMP v23 `GET /v2/owned-by/<address>?offset=N` returning an `ownedNames`) or
|
||||
substitute a different transport returning the same JSON. The resolver API is
|
||||
versioned separately from this protocol: `/v1/resolve/<name>` returns a bare
|
||||
`NameRecord` and is what relays before v22 call as `/resolve/<name>`.
|
||||
@@ -1627,6 +1626,52 @@ The names router caps the resolver response it will accept
|
||||
stays within the SMP proxied transmission budget of 16224 bytes; a response over
|
||||
the cap is `ERR NAME RESOLVER`.
|
||||
|
||||
#### Names owned by an address command
|
||||
|
||||
`ROWN` asks which names an address holds, and whether the account has been used
|
||||
at all, so a device restoring a wallet seed can find the accounts already in
|
||||
use. It is unauthenticated and accepted direct or forwarded, as `RSLV` is, from
|
||||
v23.
|
||||
|
||||
```abnf
|
||||
rown = %s"ROWN" SP address offset
|
||||
address = length "0x" 40HEXDIG ; EIP-55 checksummed, length-prefixed
|
||||
length = 1*1 OCTET ; 42
|
||||
offset = 4*4 OCTET ; Word32, network byte order: where to resume listing
|
||||
```
|
||||
|
||||
```abnf
|
||||
rownd = %s"ROWND" SP ownedNames
|
||||
```
|
||||
|
||||
`ownedNames` is a UTF-8 JSON object consuming the remainder of the transmission.
|
||||
|
||||
| Field | JSON type | Constraints |
|
||||
|---|---|---|
|
||||
| `lastBlockTs` | number | the oldest block any read behind this answer saw |
|
||||
| `names` | array | the names the address holds, each the `nameResponse` resolving it answers with |
|
||||
| `inUse` | boolean | whether the account has been used at all |
|
||||
| `nextOffset` | number | cursor to resume from, absent when the listing is complete |
|
||||
|
||||
Each name is answered in full, as `RSLV` answers it, so a client can list the
|
||||
names and act on them without resolving each one again. Enumeration is not
|
||||
maintained on expiry, so the registrar still enumerates a name past its grace;
|
||||
it answers as `available` and names nothing the account holds, so it is left out
|
||||
of `names` while still counting towards `inUse`.
|
||||
|
||||
As with `RNAME`, a router reads through a node of its own, which can lag. Every
|
||||
name carries the block it was read at, and `lastBlockTs` is the oldest of those
|
||||
and of the enumeration's own block, so a client can tell a badly lagging router
|
||||
even from an answer that carries no names — which is the answer a recovery scan
|
||||
acts on.
|
||||
|
||||
`inUse` is what the registry could see on its own chain — the account's nonce,
|
||||
its balance, and every name it holds, which is not only the names listed here:
|
||||
a later page of a held account still reports `inUse` true with an empty `names`. Holding a name is only one way for an account
|
||||
to be in use, and an account used only for other tokens, or on another chain,
|
||||
reads as unused, so a client MUST NOT treat `inUse` as false meaning the account
|
||||
has never been used.
|
||||
|
||||
## Transport connection with the SMP router
|
||||
|
||||
### General transport protocol considerations
|
||||
|
||||
+4
-19
@@ -71,7 +71,7 @@ The session invitation contains this data:
|
||||
|
||||
Host application decrypts (except the first session) and validates the invitation:
|
||||
- Session signature is valid.
|
||||
- Timestamp of a multicast announcement is not earlier than 3660 seconds before and not later than 3600 seconds after the current time of the host. The host ignores announcements outside of this interval and continues listening.
|
||||
- Timestamp is within some window from the current time.
|
||||
- Long-term key signature is valid.
|
||||
- Long-term CA and signature key are the same as in the first session.
|
||||
- Some version in the offered range is supported.
|
||||
@@ -121,15 +121,9 @@ During TLS handshake, parties validate certificate chains against previously kno
|
||||
|
||||
### Session verification and protocol negotiation
|
||||
|
||||
Once TLS session is established, both the host and controller devices present a "session security code" to the user. The session security code must be a digest of tlsunique channel binding. As it is computed as a digest of the TLS handshake for both the controller and the host, it will validate that the same TLS certificates are used on both sides, and that the same TLS session is established, mitigating the possibility of MITM attack in the connection.
|
||||
Once TLS session is established, both the host and controller devices present a "session security code" to the user who must match them (e.g., visually or via QR code scan) and confirm on the host device. The session security code must be a digest of tlsunique channel binding. As it is computed as a digest of the TLS handshake for both the controller and the host, it will validate that the same TLS certificates are used on both sides, and that the same TLS session is established, mitigating the possibility of MITM attack in the connection.
|
||||
|
||||
To verify the session, the user matches the codes (e.g., visually or via QR code scan) and confirms the session on the host device.
|
||||
|
||||
The user MUST verify the first session with a new controller. The host stores the long-term identity of the controller (CA fingerprint and Ed25519 public key) only after this session is verified. A controller with the stored long-term identity is a known controller.
|
||||
|
||||
The user MAY verify the subsequent sessions with a known controller. In a session that the host confirms without user action, the controller is authenticated only by its long-term identity stored in the first session (see [Threat model](#threat-model)). SimpleX Chat mobile apps require user verification of sessions with known controllers only when the user enables the "Verify connections" option.
|
||||
|
||||
Once the session is confirmed, the host sends HELLO block to the controller.
|
||||
Once the session is confirmed by the user, the host sends HELLO block to the controller.
|
||||
|
||||
XRCP blocks inside TLS are padded to 16384 bytes.
|
||||
|
||||
@@ -311,7 +305,7 @@ to receive: (rcvKey', sk, nonce) = HKDF(rcvKey, "SimpleXSbChain", 88)
|
||||
- prevent host and controller devices from establishing the session
|
||||
|
||||
*cannot:*
|
||||
- same as passive adversary, provided that user visually verified session code out-of-band in the first session with the controller.
|
||||
- same as passive adversary, provided that user visually verified session code out-of-band.
|
||||
|
||||
#### An active adversary with the access to the network:
|
||||
|
||||
@@ -330,15 +324,6 @@ to receive: (rcvKey', sk, nonce) = HKDF(rcvKey, "SimpleXSbChain", 88)
|
||||
*cannot:*
|
||||
- connect to the host or make host connect to itself.
|
||||
|
||||
#### An active adversary with the access to the network who also obtained the long-term private keys of a known controller (e.g., from the controller device):
|
||||
|
||||
*can:*
|
||||
- make host connect to itself instead of the controller.
|
||||
- access any data of the controlled host application, within the capabilities of the provided API, if the host confirms the session without user action.
|
||||
|
||||
*cannot:*
|
||||
- access host application data, provided that user visually verified session code out-of-band.
|
||||
|
||||
#### Compromised controller device:
|
||||
|
||||
*can:*
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Copy to .env (next to docker-compose.yml) only to change a setting; without it
|
||||
# docker-compose.yml uses the values shown here.
|
||||
|
||||
# ============================================================================
|
||||
# Chain settings.
|
||||
# Required settings — the stack will not start without these.
|
||||
# ============================================================================
|
||||
|
||||
# Ethereum network: mainnet (the SNRC `.testing` contracts live on mainnet)
|
||||
@@ -17,16 +14,9 @@ NETWORK=mainnet
|
||||
TRUSTED_NODE_URL=https://mainnet-checkpoint-sync.attestant.io
|
||||
|
||||
# ============================================================================
|
||||
# Optional overrides — leave commented unless you need to change them.
|
||||
# Optional overrides — sensible defaults are baked into docker-compose.yml,
|
||||
# so leave these commented unless you need to change them.
|
||||
# ============================================================================
|
||||
|
||||
# Nimbus NAT (default: any). For a stable public node set an explicit IP:
|
||||
# NAT=extip:1.2.3.4 # your public IPv4: curl -s ifconfig.me
|
||||
|
||||
# Resolver (defaults in the README's "Load and scaling" and "Logs" sections)
|
||||
# SNRC_WORKERS=4 # worker processes; default CPU count, at most 4
|
||||
# SNRC_RPC_TIMEOUT=5 # seconds per request to reth
|
||||
# SNRC_LOG_FORMAT=json # default text (key=value)
|
||||
# SNRC_LOG_COLOR=always # coloured `docker compose logs`
|
||||
# SNRC_LOG_LEVEL=debug # also log health checks and idle connections
|
||||
# SNRC_TRUSTED_PROXIES=172.16.0.0/12 # behind a reverse proxy on the host; see README
|
||||
+40
-98
@@ -19,15 +19,15 @@ against **Ethereum mainnet** (where the `.testing` contracts live):
|
||||
|
||||
## 1. Configure
|
||||
|
||||
The defaults in `docker-compose.yml` work as-is:
|
||||
Edit `.env` — the defaults work as-is; override only if needed:
|
||||
|
||||
```sh
|
||||
NETWORK=mainnet # default
|
||||
TRUSTED_NODE_URL=https://mainnet-checkpoint-sync.attestant.io # default
|
||||
```
|
||||
|
||||
To override these or NAT, copy `.env.example` to `.env` and edit it; `.env` is
|
||||
not committed.
|
||||
Everything else (NAT) has a working default baked into `docker-compose.yml`;
|
||||
uncomment the hints in `.env` only to override.
|
||||
|
||||
## 2. Run
|
||||
|
||||
@@ -93,7 +93,7 @@ curl -s http://127.0.0.1:8000/v2/resolve/foobar.testing | jq
|
||||
| reth p2p | `:30303` tcp/udp | Ethereum sync (open on firewall) |
|
||||
| nimbus p2p | `:9000` tcp/udp | beacon sync (open on firewall) |
|
||||
| nimbus REST | `127.0.0.1:5052` | beacon API |
|
||||
| **resolver** | `127.0.0.1:8000` | SNRC REST (`/v2/resolve`, `/resolve`, `/health`) |
|
||||
| **resolver** | `127.0.0.1:8000` | SNRC REST (`/v2/resolve`, `/v2/owned-by`, `/resolve`, `/health`) |
|
||||
|
||||
## Caveats
|
||||
|
||||
@@ -121,21 +121,13 @@ standalone for local dev (no Docker), via [`uv`](https://docs.astral.sh/uv/):
|
||||
uv run scripts/resolver/service/snrc-resolve.py # defaults to local reth + mainnet .testing
|
||||
```
|
||||
|
||||
Tests and lint run from `scripts/resolver/service`:
|
||||
|
||||
```sh
|
||||
uv run pytest
|
||||
uv run ruff check .
|
||||
```
|
||||
|
||||
Dependencies are locked in `uv.lock`; the Docker image installs exactly those.
|
||||
|
||||
Three routes, versioned separately from the protocol so each only changes when
|
||||
Four routes, versioned separately from the protocol so each only changes when
|
||||
its own shape does:
|
||||
|
||||
| Route | Called by | Answers |
|
||||
|---|---|---|
|
||||
| `/v2/resolve/<query>` | routers from SMP v22 | a `NameRegistration` |
|
||||
| `/v2/owned-by/<address>` | routers from SMP v23 | the names an address owns |
|
||||
| `/resolve/<name>` | routers before SMP v22 | a name record, flat |
|
||||
| `/health` | anyone | readiness |
|
||||
|
||||
@@ -174,6 +166,35 @@ label, so a hashed query cannot be answered with a name. See
|
||||
A subname reports the expiry and grace of the 2LD above it, since that is what
|
||||
bounds its lifetime. A subname nobody created reports as not registered.
|
||||
|
||||
### v2: `/v2/owned-by/<address>?offset=N`
|
||||
|
||||
The body is the SMP protocol's `OwnedNames`: the names the address holds, each
|
||||
the same `NameResponse` `/v2/resolve` answers with, and `inUse`, whether the
|
||||
account has been used at all. The router decodes it as is and forwards it, so
|
||||
the fields are specified with the wire, in the **Names owned by an address
|
||||
command** section of
|
||||
[`protocol/simplex-messaging.md`](../../protocol/simplex-messaging.md).
|
||||
|
||||
Answering each name in full is what lets a caller list and act on them without a
|
||||
second request for each. `lastBlockTs` is the oldest block any of the reads saw,
|
||||
so an answer with no names still says how far behind the node was. Enumeration comes off the registrar's ERC-721 index, so
|
||||
a name acquired by transfer counts; one past its grace is still enumerated but
|
||||
answers as available, so it is left out while still counting towards `inUse`.
|
||||
|
||||
`offset` is the cursor to resume from, and it counts per registrar, so a page
|
||||
holds up to `SNRC_MAX_OWNED` names for each configured TLD. `nextOffset` is the
|
||||
next cursor, or null when the listing is complete. `inUse` covers every name the
|
||||
address holds, not only the page, so a later page reports it true with an empty
|
||||
`names`.
|
||||
|
||||
| Status | Meaning |
|
||||
|---|---|
|
||||
| 200 | the names the address holds, with `inUse` |
|
||||
| 400 | `badAddress`, `badOffset`, `noRegistrarConfigured` |
|
||||
| 502 | `upstreamError` |
|
||||
|
||||
Error bodies carry `address`, a fixed `error` code and a `message`.
|
||||
|
||||
### v1: `/resolve/<name>`
|
||||
|
||||
What routers before SMP v22 call. Its shape is unrelated to v2's: the record is
|
||||
@@ -252,7 +273,7 @@ wrong, `error` and `status` hold the same value, so one field is enough to read.
|
||||
|
||||
`upstreamError` says only which exception type the RPC call raised. The text
|
||||
goes to the resolver's log instead, because `SNRC_RPC` can carry a provider key
|
||||
and the exception can carry the URL it failed on. It is also the answer
|
||||
and urlopen puts the URL it failed on into the message. It is also the answer
|
||||
when a registrar, controller or oracle address has no contract behind it: the
|
||||
empty reply is refused rather than read as zero, which would make every name
|
||||
look free.
|
||||
@@ -384,86 +405,7 @@ To override any of them, set `SNRC_REGISTRY_<TLD>`, `SNRC_REGISTRAR_<TLD>` or
|
||||
`SNRC_CONTROLLER_<TLD>` on the `resolver` service in `docker-compose.yml`, or
|
||||
as env vars when you run the script directly.
|
||||
|
||||
### Load and scaling
|
||||
|
||||
A lookup reads the chain in at most three rounds: the name's status and
|
||||
registry entries, then its record from its resolver, then prices for a name
|
||||
that is free. The node runs the calls of a JSON-RPC batch one after another,
|
||||
so the contract reads of a round go to the chain as one `eth_call` to
|
||||
[Multicall3](https://github.com/mds1/multicall). A node that is slow for a
|
||||
moment therefore delays a lookup a few times, not once per read. Connections to
|
||||
the node are kept open and reused.
|
||||
|
||||
The resolver answers HTTP/1.1, so each smp-server keeps its connections to it
|
||||
open instead of connecting for every lookup. An idle connection is closed after
|
||||
60 s; the smp-server drops its own idle ones after 30 s, so it closes them first.
|
||||
|
||||
Each access log line ends with how long the request took, so slow requests
|
||||
show up in `docker compose logs resolver`.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `SNRC_WORKERS` | CPU count, at most 4 | processes sharing the port. If one exits, the others stop, so the container restarts |
|
||||
| `SNRC_RPC_TIMEOUT` | `5` | seconds to wait for each request to the node; the smp-server gives up after 3 |
|
||||
| `SNRC_MULTICALL` | `0xcA11bde05977b3631167028862bE2a173976CA11` | Multicall3 address. If it does not answer, each round is sent as a plain batch and the log says so once |
|
||||
|
||||
### Logs
|
||||
|
||||
Each event is one line: UTC time, level, event name, then `key=value` fields.
|
||||
|
||||
```
|
||||
2026-09-26T08:06:19.992Z INFO request client=203.0.113.7 worker=12 method=GET path=/v2/resolve/[4fdd…].testing status=200 bytes=701 ms=35
|
||||
2026-09-26T08:06:20.051Z WARN upstream_error name=foobar.testing error=ConnectionRefusedError message="[Errno 111] Connection refused"
|
||||
```
|
||||
|
||||
| Event | Level | Meaning |
|
||||
|---|---|---|
|
||||
| `listening` | info | started: address, workers, RPC endpoint, registries, trusted proxies |
|
||||
| `request` | info | one answered request; `/health` only at debug |
|
||||
| `upstream_error` | warn | a read from the node failed; the caller got 502 |
|
||||
| `multicall_unavailable` | warn | Multicall3 did not answer; rounds are sent as plain batches |
|
||||
| `client_gone` | warn | the caller hung up before the answer, usually an smp-server past its timeout |
|
||||
| `http_error` | warn | a malformed request; idle keep-alive timeouts only at debug |
|
||||
| `request_failed` | error | a request failed unexpectedly, with its traceback |
|
||||
| `worker_exited`, `worker_failed` | error | a worker stopped; the others stop too, so the container restarts |
|
||||
| `stopping` | info | stopped by a signal |
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `SNRC_LOG_FORMAT` | `text` | `text` (`key=value`) or `json`, one object per line |
|
||||
| `SNRC_LOG_COLOR` | `auto` | `auto` colours only a terminal, `always`, `never`. Docker output is no terminal, so set `always` for coloured `docker compose logs` |
|
||||
| `SNRC_LOG_LEVEL` | `info` | `debug` also logs health checks and idle connections closing |
|
||||
| `SNRC_TRUSTED_PROXIES` | none | addresses or CIDRs of reverse proxies whose `X-Forwarded-For` names the client |
|
||||
|
||||
With docker compose, set these, and those in "Load and scaling", in `.env`
|
||||
(`.env.example` lists them).
|
||||
|
||||
Behind a reverse proxy such as Caddy on the host, the resolver sees the Docker
|
||||
network's gateway rather than the client. Trust that gateway, and the logged
|
||||
client is the last `X-Forwarded-For` address no trusted proxy added, so a caller
|
||||
through the proxy cannot claim another address. Everything else on the host that
|
||||
connects to `127.0.0.1:8000`, the smp-servers included, arrives through the same
|
||||
gateway and could set the logged address too; run the proxy in the compose network
|
||||
and trust only its address to avoid that.
|
||||
|
||||
The gateway is
|
||||
`docker network inspect resolver_default --format '{{(index .IPAM.Config 0).Gateway}}'`,
|
||||
with the network named after the compose project. It changes when the network is
|
||||
recreated, as `docker compose down` does, so trust one of, in `.env`:
|
||||
|
||||
- `SNRC_TRUSTED_PROXIES=172.16.0.0/12`: Docker's default pools for compose
|
||||
networks, whatever subnet it picks. This also trusts every other container on the
|
||||
host. Add the gateway too if Docker ever gives the network a `192.168.x.x` one.
|
||||
- a pinned subnet, so the gateway stays fixed, and only that gateway:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
default:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.30.0.0/24
|
||||
gateway: 172.30.0.1
|
||||
```
|
||||
|
||||
in `docker-compose.yml`, with `SNRC_TRUSTED_PROXIES=172.30.0.1`, on a subnet no
|
||||
other network on the host uses.
|
||||
`SNRC_MAX_OWNED` (default 16) caps the names `/v2/owned-by` returns per
|
||||
registrar per page. A router will not read a body over 16000 bytes, and a client
|
||||
cannot ask for a smaller page, so keep it times the number of configured TLDs
|
||||
well under that.
|
||||
|
||||
@@ -38,14 +38,14 @@ services:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
if [ -d /home/user/nimbus-eth2/build/data/${NETWORK:-mainnet}/db ]; then
|
||||
if [ -d /home/user/nimbus-eth2/build/data/${NETWORK}/db ]; then
|
||||
echo "Nimbus data dir already initialised — skipping checkpoint sync";
|
||||
exit 0;
|
||||
fi;
|
||||
/home/user/nimbus-eth2/build/nimbus_beacon_node trustedNodeSync \
|
||||
--network=${NETWORK:-mainnet} \
|
||||
--data-dir=/home/user/nimbus-eth2/build/data/${NETWORK:-mainnet} \
|
||||
--trusted-node-url=${TRUSTED_NODE_URL:-https://mainnet-checkpoint-sync.attestant.io} \
|
||||
--network=${NETWORK} \
|
||||
--data-dir=/home/user/nimbus-eth2/build/data/${NETWORK} \
|
||||
--trusted-node-url=${TRUSTED_NODE_URL} \
|
||||
--backfill=false
|
||||
restart: "no"
|
||||
|
||||
@@ -69,8 +69,8 @@ services:
|
||||
echo "Reth data already initialised — skipping snapshot download";
|
||||
exit 0;
|
||||
fi;
|
||||
echo "Downloading Reth ${NETWORK:-mainnet} --minimal snapshot...";
|
||||
reth download --datadir /data --chain ${NETWORK:-mainnet} --minimal && \
|
||||
echo "Downloading Reth ${NETWORK} --minimal snapshot...";
|
||||
reth download --datadir /data --chain ${NETWORK} --minimal && \
|
||||
touch /data/.snapshot-done && \
|
||||
echo "Snapshot download complete"
|
||||
restart: "no"
|
||||
@@ -92,7 +92,7 @@ services:
|
||||
command: >
|
||||
node
|
||||
--datadir /data
|
||||
--chain ${NETWORK:-mainnet}
|
||||
--chain ${NETWORK}
|
||||
--minimal
|
||||
--authrpc.jwtsecret /jwt/jwt.hex
|
||||
--authrpc.addr 0.0.0.0 --authrpc.port 8551
|
||||
@@ -117,8 +117,8 @@ services:
|
||||
- "9000:9000/udp"
|
||||
- "127.0.0.1:5052:5052"
|
||||
command: >
|
||||
--network=${NETWORK:-mainnet}
|
||||
--data-dir=/home/user/nimbus-eth2/build/data/${NETWORK:-mainnet}
|
||||
--network=${NETWORK}
|
||||
--data-dir=/home/user/nimbus-eth2/build/data/${NETWORK}
|
||||
--el=http://reth:8551
|
||||
--jwt-secret=/jwt/jwt.hex
|
||||
--non-interactive
|
||||
@@ -131,7 +131,6 @@ services:
|
||||
# smp-server points its [NAMES] resolver_endpoint at this URL.
|
||||
# To change the host port, edit the LEFT side of the port mapping below.
|
||||
resolver:
|
||||
image: snrc-resolve:local
|
||||
build:
|
||||
context: ./service
|
||||
dockerfile: Dockerfile
|
||||
@@ -157,18 +156,8 @@ services:
|
||||
# SNRC_REGISTRAR_SIMPLEX: 0x...
|
||||
# SNRC_CONTROLLER_TESTING: 0x...
|
||||
# SNRC_CONTROLLER_SIMPLEX: 0x...
|
||||
# Set in .env (see .env.example); unset or empty means the resolver's default.
|
||||
SNRC_WORKERS: ${SNRC_WORKERS:-}
|
||||
SNRC_RPC_TIMEOUT: ${SNRC_RPC_TIMEOUT:-}
|
||||
SNRC_LOG_FORMAT: ${SNRC_LOG_FORMAT:-}
|
||||
SNRC_LOG_COLOR: ${SNRC_LOG_COLOR:-}
|
||||
SNRC_LOG_LEVEL: ${SNRC_LOG_LEVEL:-}
|
||||
SNRC_TRUSTED_PROXIES: ${SNRC_TRUSTED_PROXIES:-}
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
# a line per lookup; the local driver keeps 5 rotated files of 20 MB
|
||||
logging:
|
||||
driver: local
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.venv
|
||||
**/__pycache__
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
test_snrc_resolve.py
|
||||
@@ -1 +0,0 @@
|
||||
3.13
|
||||
@@ -1,10 +1,9 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# ---------- builder ----------
|
||||
# The official uv image (Astral) on top of a slim Python base installs the
|
||||
# locked dependencies into a portable .venv for the runtime stage.
|
||||
# Pinned to the uv that wrote uv.lock (its bookworm images stopped at 0.9.30), on the
|
||||
# runtime's Debian release, so the .venv's Python path exists there.
|
||||
FROM ghcr.io/astral-sh/uv:0.12.16-python3.13-trixie-slim AS builder
|
||||
# Use the official uv image (Astral) on top of a slim Python base.
|
||||
# uv resolves and installs the lockfile-free pyproject.toml in seconds and
|
||||
# produces a portable .venv we can copy into the runtime stage.
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
|
||||
|
||||
ENV UV_LINK_MODE=copy \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
@@ -13,17 +12,17 @@ ENV UV_LINK_MODE=copy \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies first (separate layer), exactly as locked: script edits don't bust this cache.
|
||||
COPY pyproject.toml uv.lock .python-version ./
|
||||
# Install deps first (separate layer) — script edits won't bust this cache.
|
||||
COPY pyproject.toml ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-install-project
|
||||
uv sync --no-dev --no-install-project
|
||||
|
||||
# Script is added after the dep layer for cache friendliness.
|
||||
COPY snrc-resolve.py ./
|
||||
|
||||
# ---------- runtime ----------
|
||||
# Slim runtime — only the venv + script. No uv, no apt.
|
||||
FROM python:3.13-slim-trixie AS runtime
|
||||
FROM python:3.13-slim AS runtime
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
@@ -34,15 +33,15 @@ RUN groupadd --system --gid 10001 snrc && \
|
||||
useradd --system --uid 10001 --gid snrc --no-create-home --shell /usr/sbin/nologin snrc
|
||||
|
||||
WORKDIR /app
|
||||
# owned by root, so the service cannot rewrite its own code
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=builder --chown=snrc:snrc /app /app
|
||||
|
||||
USER snrc:snrc
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Liveness check hits the script's own /health route. Three failures mark the
|
||||
# container unhealthy; compose does not restart it for that, but `docker ps` shows it.
|
||||
# Liveness check hits the script's own /health route. ThreadingHTTPServer is
|
||||
# fast enough that 3s is generous for a localhost probe; restart if it stops
|
||||
# responding entirely.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD ["python", "-c", "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).status == 200 else 1)"]
|
||||
|
||||
|
||||
@@ -9,14 +9,5 @@ dependencies = [
|
||||
"eth-hash[pycryptodome]>=0.7",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8",
|
||||
"ruff>=0.6",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E9", "F", "B"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Generated
-156
@@ -1,156 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eth-hash"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/f5/c67fc24f2f676aa9b7ab29679d44f113f314c817207cd4319353356f62da/eth_hash-0.8.0.tar.gz", hash = "sha256:b009752b620da2e9c7668014849d1f5fadbe4f138603f1871cc5d4ca706896b1", size = 12225, upload-time = "2026-03-25T16:36:55.099Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/87/b36792150ca0b28e4df683a34be15a61461ca0e349e5b5cf3ec8f694edb9/eth_hash-0.8.0-py3-none-any.whl", hash = "sha256:523718a51b369ab89866b929a5c93c52978cd866ea309192ad980dd8271f9fac", size = 7965, upload-time = "2026-03-25T16:36:54.205Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
pycryptodome = [
|
||||
{ name = "pycryptodome" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodome"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.16.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snrc-resolve"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "eth-hash", extra = ["pycryptodome"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "eth-hash", extras = ["pycryptodome"], specifier = ">=0.7" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pytest", specifier = ">=8" },
|
||||
{ name = "ruff", specifier = ">=0.6" },
|
||||
]
|
||||
+19
-6
@@ -1,7 +1,7 @@
|
||||
cabal-version: 3.0
|
||||
|
||||
name: simplexmq
|
||||
version: 7.1.0.10
|
||||
version: 7.1.0.8
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -21,6 +21,7 @@ license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
src/Simplex/Messaging/Crypto/BIP39/english.txt
|
||||
CHANGELOG.md
|
||||
cbits/sha512.h
|
||||
cbits/sntrup761.h
|
||||
@@ -31,6 +32,8 @@ extra-source-files:
|
||||
cbits/blst/**/*.asm
|
||||
cbits/libbbs/**/*.c
|
||||
cbits/libbbs/**/*.h
|
||||
cbits/libsecp256k1/**/*.c
|
||||
cbits/libsecp256k1/**/*.h
|
||||
apps/common/Web/static/index.html
|
||||
apps/common/Web/static/link.html
|
||||
apps/common/Web/static/media/apk_icon.png
|
||||
@@ -135,7 +138,11 @@ 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.Entitlement
|
||||
Simplex.Messaging.Crypto.Secp256k1
|
||||
Simplex.Messaging.Crypto.SNTRUP761
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
@@ -144,6 +151,8 @@ library
|
||||
Simplex.Messaging.Crypto.ShortLink
|
||||
Simplex.Messaging.Encoding
|
||||
Simplex.Messaging.Encoding.String
|
||||
Simplex.Messaging.Eth.Address
|
||||
Simplex.Messaging.Eth.Keccak
|
||||
Simplex.Messaging.Names.Record
|
||||
Simplex.Messaging.Notifications.Client
|
||||
Simplex.Messaging.Notifications.Protocol
|
||||
@@ -194,8 +203,6 @@ library
|
||||
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260411_service_certs
|
||||
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
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Messaging.Agent.Store.SQLite
|
||||
@@ -250,9 +257,6 @@ library
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260411_service_certs
|
||||
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.Util
|
||||
if flag(client_postgres) || flag(server_postgres)
|
||||
exposed-modules:
|
||||
@@ -331,6 +335,10 @@ library
|
||||
cbits/blst/src
|
||||
cbits/libbbs/include
|
||||
cbits/libbbs/src
|
||||
cbits/libsecp256k1
|
||||
cbits/libsecp256k1/include
|
||||
cbits/libsecp256k1/src
|
||||
-- libsecp256k1 is built without its autotools config header: every knob has an #ifndef default matching the checked-in tables
|
||||
cc-options: -D__BLST_PORTABLE__
|
||||
if flag(commoncrypto)
|
||||
cc-options: -DBBS_CRYPTO_CC
|
||||
@@ -345,6 +353,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:
|
||||
@@ -372,6 +383,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.*
|
||||
@@ -549,6 +561,7 @@ test-suite simplexmq-test
|
||||
CoreTests.CryptoFileTests
|
||||
CoreTests.CryptoTests
|
||||
CoreTests.EncodingTests
|
||||
CoreTests.EthCryptoTests
|
||||
CoreTests.MsgStoreTests
|
||||
CoreTests.RetryIntervalTests
|
||||
CoreTests.SOCKSSettings
|
||||
|
||||
@@ -17,16 +17,11 @@ module Simplex.FileTransfer.Agent
|
||||
toFSFilePath,
|
||||
-- Receiving files
|
||||
xftpReceiveFile',
|
||||
xftpPrepareReceiveFile',
|
||||
xftpStartReceiveFile',
|
||||
xftpDeleteRcvFile',
|
||||
xftpDeleteRcvFiles',
|
||||
-- Sending files
|
||||
xftpSendFile',
|
||||
xftpSendDescription',
|
||||
xftpPrepareSendFile',
|
||||
xftpPrepareSendDescription',
|
||||
xftpStartSendFile',
|
||||
deleteSndFileInternal,
|
||||
deleteSndFilesInternal,
|
||||
deleteSndFileRemote,
|
||||
@@ -132,12 +127,7 @@ closeXFTPAgent a = do
|
||||
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
|
||||
|
||||
xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AM RcvFileId
|
||||
xftpReceiveFile' c userId vfd cfArgs approvedRelays = do
|
||||
fId <- xftpPrepareReceiveFile' c userId vfd cfArgs approvedRelays
|
||||
fId <$ xftpStartReceiveFile' c fId
|
||||
|
||||
xftpPrepareReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AM RcvFileId
|
||||
xftpPrepareReceiveFile' c userId (ValidFileDescription fd@FileDescription {redirect}) cfArgs approvedRelays = do
|
||||
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs approvedRelays = do
|
||||
g <- asks random
|
||||
prefixPath <- lift $ getPrefixPath "rcv.xftp"
|
||||
createDirectory prefixPath
|
||||
@@ -147,7 +137,7 @@ xftpPrepareReceiveFile' c userId (ValidFileDescription fd@FileDescription {redir
|
||||
lift $ createDirectory =<< toFSFilePath relTmpPath
|
||||
lift $ createEmptyFile =<< toFSFilePath relSavePath
|
||||
let saveFile = CryptoFile relSavePath cfArgs
|
||||
case redirect of
|
||||
fId <- case redirect of
|
||||
Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile approvedRelays
|
||||
Just _ -> do
|
||||
-- prepare description paths
|
||||
@@ -159,11 +149,8 @@ xftpPrepareReceiveFile' c userId (ValidFileDescription fd@FileDescription {redir
|
||||
let saveFileRedirect = CryptoFile relSavePathRedirect $ Just cfArgsRedirect
|
||||
-- create download tasks
|
||||
withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile approvedRelays
|
||||
|
||||
xftpStartReceiveFile' :: AgentClient -> RcvFileId -> AM ()
|
||||
xftpStartReceiveFile' c rcvFileEntityId = do
|
||||
srvs <- withStore c (`startPreparedRcvFile` rcvFileEntityId)
|
||||
lift $ forM_ srvs $ void . getXFTPRcvWorker True c . Just
|
||||
forM_ chunks (downloadChunk c)
|
||||
pure fId
|
||||
|
||||
downloadChunk :: AgentClient -> FileChunk -> AM ()
|
||||
downloadChunk c FileChunk {replicas = (FileChunkReplica {server} : _)} = do
|
||||
@@ -366,11 +353,6 @@ notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, AEvt (sAEnti
|
||||
|
||||
xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> Maybe Word32 -> AM SndFileId
|
||||
xftpSendFile' c userId file numRecipients storageHours = do
|
||||
fId <- xftpPrepareSendFile' c userId file numRecipients storageHours
|
||||
fId <$ xftpStartSendFile' c fId
|
||||
|
||||
xftpPrepareSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> Maybe Word32 -> AM SndFileId
|
||||
xftpPrepareSendFile' c userId file numRecipients storageHours = do
|
||||
g <- asks random
|
||||
prefixPath <- lift $ getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
@@ -378,15 +360,12 @@ xftpPrepareSendFile' c userId file numRecipients storageHours = do
|
||||
key <- atomically $ C.randomSbKey g
|
||||
nonce <- atomically $ C.randomCbNonce g
|
||||
-- saving absolute filePath will not allow to restore file encryption after app update, but it's a short window
|
||||
withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce Nothing storageHours
|
||||
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce Nothing storageHours
|
||||
lift . void $ getXFTPSndWorker True c Nothing
|
||||
pure fId
|
||||
|
||||
xftpSendDescription' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Int -> AM SndFileId
|
||||
xftpSendDescription' c userId vfd numRecipients = do
|
||||
fId <- xftpPrepareSendDescription' c userId vfd numRecipients
|
||||
fId <$ xftpStartSendFile' c fId
|
||||
|
||||
xftpPrepareSendDescription' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Int -> AM SndFileId
|
||||
xftpPrepareSendDescription' c userId (ValidFileDescription fdDirect@FileDescription {size, digest}) numRecipients = do
|
||||
xftpSendDescription' c userId (ValidFileDescription fdDirect@FileDescription {size, digest}) numRecipients = do
|
||||
g <- asks random
|
||||
prefixPath <- lift $ getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
@@ -397,12 +376,9 @@ xftpPrepareSendDescription' c userId (ValidFileDescription fdDirect@FileDescript
|
||||
liftError (FILE . FILE_IO . show) $ CF.writeFile file (LB.fromStrict $ strEncode fdDirect)
|
||||
key <- atomically $ C.randomSbKey g
|
||||
nonce <- atomically $ C.randomCbNonce g
|
||||
withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce (Just RedirectFileInfo {size, digest}) Nothing
|
||||
|
||||
xftpStartSendFile' :: AgentClient -> SndFileId -> AM ()
|
||||
xftpStartSendFile' c sndFileEntityId = do
|
||||
withStore c (`startPreparedSndFile` sndFileEntityId)
|
||||
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce (Just RedirectFileInfo {size, digest}) Nothing
|
||||
lift . void $ getXFTPSndWorker True c Nothing
|
||||
pure fId
|
||||
|
||||
resumeXFTPSndWork :: AgentClient -> Maybe XFTPServer -> AM' ()
|
||||
resumeXFTPSndWork = void .: getXFTPSndWorker False
|
||||
@@ -568,10 +544,9 @@ 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}
|
||||
|
||||
@@ -80,7 +80,7 @@ import Simplex.Messaging.Protocol
|
||||
tParse,
|
||||
)
|
||||
import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
|
||||
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) (eitherToMaybe $ C.cbNonce $ bs corrId) tForAuth
|
||||
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth False (Just pKey) (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)
|
||||
|
||||
@@ -25,6 +25,7 @@ import Control.Monad.Reader
|
||||
import Control.Monad.Trans.Except
|
||||
import qualified Data.Aeson as J
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Base64.URL as B64
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
@@ -593,7 +594,7 @@ processXFTPRequest ent HTTP2Body {bodyPart} = \case
|
||||
\used -> let used' = used + fromIntegral size in if used' <= quota then (True, used') else (False, used)
|
||||
receive = do
|
||||
path <- asks $ filesPath . config
|
||||
let fPath = path </> serverFileName senderId
|
||||
let fPath = path </> B.unpack (B64.encode $ unEntityId senderId)
|
||||
receiveChunk (XFTPRcvChunkSpec fPath size digest) >>= \case
|
||||
Right () -> do
|
||||
stats <- asks serverStats
|
||||
|
||||
@@ -17,7 +17,6 @@ module Simplex.FileTransfer.Server.StoreLog
|
||||
logDeleteFile,
|
||||
logBlockFile,
|
||||
logAckFile,
|
||||
serverFileName,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -25,7 +24,6 @@ import Control.Applicative (optional, (<|>))
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Base64.URL as B64
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Composition ((.:), (.::.))
|
||||
@@ -37,11 +35,10 @@ import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BlockingInfo, EntityId (..), RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Protocol (BlockingInfo, RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (ServerEntityStatus (..))
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import System.FilePath (takeFileName)
|
||||
import System.IO
|
||||
|
||||
data FileStoreLogRecord
|
||||
@@ -102,9 +99,6 @@ logBlockFile s fId = logFileStoreRecord s . BlockFile fId
|
||||
logAckFile :: StoreLog 'WriteMode -> RecipientId -> IO ()
|
||||
logAckFile s = logFileStoreRecord s . AckFile
|
||||
|
||||
serverFileName :: SenderId -> FilePath
|
||||
serverFileName = B.unpack . B64.encode . unEntityId
|
||||
|
||||
readWriteFileStore :: FilePath -> STMFileStore -> IO (StoreLog 'WriteMode)
|
||||
readWriteFileStore = readWriteStoreLog readFileStore writeFileStore
|
||||
|
||||
@@ -121,9 +115,7 @@ readFileStore f st = mapM_ (addFileLogRecord . LB.toStrict) . LB.lines =<< LB.re
|
||||
AddFile sId file createdAt expiresAt status
|
||||
| size file > 0 -> addFile st sId file createdAt expiresAt status
|
||||
| otherwise -> pure $ Left SIZE
|
||||
PutFile qId path
|
||||
| takeFileName path == serverFileName qId -> setFilePath st qId path
|
||||
| otherwise -> pure $ Left AUTH
|
||||
PutFile qId path -> setFilePath st qId path
|
||||
AddRecipients sId rcps -> runExceptT $ addRecipients sId rcps
|
||||
DeleteFile sId -> deleteFile st sId
|
||||
BlockFile sId info -> blockFile st sId info True
|
||||
|
||||
@@ -93,8 +93,7 @@ data RcvFile = RcvFile
|
||||
deriving (Show)
|
||||
|
||||
data RcvFileStatus
|
||||
= RFSPrepared
|
||||
| RFSReceiving
|
||||
= RFSReceiving
|
||||
| RFSReceived
|
||||
| RFSDecrypting
|
||||
| RFSComplete
|
||||
@@ -107,7 +106,6 @@ instance ToField RcvFileStatus where toField = toField . textEncode
|
||||
|
||||
instance TextEncoding RcvFileStatus where
|
||||
textDecode = \case
|
||||
"prepared" -> Just RFSPrepared
|
||||
"receiving" -> Just RFSReceiving
|
||||
"received" -> Just RFSReceived
|
||||
"decrypting" -> Just RFSDecrypting
|
||||
@@ -115,7 +113,6 @@ instance TextEncoding RcvFileStatus where
|
||||
"error" -> Just RFSError
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
RFSPrepared -> "prepared"
|
||||
RFSReceiving -> "receiving"
|
||||
RFSReceived -> "received"
|
||||
RFSDecrypting -> "decrypting"
|
||||
@@ -180,8 +177,7 @@ sndFileEncPath :: FilePath -> FilePath
|
||||
sndFileEncPath prefixPath = prefixPath </> "xftp.encrypted"
|
||||
|
||||
data SndFileStatus
|
||||
= SFSPrepared
|
||||
| SFSNew -- db record created
|
||||
= SFSNew -- db record created
|
||||
| SFSEncrypting -- encryption started
|
||||
| SFSEncrypted -- encryption complete
|
||||
| SFSUploading -- all chunk replicas are created on servers
|
||||
@@ -195,7 +191,6 @@ instance ToField SndFileStatus where toField = toField . textEncode
|
||||
|
||||
instance TextEncoding SndFileStatus where
|
||||
textDecode = \case
|
||||
"prepared" -> Just SFSPrepared
|
||||
"new" -> Just SFSNew
|
||||
"encrypting" -> Just SFSEncrypting
|
||||
"encrypted" -> Just SFSEncrypted
|
||||
@@ -204,7 +199,6 @@ instance TextEncoding SndFileStatus where
|
||||
"error" -> Just SFSError
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
SFSPrepared -> "prepared"
|
||||
SFSNew -> "new"
|
||||
SFSEncrypting -> "encrypting"
|
||||
SFSEncrypted -> "encrypted"
|
||||
|
||||
+285
-374
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,7 @@ module Simplex.Messaging.Agent.Client
|
||||
secureGetQueueLink,
|
||||
getQueueLink,
|
||||
resolveName,
|
||||
ownedNames,
|
||||
getNextNameServer,
|
||||
enableQueueNotifications,
|
||||
EnableQueueNtfReq (..),
|
||||
@@ -258,6 +259,8 @@ import Simplex.Messaging.Crypto.BBS (BBSPresHeader (..), BBSPublicKey)
|
||||
import Simplex.Messaging.Crypto.Entitlement (EntitlementCredential, EntitlementProof, generateEntitlementProof)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Eth.Address (Address)
|
||||
import Simplex.Messaging.Names.Record (OwnedNames)
|
||||
import Simplex.Messaging.Notifications.Client
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion)
|
||||
@@ -312,7 +315,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 (..), newNtfCredsSMPVersion)
|
||||
import Simplex.Messaging.Transport (HandshakeError (..), SMPServiceRole (..), SMPVersion, ServiceCredentials (..), SessionId, THClientService' (..), THandleAuth (..), THandleParams (sessionId, thAuth, thVersion, serverInfo), TransportError (..), TransportPeer (..), shortLinksSMPVersion, newNtfCredsSMPVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Credentials
|
||||
import Simplex.Messaging.Util
|
||||
@@ -1261,12 +1264,9 @@ sendOrProxySMPCommand c nm userId destSrv@ProtocolServer {host = destHosts} conn
|
||||
Left e -> throwE e
|
||||
|
||||
ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool
|
||||
ipAddressProtected NetworkConfig {socksProxy, socksMode, hostMode} (ProtocolServer _ hosts _ _)
|
||||
| isJust socksProxy = socksMode == SMAlways || if hostMode == HMPublic then allOnion else anyOnion
|
||||
| otherwise = hostMode == HMOnion && anyOnion
|
||||
ipAddressProtected NetworkConfig {socksProxy, hostMode} (ProtocolServer _ hosts _ _) = do
|
||||
isJust socksProxy || (hostMode == HMOnion && any isOnionHost hosts)
|
||||
where
|
||||
anyOnion = any isOnionHost hosts
|
||||
allOnion = all isOnionHost hosts
|
||||
isOnionHost = \case THOnionHost _ -> True; _ -> False
|
||||
|
||||
withNtfClient :: AgentClient -> NetworkRequestMode -> NtfServer -> EntityId -> ByteString -> (NtfClient -> ExceptT NtfClientError IO a) -> AM a
|
||||
@@ -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 qik
|
||||
shortLink <- mkShortLinkCreds thParams' 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 :: QueueIdsKeys -> AM (Maybe ShortLinkCreds)
|
||||
mkShortLinkCreds QIK {sndId, queueMode, linkId} = case (cqrd, queueMode) of
|
||||
mkShortLinkCreds :: THandleParams SMPVersion 'TClient -> QueueIdsKeys -> AM (Maybe ShortLinkCreds)
|
||||
mkShortLinkCreds thParams' 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,9 +1555,11 @@ 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)
|
||||
@@ -1982,7 +1984,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) msgId
|
||||
decrypt = agentCbDecrypt (rcvDhSecret rq) (C.cbNonce msgId)
|
||||
|
||||
secureQueue :: AgentClient -> NetworkRequestMode -> RcvQueue -> SndPublicAuthKey -> AM ()
|
||||
secureQueue c nm rq@RcvQueue {rcvId, rcvPrivateKey} senderKey =
|
||||
@@ -2030,14 +2032,23 @@ resolveName c nm userId server domain =
|
||||
resolveViaProxy smp proxySess = proxyResolveName smp nm proxySess domain
|
||||
resolveDirectly smp = directResolveName smp nm domain
|
||||
|
||||
-- | Names an address owns, from one names-capable relay, proxied as resolveName is.
|
||||
ownedNames :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> Address -> Word32 -> AM OwnedNames
|
||||
ownedNames c nm userId server addr offset =
|
||||
snd <$> sendOrProxySMPCommand c nm userId server "" "ROWN" NoEntity ownedViaProxy ownedDirectly
|
||||
where
|
||||
ownedViaProxy smp proxySess = proxyOwnedNames smp nm proxySess addr offset
|
||||
ownedDirectly smp = directOwnedNames smp nm addr offset
|
||||
|
||||
-- | Pick a names-capable server for the user (the agent owns server selection,
|
||||
-- accounting for the names role). nameSrvs is opt-in (a plain list); empty means
|
||||
-- no server resolves names - a declared agent error, never a fallback.
|
||||
getNextNameServer :: AgentClient -> UserId -> AM SMPServer
|
||||
getNextNameServer c userId =
|
||||
-- Used servers are avoided where the set allows, operator first: one operator asked about every account learns the wallet.
|
||||
getNextNameServer :: AgentClient -> UserId -> [SMPServer] -> AM SMPServer
|
||||
getNextNameServer c userId usedSrvs =
|
||||
liftIO (TM.lookupIO userId (userServers c :: TMap UserId (UserServers 'PSMP))) >>= \case
|
||||
Just UserServers {nameSrvs} -> case L.nonEmpty nameSrvs of
|
||||
Just srvs -> protoServer <$> pickServer srvs
|
||||
Just srvs -> protoServer . snd <$> getNextServer_ srvs (usedOperatorsHosts srvs usedSrvs)
|
||||
Nothing -> throwE NO_NAME_SERVERS
|
||||
Nothing -> throwE $ INTERNAL "unknown userId - no user servers"
|
||||
|
||||
@@ -2080,9 +2091,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 QueueDrained
|
||||
sendAck :: AgentClient -> RcvQueue -> MsgId -> AM ()
|
||||
sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId =
|
||||
withSMPClient c NRMBackground rq ("ACK:" <> logSecret' (SMP.unMsgId msgId)) $ \smp ->
|
||||
withSMPClient c NRMBackground rq ("ACK:" <> logSecret' msgId) $ \smp ->
|
||||
ackSMPMessage smp rcvPrivateKey rcvId msgId
|
||||
|
||||
hasGetLock :: SomeRcvQueue q => AgentClient -> q -> IO Bool
|
||||
@@ -2277,7 +2288,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.CbNonceI n => C.DhSecretX25519 -> n -> ByteString -> Either AgentErrorType ByteString
|
||||
agentCbDecrypt :: C.DhSecretX25519 -> C.CbNonce -> ByteString -> Either AgentErrorType ByteString
|
||||
agentCbDecrypt dhSecret nonce msg =
|
||||
first cryptoError $
|
||||
C.cbDecrypt dhSecret nonce msg
|
||||
|
||||
@@ -162,7 +162,6 @@ data AgentConfig = AgentConfig
|
||||
connDeleteDeliveryTimeout :: NominalDiffTime,
|
||||
helloTimeout :: NominalDiffTime,
|
||||
quotaExceededTimeout :: NominalDiffTime,
|
||||
commandQuotaRetryInterval :: RetryInterval,
|
||||
persistErrorInterval :: NominalDiffTime,
|
||||
initialCleanupDelay :: Int64,
|
||||
cleanupInterval :: Int64,
|
||||
@@ -242,7 +241,6 @@ defaultAgentConfig =
|
||||
connDeleteDeliveryTimeout = 2 * nominalDay,
|
||||
helloTimeout = 2 * nominalDay,
|
||||
quotaExceededTimeout = 7 * nominalDay,
|
||||
commandQuotaRetryInterval = RetryInterval {initialInterval = 30_000000, increaseAfter = 0, maxInterval = 3600_000000},
|
||||
persistErrorInterval = 3, -- seconds
|
||||
initialCleanupDelay = 30 * 1000000, -- 30 seconds
|
||||
cleanupInterval = 5 * 60 * 1000000, -- 5 minutes
|
||||
|
||||
@@ -79,7 +79,6 @@ module Simplex.Messaging.Agent.Protocol
|
||||
SMPConfirmation (..),
|
||||
AgentMsgEnvelope (..),
|
||||
AgentMessage (..),
|
||||
RatchetInfo (..),
|
||||
RequestSignature (..),
|
||||
AgentMessageType (..),
|
||||
APrivHeader (..),
|
||||
@@ -167,8 +166,6 @@ module Simplex.Messaging.Agent.Protocol
|
||||
ConnId,
|
||||
ConfirmationId,
|
||||
InvitationId,
|
||||
ConnVerifyCodes (..),
|
||||
ContactRequestBinding (..),
|
||||
MsgIntegrity (..),
|
||||
MsgErrorType (..),
|
||||
QueueStatus (..),
|
||||
@@ -407,7 +404,7 @@ data AEvent (e :: AEntity) where
|
||||
LINK :: ConnShortLink 'CMContact -> UserConnLinkData 'CMContact -> AEvent AEConn
|
||||
LDATA :: FixedLinkData 'CMContact -> ConnLinkData 'CMContact -> ConnectionRequestUri 'CMContact -> AEvent AEConn
|
||||
CONF :: ConfirmationId -> PQSupport -> [SMPServer] -> ConnInfo -> AEvent AEConn -- ConnInfo is from sender, [SMPServer] will be empty only in v1 handshake
|
||||
REQ :: InvitationId -> PQSupport -> NonEmpty SMPServer -> ConnInfo -> ContactRequestBinding -> Bool -> AEvent AEConn -- ConnInfo is from sender; Bool - rejection reason can be sent
|
||||
REQ :: InvitationId -> PQSupport -> NonEmpty SMPServer -> ConnInfo -> Bool -> AEvent AEConn -- ConnInfo is from sender; Bool - rejection reason can be sent
|
||||
SREQ :: InvitationId -> Maybe C.PublicKeyEd25519 -> MsgBody -> AEvent AEConn
|
||||
SSENT :: AgentMsgId -> Maybe SMPServer -> AEvent AEConn
|
||||
RJCT :: ConnInfo -> AEvent AEConn
|
||||
@@ -612,7 +609,6 @@ data RcvSwitchStatus
|
||||
| RSSendingQADD
|
||||
| RSSendingQUSE
|
||||
| RSReceivedMessage
|
||||
| RSReceivedQEND
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding RcvSwitchStatus where
|
||||
@@ -621,14 +617,12 @@ 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
|
||||
@@ -932,7 +926,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 RatchetInfo
|
||||
| AgentRatchetInfo ByteString
|
||||
| AgentMessage APrivHeader AMessage
|
||||
| AgentServiceRequest (NonEmpty SMPQueueInfo) (Maybe RequestSignature) MsgBody
|
||||
| AgentServiceResponse MsgBody
|
||||
@@ -943,7 +937,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', info)
|
||||
AgentRatchetInfo info -> smpEncode ('R', Tail info)
|
||||
AgentMessage hdr aMsg -> smpEncode ('M', hdr, aMsg)
|
||||
AgentServiceRequest qs sig_ body -> smpEncode ('A', qs, sig_, Tail body)
|
||||
AgentServiceResponse body -> smpEncode ('P', Tail body)
|
||||
@@ -952,23 +946,13 @@ instance Encoding AgentMessage where
|
||||
smpP >>= \case
|
||||
'I' -> AgentConnInfo . unTail <$> smpP
|
||||
'D' -> AgentConnInfoReply <$> smpP <*> (unTail <$> smpP)
|
||||
'R' -> AgentRatchetInfo <$> smpP
|
||||
'R' -> AgentRatchetInfo . unTail <$> 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
|
||||
@@ -1388,15 +1372,6 @@ type ConfirmationId = ByteString
|
||||
|
||||
type InvitationId = ByteString
|
||||
|
||||
data ConnVerifyCodes = ConnVerifyCodes
|
||||
{ codeAD :: ByteString,
|
||||
codePQ :: Maybe ByteString
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ContactRequestBinding = CRBRatchet ConnVerifyCodes | CRBRequest ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
extraSMPServerHosts :: Map TransportHost TransportHost
|
||||
extraSMPServerHosts =
|
||||
M.fromList
|
||||
|
||||
@@ -10,7 +10,6 @@ module Simplex.Messaging.Agent.RetryInterval
|
||||
withRetryInterval,
|
||||
withRetryIntervalCount,
|
||||
withRetryForeground,
|
||||
withRetryInterval2,
|
||||
withRetryLock2,
|
||||
updateRetryInterval2,
|
||||
nextRetryDelay,
|
||||
@@ -87,24 +86,9 @@ withRetryForeground ri isForeground isOnline action = callAction 0 $ initialInte
|
||||
| otherwise = (elapsed + delay, nextRetryDelay elapsed' delay ri)
|
||||
callAction elapsed' delay'
|
||||
|
||||
withRetryInterval2 :: forall m. MonadIO m => RetryInterval2 -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
|
||||
withRetryInterval2 = withRetryWait2 $ liftIO . threadDelay'
|
||||
|
||||
-- This function allows action to toggle between slow and fast retry intervals.
|
||||
withRetryLock2 :: forall m. MonadIO m => RetryInterval2 -> TMVar () -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
|
||||
withRetryLock2 ri lock = withRetryWait2 wait ri
|
||||
where
|
||||
wait delay = do
|
||||
waiting <- newTVarIO True
|
||||
_ <- liftIO . forkIO $ do
|
||||
threadDelay' delay
|
||||
atomically $ whenM (readTVar waiting) $ void $ tryPutTMVar lock ()
|
||||
atomically $ do
|
||||
takeTMVar lock
|
||||
writeTVar waiting False
|
||||
|
||||
withRetryWait2 :: forall m. Monad m => (Int64 -> m ()) -> RetryInterval2 -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
|
||||
withRetryWait2 wait RetryInterval2 {riSlow, riFast} action =
|
||||
withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
|
||||
callAction (0, initialInterval riSlow) (0, initialInterval riFast)
|
||||
where
|
||||
callAction :: (Int64, Int64) -> (Int64, Int64) -> m ()
|
||||
@@ -118,6 +102,14 @@ withRetryWait2 wait RetryInterval2 {riSlow, riFast} action =
|
||||
let elapsed' = elapsed + delay
|
||||
delay' = nextRetryDelay elapsed' delay ri
|
||||
call (elapsed', delay')
|
||||
wait delay = do
|
||||
waiting <- newTVarIO True
|
||||
_ <- liftIO . forkIO $ do
|
||||
threadDelay' delay
|
||||
atomically $ whenM (readTVar waiting) $ void $ tryPutTMVar lock ()
|
||||
atomically $ do
|
||||
takeTMVar lock
|
||||
writeTVar waiting False
|
||||
|
||||
nextRetryDelay :: Int64 -> Int64 -> RetryInterval -> Int64
|
||||
nextRetryDelay elapsed delay RetryInterval {increaseAfter, maxInterval} =
|
||||
|
||||
@@ -219,7 +219,6 @@ 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
|
||||
@@ -490,8 +489,7 @@ data PendingCommand = PendingCommand
|
||||
corrId :: ACorrId,
|
||||
userId :: UserId,
|
||||
connId :: ConnId,
|
||||
command :: AgentCommand,
|
||||
createdAt :: UTCTime
|
||||
command :: AgentCommand
|
||||
}
|
||||
|
||||
data AgentCmdType = ACClient | ACInternal
|
||||
|
||||
@@ -76,7 +76,6 @@ module Simplex.Messaging.Agent.Store.AgentStore
|
||||
getExpiredServiceConns,
|
||||
deleteExpiredServiceRequests,
|
||||
getDeletedWaitingDeliveryConnIds,
|
||||
getConnRatchetSync,
|
||||
setConnRatchetSync,
|
||||
addProcessedRatchetKeyHash,
|
||||
checkRatchetKeyHashExists,
|
||||
@@ -100,7 +99,6 @@ module Simplex.Messaging.Agent.Store.AgentStore
|
||||
setSndQueuePrimary,
|
||||
deleteConnRcvQueue,
|
||||
incRcvDeleteErrors,
|
||||
getEndedRcvQueueConnIds,
|
||||
deleteConnSndQueue,
|
||||
getPrimaryRcvQueue,
|
||||
getRcvQueue,
|
||||
@@ -167,8 +165,6 @@ module Simplex.Messaging.Agent.Store.AgentStore
|
||||
createRatchet,
|
||||
deleteRatchet,
|
||||
getRatchet,
|
||||
getRatchetVerifyCodes,
|
||||
ratchetVerifyCodes,
|
||||
getRatchetForUpdate,
|
||||
getSkippedMsgKeys,
|
||||
updateRatchet,
|
||||
@@ -217,7 +213,6 @@ module Simplex.Messaging.Agent.Store.AgentStore
|
||||
-- Rcv files
|
||||
createRcvFile,
|
||||
createRcvFileRedirect,
|
||||
startPreparedRcvFile,
|
||||
lockRcvFileForUpdate,
|
||||
getRcvFile,
|
||||
getRcvFileByEntityId,
|
||||
@@ -239,7 +234,6 @@ module Simplex.Messaging.Agent.Store.AgentStore
|
||||
getRcvFilesExpired,
|
||||
-- Snd files
|
||||
createSndFile,
|
||||
startPreparedSndFile,
|
||||
lockSndFileForUpdate,
|
||||
getSndFile,
|
||||
getSndFileByEntityId,
|
||||
@@ -297,7 +291,6 @@ import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortBy)
|
||||
@@ -764,10 +757,6 @@ 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)
|
||||
@@ -1260,7 +1249,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, msgId)
|
||||
(connId, Binary 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)) =
|
||||
@@ -1292,7 +1281,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, msgId)
|
||||
DB.query db "SELECT broker_ts FROM rcv_messages WHERE conn_id = ? AND broker_id = ?" (connId, Binary msgId)
|
||||
|
||||
deleteMsg :: DB.Connection -> ConnId -> InternalId -> IO ()
|
||||
deleteMsg db connId msgId =
|
||||
@@ -1485,11 +1474,9 @@ createSndRatchet db connId ratchetState (CR.AE2ERatchetParams s (CR.E2ERatchetPa
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO ratchets
|
||||
(conn_id, ratchet_state, rc_verify_code_ad, rc_verify_code_pq, x3dh_pub_key_1, x3dh_pub_key_2, pq_pub_kem) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(conn_id, ratchet_state, x3dh_pub_key_1, x3dh_pub_key_2, pq_pub_kem) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (conn_id) DO UPDATE SET
|
||||
ratchet_state = EXCLUDED.ratchet_state,
|
||||
rc_verify_code_ad = EXCLUDED.rc_verify_code_ad,
|
||||
rc_verify_code_pq = EXCLUDED.rc_verify_code_pq,
|
||||
x3dh_priv_key_1 = NULL,
|
||||
x3dh_priv_key_2 = NULL,
|
||||
x3dh_pub_key_1 = EXCLUDED.x3dh_pub_key_1,
|
||||
@@ -1497,13 +1484,7 @@ createSndRatchet db connId ratchetState (CR.AE2ERatchetParams s (CR.E2ERatchetPa
|
||||
pq_priv_kem = NULL,
|
||||
pq_pub_kem = EXCLUDED.pq_pub_kem
|
||||
|]
|
||||
((connId, ratchetState) :. verifyCodesRow (ratchetVerifyCodes ratchetState) :. (x3dhPubKey1, x3dhPubKey2, CR.ARKP s <$> pqPubKem))
|
||||
|
||||
ratchetVerifyCodes :: RatchetX448 -> ConnVerifyCodes
|
||||
ratchetVerifyCodes CR.Ratchet {rcAD, rcVCPQ} = ConnVerifyCodes {codeAD = C.sha256Hash $ unStr rcAD, codePQ = unStr <$> rcVCPQ}
|
||||
|
||||
verifyCodesRow :: ConnVerifyCodes -> (Binary ByteString, Maybe (Binary ByteString))
|
||||
verifyCodesRow ConnVerifyCodes {codeAD, codePQ} = (Binary codeAD, Binary <$> codePQ)
|
||||
(connId, ratchetState, x3dhPubKey1, x3dhPubKey2, CR.ARKP s <$> pqPubKem)
|
||||
|
||||
getSndRatchet :: DB.Connection -> ConnId -> CR.VersionE2E -> IO (Either StoreError (RatchetX448, CR.AE2ERatchetParams 'C.X448))
|
||||
getSndRatchet db connId v =
|
||||
@@ -1524,12 +1505,10 @@ createRatchet db connId rc =
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO ratchets (conn_id, ratchet_state, rc_verify_code_ad, rc_verify_code_pq)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO ratchets (conn_id, ratchet_state)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (conn_id) DO UPDATE SET
|
||||
ratchet_state = EXCLUDED.ratchet_state,
|
||||
rc_verify_code_ad = EXCLUDED.rc_verify_code_ad,
|
||||
rc_verify_code_pq = EXCLUDED.rc_verify_code_pq,
|
||||
ratchet_state = ?,
|
||||
x3dh_priv_key_1 = NULL,
|
||||
x3dh_priv_key_2 = NULL,
|
||||
x3dh_pub_key_1 = NULL,
|
||||
@@ -1537,42 +1516,7 @@ createRatchet db connId rc =
|
||||
pq_priv_kem = NULL,
|
||||
pq_pub_kem = NULL
|
||||
|]
|
||||
((connId, rc) :. verifyCodesRow (ratchetVerifyCodes rc))
|
||||
|
||||
getRatchetVerifyCodes :: DB.Connection -> [ConnId] -> IO (Map ConnId ConnVerifyCodes)
|
||||
getRatchetVerifyCodes db connIds = do
|
||||
(saved, computed) <- partitionEithers . mapMaybe rowCodes <$> selectCodes
|
||||
stored <- saveCodes computed
|
||||
pure $ M.fromList $ saved <> stored
|
||||
where
|
||||
rowCodes (connId, codeAD_, codePQ_, rc_) = case codeAD_ of
|
||||
Just codeAD -> Just $ Left $ toCodes (connId, codeAD, codePQ_)
|
||||
Nothing -> Right . (connId,) . ratchetVerifyCodes <$> rc_
|
||||
toCodes (connId, Binary codeAD, codePQ_) = (connId, ConnVerifyCodes {codeAD, codePQ = fromBinary <$> codePQ_})
|
||||
codesRow (connId, cs) = verifyCodesRow cs :. Only connId
|
||||
codesQuery :: Query
|
||||
codesQuery = "SELECT conn_id, rc_verify_code_ad, rc_verify_code_pq, CASE WHEN rc_verify_code_ad IS NULL THEN ratchet_state END FROM ratchets"
|
||||
selectCodes :: IO [(ConnId, Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe RatchetX448)]
|
||||
saveCodes :: [(ConnId, ConnVerifyCodes)] -> IO [(ConnId, ConnVerifyCodes)]
|
||||
#if defined(dbPostgres)
|
||||
selectCodes = DB.query db (codesQuery <> " WHERE conn_id IN ?") (Only (In connIds))
|
||||
saveCodes cs =
|
||||
map toCodes
|
||||
<$> DB.returning
|
||||
db
|
||||
[sql|
|
||||
UPDATE ratchets r
|
||||
SET rc_verify_code_ad = COALESCE(r.rc_verify_code_ad, (upd.rc_verify_code_ad :: BYTEA)),
|
||||
rc_verify_code_pq = CASE WHEN r.rc_verify_code_ad IS NULL THEN (upd.rc_verify_code_pq :: BYTEA) ELSE r.rc_verify_code_pq END
|
||||
FROM (VALUES(?, ?, ?)) AS upd(rc_verify_code_ad, rc_verify_code_pq, conn_id)
|
||||
WHERE r.conn_id = (upd.conn_id :: BYTEA)
|
||||
RETURNING r.conn_id, r.rc_verify_code_ad, r.rc_verify_code_pq
|
||||
|]
|
||||
(map codesRow cs)
|
||||
#else
|
||||
selectCodes = concat <$> mapM (DB.query db (codesQuery <> " WHERE conn_id = ?") . Only) connIds
|
||||
saveCodes cs = cs <$ unless (null cs) (DB.executeMany db "UPDATE ratchets SET rc_verify_code_ad = ?, rc_verify_code_pq = ? WHERE conn_id = ?" $ map codesRow cs)
|
||||
#endif
|
||||
(connId, rc, rc)
|
||||
|
||||
deleteRatchet :: DB.Connection -> ConnId -> IO ()
|
||||
deleteRatchet db connId =
|
||||
@@ -1600,16 +1544,12 @@ getRatchet_ q db connId =
|
||||
where
|
||||
ratchet = maybe (Left SERatchetNotFound) Right . fromOnly
|
||||
|
||||
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
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -1728,7 +1668,7 @@ getPendingServerCommand db connId srv_ = getWorkItem "command" getCmdId getComma
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT c.corr_id, cs.user_id, c.command, c.created_at
|
||||
SELECT c.corr_id, cs.user_id, c.command
|
||||
FROM commands c
|
||||
JOIN connections cs USING (conn_id)
|
||||
WHERE c.command_id = ?
|
||||
@@ -1736,7 +1676,7 @@ getPendingServerCommand db connId srv_ = getWorkItem "command" getCmdId getComma
|
||||
(Only cmdId)
|
||||
where
|
||||
err = SEInternal $ "command " <> bshow cmdId <> " returned []"
|
||||
pendingCommand (corrId, userId, command, createdAt) = PendingCommand {cmdId, corrId, userId, connId, command, createdAt}
|
||||
pendingCommand (corrId, userId, command) = PendingCommand {cmdId, corrId, userId, connId, command}
|
||||
markCommandFailed cmdId = DB.execute db "UPDATE commands SET failed = 1 WHERE command_id = ?" (Only cmdId)
|
||||
|
||||
updateCommandServer :: DB.Connection -> AsyncCmdId -> SMPServer -> IO (Either StoreError ())
|
||||
@@ -2510,12 +2450,12 @@ getSubscriptionServers db onlyNeeded =
|
||||
|
||||
-- TODO [certs rcv] check index for getting queues with service present
|
||||
getUserServerRcvQueueSubs :: DB.Connection -> UserId -> SMPServer -> Bool -> ServiceAssoc -> Int -> Maybe SMP.RecipientId -> IO [RcvQueueSub]
|
||||
getUserServerRcvQueueSubs db userId srv@(SMPServer h p kh) onlyNeeded hasService limit cursor_ =
|
||||
map (toServerRcvQueueSub srv) <$> case cursor_ of
|
||||
getUserServerRcvQueueSubs db userId (SMPServer h p kh) onlyNeeded hasService limit cursor_ =
|
||||
map toRcvQueueSub <$> case cursor_ of
|
||||
Nothing -> DB.query db (q <> orderLimit) (userId, h, p, kh, limit)
|
||||
Just cursor -> DB.query db (q <> " AND q.rcv_id > ? " <> orderLimit) (userId, h, p, kh, cursor, limit)
|
||||
where
|
||||
q = serverRcvQueueSubQuery <> toSubscribe <> " c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ?" <> serviceCond
|
||||
q = rcvQueueSubQuery <> toSubscribe <> " c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ?" <> serviceCond
|
||||
orderLimit = " ORDER BY q.rcv_id LIMIT ?"
|
||||
toSubscribe
|
||||
| onlyNeeded = " WHERE q.to_subscribe = 1 AND "
|
||||
@@ -2528,7 +2468,7 @@ unassocUserServerRcvQueueSubs :: DB.Connection -> UserId -> SMPServer -> IO [Rcv
|
||||
unassocUserServerRcvQueueSubs db userId srv@(SMPServer h p kh) = do
|
||||
deleteClientService db userId srv
|
||||
#if defined(dbPostgres)
|
||||
map (toServerRcvQueueSub srv)
|
||||
map toRcvQueueSub
|
||||
<$> DB.query
|
||||
db
|
||||
(removeRcvAssocsQuery <> " " <> returningColumns)
|
||||
@@ -2536,15 +2476,15 @@ unassocUserServerRcvQueueSubs db userId srv@(SMPServer h p kh) = do
|
||||
where
|
||||
returningColumns =
|
||||
[sql|
|
||||
RETURNING c.user_id, rcv_queues.conn_id,
|
||||
RETURNING c.user_id, rcv_queues.conn_id, rcv_queues.host, rcv_queues.port, COALESCE(rcv_queues.server_key_hash, s.key_hash),
|
||||
rcv_queues.rcv_id, rcv_queues.rcv_private_key, rcv_queues.status, c.enable_ntfs, rcv_queues.client_notice_id,
|
||||
rcv_queues.rcv_queue_id, rcv_queues.rcv_primary, rcv_queues.replace_rcv_queue_id
|
||||
|]
|
||||
#else
|
||||
qs <- map (toServerRcvQueueSub srv)
|
||||
qs <- map toRcvQueueSub
|
||||
<$> DB.query
|
||||
db
|
||||
(serverRcvQueueSubQuery <> " WHERE c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ? AND q.rcv_service_assoc = 1")
|
||||
(rcvQueueSubQuery <> " WHERE c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ? AND q.rcv_service_assoc = 1")
|
||||
(userId, h, p, kh)
|
||||
DB.execute db removeRcvAssocsQuery (h, p, userId, kh)
|
||||
pure qs
|
||||
@@ -2803,11 +2743,6 @@ 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)
|
||||
@@ -2829,35 +2764,21 @@ checkRatchetKeyHashExists db connId hash =
|
||||
(connId, Binary hash)
|
||||
|
||||
deleteRatchetKeyHashesExpired :: DB.Connection -> NominalDiffTime -> Int -> IO ()
|
||||
deleteRatchetKeyHashesExpired db ttl maxConnHashes = do
|
||||
deleteRatchetKeyHashesExpired db ttl limit = do
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
#if defined(dbPostgres)
|
||||
DB.execute
|
||||
db
|
||||
("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(*) > ?
|
||||
|]
|
||||
[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)
|
||||
|
||||
-- | returns all connection queues, the first queue is the primary one
|
||||
getRcvQueuesByConnId_ :: DB.Connection -> ConnId -> IO (Maybe (NonEmpty RcvQueue))
|
||||
@@ -2914,32 +2835,17 @@ getRcvQueueSubsByConnId_ db connId =
|
||||
rcvQueueSubQuery :: Query
|
||||
rcvQueueSubQuery =
|
||||
[sql|
|
||||
SELECT q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), c.user_id, q.conn_id, q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
|
||||
FROM rcv_queues q
|
||||
JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
JOIN connections c ON q.conn_id = c.conn_id
|
||||
|]
|
||||
|
||||
serverRcvQueueSubQuery :: Query
|
||||
serverRcvQueueSubQuery =
|
||||
[sql|
|
||||
SELECT c.user_id, q.conn_id, q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id,
|
||||
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id
|
||||
FROM rcv_queues q
|
||||
JOIN servers s ON q.host = s.host AND q.port = s.port
|
||||
JOIN connections c ON q.conn_id = c.conn_id
|
||||
|]
|
||||
|
||||
type RcvQueueSubRow = (UserId, ConnId, SMP.RecipientId, SMP.RcvPrivateAuthKey) :. (QueueStatus, Maybe BoolInt, Maybe NoticeId, Int64, BoolInt, Maybe Int64)
|
||||
|
||||
toRcvQueueSub :: (NonEmpty TransportHost, ServiceName, C.KeyHash) :. RcvQueueSubRow -> RcvQueueSub
|
||||
toRcvQueueSub ((host, port, keyHash) :. r) = toServerRcvQueueSub (SMPServer host port keyHash) r
|
||||
|
||||
toServerRcvQueueSub :: SMPServer -> RcvQueueSubRow -> RcvQueueSub
|
||||
toServerRcvQueueSub server ((userId, connId, rcvId, rcvPrivateKey) :. (status, enableNtfs_, clientNoticeId, dbQueueId, BI primary, dbReplaceQueueId)) =
|
||||
toRcvQueueSub :: (UserId, ConnId, NonEmpty TransportHost, ServiceName, C.KeyHash, SMP.RecipientId, SMP.RcvPrivateAuthKey) :. (QueueStatus, Maybe BoolInt, Maybe NoticeId, Int64, BoolInt, Maybe Int64) -> RcvQueueSub
|
||||
toRcvQueueSub ((userId, connId, host, port, keyHash, rcvId, rcvPrivateKey) :. (status, enableNtfs_, clientNoticeId, dbQueueId, BI primary, dbReplaceQueueId)) =
|
||||
let enableNtfs = maybe True unBI enableNtfs_
|
||||
in RcvQueueSub {userId, connId, server, rcvId, rcvPrivateKey, status, enableNtfs, clientNoticeId, dbQueueId, primary, dbReplaceQueueId}
|
||||
in RcvQueueSub {userId, connId, server = SMPServer host port keyHash, rcvId, rcvPrivateKey, status, enableNtfs, clientNoticeId, dbQueueId, primary, dbReplaceQueueId}
|
||||
|
||||
getRcvQueueById :: DB.Connection -> ConnId -> Int64 -> IO (Either StoreError RcvQueue)
|
||||
getRcvQueueById db connId dbRcvId =
|
||||
@@ -3040,7 +2946,7 @@ insertRcvMsgDetails_ db connId RcvQueue {dbQueueId} RcvMsgData {msgMeta, interna
|
||||
VALUES
|
||||
(?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
(connId, dbQueueId, internalRcvId, fst recipient, sndMsgId, fst broker, snd broker, Binary internalHash, Binary externalPrevSndHash, integrity)
|
||||
(connId, dbQueueId, internalRcvId, fst recipient, sndMsgId, Binary (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 ()
|
||||
@@ -3188,8 +3094,7 @@ 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
|
||||
nonce <- atomically $ C.randomCbNonce gVar
|
||||
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId (dummyDst nonce) prefixPath dstPath dstFile Nothing Nothing approvedRelays
|
||||
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst 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
|
||||
@@ -3197,7 +3102,7 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire
|
||||
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId
|
||||
pure dstEntityId
|
||||
where
|
||||
dummyDst nonce =
|
||||
dummyDst =
|
||||
FileDescription
|
||||
{ party = SFRecipient,
|
||||
size,
|
||||
@@ -3205,34 +3110,11 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire
|
||||
redirect = Nothing,
|
||||
-- updated later with updateRcvFileRedirect
|
||||
key = C.unsafeSbKey $ B.replicate 32 '#',
|
||||
nonce,
|
||||
nonce = C.cbNonce "",
|
||||
chunkSize = FileSize 0,
|
||||
chunks = []
|
||||
}
|
||||
|
||||
startPreparedRcvFile :: DB.Connection -> RcvFileId -> IO (Either StoreError [XFTPServer])
|
||||
startPreparedRcvFile db rcvFileEntityId = runExceptT $ do
|
||||
rcvFileId <- ExceptT $ getRcvFileIdByEntityId_ db rcvFileEntityId
|
||||
liftIO $ do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE rcv_files SET status = ?, updated_at = ? WHERE (rcv_file_id = ? OR redirect_id = ?) AND status = ?"
|
||||
(RFSReceiving, updatedAt, rcvFileId, rcvFileId, RFSPrepared)
|
||||
map toXFTPServer
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT DISTINCT
|
||||
s.xftp_host, s.xftp_port, s.xftp_key_hash
|
||||
FROM rcv_file_chunk_replicas r
|
||||
JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id
|
||||
JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id
|
||||
JOIN rcv_files f ON f.rcv_file_id = c.rcv_file_id
|
||||
WHERE (f.rcv_file_id = ? OR f.redirect_id = ?) AND r.replica_number = 1
|
||||
|]
|
||||
(rcvFileId, rcvFileId)
|
||||
|
||||
insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> Bool -> IO (Either StoreError (RcvFileId, DBRcvFileId))
|
||||
insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ approvedRelays = runExceptT $ do
|
||||
let (redirectDigest_, redirectSize_) = case redirect of
|
||||
@@ -3243,7 +3125,7 @@ insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSiz
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((Binary rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSPrepared, redirectId_, Binary <$> redirectEntityId_, redirectDigest_, redirectSize_, BI approvedRelays))
|
||||
((Binary rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, Binary <$> redirectEntityId_, redirectDigest_, redirectSize_, BI approvedRelays))
|
||||
rcvFileId <- liftIO $ insertedRowId db
|
||||
pure (rcvFileEntityId, rcvFileId)
|
||||
|
||||
@@ -3548,23 +3430,13 @@ createSndFile db gVar userId (CryptoFile path cfArgs) numRecipients prefixPath k
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO snd_files (snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, prefix_path, key, nonce, status, redirect_size, redirect_digest, storage_time) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((Binary sndFileEntityId, userId, path, fileKey <$> cfArgs, fileNonce <$> cfArgs, numRecipients) :. (prefixPath, key, nonce, SFSPrepared, redirectSize_, redirectDigest_, storageHours))
|
||||
((Binary sndFileEntityId, userId, path, fileKey <$> cfArgs, fileNonce <$> cfArgs, numRecipients) :. (prefixPath, key, nonce, SFSNew, redirectSize_, redirectDigest_, storageHours))
|
||||
where
|
||||
(redirectSize_, redirectDigest_) =
|
||||
case redirect_ of
|
||||
Nothing -> (Nothing, Nothing)
|
||||
Just RedirectFileInfo {size, digest} -> (Just size, Just digest)
|
||||
|
||||
startPreparedSndFile :: DB.Connection -> SndFileId -> IO (Either StoreError ())
|
||||
startPreparedSndFile db sndFileEntityId = runExceptT $ do
|
||||
sndFileId <- ExceptT $ getSndFileIdByEntityId_ db sndFileEntityId
|
||||
liftIO $ do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE snd_files SET status = ?, updated_at = ? WHERE snd_file_id = ? AND status = ?"
|
||||
(SFSNew, updatedAt, sndFileId, SFSPrepared)
|
||||
|
||||
getSndFileByEntityId :: DB.Connection -> SndFileId -> IO (Either StoreError SndFile)
|
||||
getSndFileByEntityId db sndFileEntityId = runExceptT $ do
|
||||
sndFileId <- ExceptT $ getSndFileIdByEntityId_ db sndFileEntityId
|
||||
|
||||
@@ -13,7 +13,6 @@ module Simplex.Messaging.Agent.Store.Postgres.DB
|
||||
execute,
|
||||
execute_,
|
||||
executeMany,
|
||||
returning,
|
||||
query,
|
||||
query_,
|
||||
blobFieldDecoder,
|
||||
@@ -61,10 +60,6 @@ executeMany :: ToRow q => PSQL.Connection -> Query -> [q] -> IO ()
|
||||
executeMany db q qs = void $ PSQL.executeMany db q qs `E.catch` addSql q
|
||||
{-# INLINE executeMany #-}
|
||||
|
||||
returning :: (ToRow q, FromRow r) => PSQL.Connection -> Query -> [q] -> IO [r]
|
||||
returning db q qs = PSQL.returning db q qs `E.catch` addSql q
|
||||
{-# INLINE returning #-}
|
||||
|
||||
query :: (ToRow q, FromRow r) => PSQL.Connection -> Query -> q -> IO [r]
|
||||
query db q qs = PSQL.query db q qs `E.catch` addSql q
|
||||
{-# INLINE query #-}
|
||||
|
||||
@@ -15,8 +15,6 @@ import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260410_receive_attem
|
||||
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.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -31,9 +29,7 @@ schemaMigrations =
|
||||
("20260410_receive_attempts", m20260410_receive_attempts, Just down_m20260410_receive_attempts),
|
||||
("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),
|
||||
("20260929_ratchet_indexes", m20260929_ratchet_indexes, Just down_m20260929_ratchet_indexes)
|
||||
("20260823_snd_files_entitlement", m20260823_snd_files_entitlement, Just down_m20260823_snd_files_entitlement)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20260919_ratchet_verify_codes where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260919_ratchet_verify_codes :: Text
|
||||
m20260919_ratchet_verify_codes =
|
||||
[r|
|
||||
ALTER TABLE ratchets ADD COLUMN rc_verify_code_ad BYTEA;
|
||||
ALTER TABLE ratchets ADD COLUMN rc_verify_code_pq BYTEA;
|
||||
|]
|
||||
|
||||
down_m20260919_ratchet_verify_codes :: Text
|
||||
down_m20260919_ratchet_verify_codes =
|
||||
[r|
|
||||
ALTER TABLE ratchets DROP COLUMN rc_verify_code_pq;
|
||||
ALTER TABLE ratchets DROP COLUMN rc_verify_code_ad;
|
||||
|]
|
||||
@@ -1,34 +0,0 @@
|
||||
{-# 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);
|
||||
|]
|
||||
@@ -448,9 +448,7 @@ CREATE TABLE smp_agent_test_protocol_schema.ratchets (
|
||||
x3dh_pub_key_1 bytea,
|
||||
x3dh_pub_key_2 bytea,
|
||||
pq_priv_kem bytea,
|
||||
pq_pub_kem bytea,
|
||||
rc_verify_code_ad bytea,
|
||||
rc_verify_code_pq bytea
|
||||
pq_pub_kem bytea
|
||||
);
|
||||
|
||||
|
||||
@@ -1169,15 +1167,11 @@ 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 UNIQUE INDEX idx_processed_ratchet_key_hashes_hash ON smp_agent_test_protocol_schema.processed_ratchet_key_hashes USING btree (conn_id, hash);
|
||||
CREATE INDEX idx_processed_ratchet_key_hashes_hash ON smp_agent_test_protocol_schema.processed_ratchet_key_hashes USING btree (conn_id, hash);
|
||||
|
||||
|
||||
|
||||
@@ -1245,7 +1239,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, skipped_message_id);
|
||||
CREATE INDEX idx_skipped_messages_conn_id ON smp_agent_test_protocol_schema.skipped_messages USING btree (conn_id);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -100,8 +100,7 @@ withTransaction st = withTransactionPriority st False
|
||||
{-# INLINE withTransaction #-}
|
||||
|
||||
withTransactionPriority :: DBStore -> Bool -> (DB.Connection -> IO a) -> IO a
|
||||
withTransactionPriority st@DBStore {dbClosed} priority action = withConnectionPriority st priority $ \db ->
|
||||
ifM (readTVarIO dbClosed) (fail "withTransaction: database closed") (dbBusyLoop $ transaction db)
|
||||
withTransactionPriority st priority action = withConnectionPriority st priority $ dbBusyLoop . transaction
|
||||
where
|
||||
transaction db@DB.Connection {conn} = SQL.withImmediateTransaction conn $ action db
|
||||
|
||||
|
||||
@@ -51,9 +51,6 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260410_receive_attempt
|
||||
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.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -104,10 +101,7 @@ schemaMigrations =
|
||||
("m20260410_receive_attempts", m20260410_receive_attempts, Just down_m20260410_receive_attempts),
|
||||
("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),
|
||||
("m20260929_ratchet_indexes", m20260929_ratchet_indexes, Just down_m20260929_ratchet_indexes),
|
||||
("m20261005_commands_autoincrement", m20261005_commands_autoincrement, Just down_m20261005_commands_autoincrement)
|
||||
("m20260823_snd_files_entitlement", m20260823_snd_files_entitlement, Just down_m20260823_snd_files_entitlement)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20260919_ratchet_verify_codes where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260919_ratchet_verify_codes :: Query
|
||||
m20260919_ratchet_verify_codes =
|
||||
[sql|
|
||||
ALTER TABLE ratchets ADD COLUMN rc_verify_code_ad BLOB;
|
||||
ALTER TABLE ratchets ADD COLUMN rc_verify_code_pq BLOB;
|
||||
|]
|
||||
|
||||
down_m20260919_ratchet_verify_codes :: Query
|
||||
down_m20260919_ratchet_verify_codes =
|
||||
[sql|
|
||||
ALTER TABLE ratchets DROP COLUMN rc_verify_code_pq;
|
||||
ALTER TABLE ratchets DROP COLUMN rc_verify_code_ad;
|
||||
|]
|
||||
@@ -1,29 +0,0 @@
|
||||
{-# 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);
|
||||
|]
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
{-# 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;
|
||||
|]
|
||||
@@ -182,9 +182,7 @@ CREATE TABLE ratchets(
|
||||
x3dh_pub_key_1 BLOB,
|
||||
x3dh_pub_key_2 BLOB,
|
||||
pq_priv_kem BLOB,
|
||||
pq_pub_kem BLOB,
|
||||
rc_verify_code_ad BLOB,
|
||||
rc_verify_code_pq BLOB
|
||||
pq_pub_kem BLOB
|
||||
) WITHOUT ROWID, STRICT;
|
||||
CREATE TABLE skipped_messages(
|
||||
skipped_message_id INTEGER PRIMARY KEY,
|
||||
@@ -247,7 +245,7 @@ CREATE TABLE ntf_subscriptions(
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) WITHOUT ROWID, STRICT;
|
||||
CREATE TABLE commands(
|
||||
command_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
command_id INTEGER PRIMARY KEY,
|
||||
conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
host TEXT,
|
||||
port TEXT,
|
||||
@@ -261,7 +259,6 @@ 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,
|
||||
@@ -270,6 +267,7 @@ 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,6 +571,10 @@ 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
|
||||
@@ -637,14 +639,6 @@ 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 TRIGGER tr_rcv_queue_insert
|
||||
AFTER INSERT ON rcv_queues
|
||||
FOR EACH ROW
|
||||
|
||||
@@ -67,7 +67,6 @@ module Simplex.Messaging.Client
|
||||
enableSMPQueuesNtfs,
|
||||
disableSMPQueuesNtfs,
|
||||
sendSMPMessage,
|
||||
QueueDrained,
|
||||
ackSMPMessage,
|
||||
suspendSMPQueue,
|
||||
deleteSMPQueue,
|
||||
@@ -76,6 +75,8 @@ module Simplex.Messaging.Client
|
||||
proxySMPMessage,
|
||||
proxyResolveName,
|
||||
directResolveName,
|
||||
proxyOwnedNames,
|
||||
directOwnedNames,
|
||||
forwardSMPTransmission,
|
||||
getSMPQueueInfo,
|
||||
sendProtocolCommand,
|
||||
@@ -151,10 +152,11 @@ import Data.Int (Int64)
|
||||
import Data.List (find, isSuffixOf)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (catMaybes, fromMaybe, isJust)
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
@@ -163,6 +165,8 @@ import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Eth.Address (Address)
|
||||
import Simplex.Messaging.Names.Record (OwnedNames)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Protocol.Types
|
||||
@@ -196,7 +200,6 @@ data PClient v err msg = PClient
|
||||
transportHost :: TransportHost,
|
||||
tcpConnectTimeout :: NetworkTimeout,
|
||||
tcpTimeout :: NetworkTimeout,
|
||||
proxiedRelayVRange :: VersionRange v,
|
||||
sendPings :: TVar Bool,
|
||||
lastReceived :: TVar UTCTime,
|
||||
timeoutErrorCount :: TVar Int,
|
||||
@@ -242,7 +245,6 @@ smpClientStub g sessionId thVersion thAuth = do
|
||||
transportHost = "localhost",
|
||||
tcpConnectTimeout,
|
||||
tcpTimeout,
|
||||
proxiedRelayVRange = supportedClientSMPRelayVRange,
|
||||
sendPings,
|
||||
lastReceived,
|
||||
timeoutErrorCount,
|
||||
@@ -443,12 +445,9 @@ defaultNetworkConfig =
|
||||
|
||||
transportClientConfig :: NetworkConfig -> NetworkRequestMode -> TransportHost -> Bool -> Maybe [ALPN] -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} nm host useSNI clientALPN =
|
||||
TransportClientConfig {socksProxy = socks, tcpConnectTimeout = tOut, tcpKeepAlive = keepAlive, logTLSErrors, clientCredentials = Nothing, clientALPN, useSNI}
|
||||
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout = tOut, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, clientALPN, useSNI}
|
||||
where
|
||||
tOut = netTimeoutInt tcpConnectTimeout nm
|
||||
socks = useSocksProxy socksMode
|
||||
-- with SOCKS proxy the socket is connected to the proxy, and a slow circuit should not fail the connection
|
||||
keepAlive = (\ka -> if isJust socks then ka {unackedDataTimeout = Nothing} else ka) <$> tcpKeepAlive
|
||||
socksProxy' = (\(SocksProxyWithAuth _ proxy) -> proxy) <$> socksProxy
|
||||
useSocksProxy SMAlways = socksProxy'
|
||||
useSocksProxy SMOnion = case host of
|
||||
@@ -483,7 +482,6 @@ data ProtocolClientConfig v = ProtocolClientConfig
|
||||
serviceCredentials :: Maybe ServiceCredentials,
|
||||
-- | client-server protocol version range
|
||||
serverVRange :: VersionRange v,
|
||||
proxiedRelayVRange :: VersionRange v,
|
||||
-- | agree shared session secret (used in SMP proxy for additional encryption layer)
|
||||
agreeSecret :: Bool,
|
||||
-- | Whether connecting client is a proxy server. See comment in ClientHandshake
|
||||
@@ -502,7 +500,6 @@ defaultClientConfig clientALPN useSNI serverVRange =
|
||||
clientALPN,
|
||||
serviceCredentials = Nothing,
|
||||
serverVRange,
|
||||
proxiedRelayVRange = serverVRange,
|
||||
agreeSecret = False,
|
||||
proxyServer = False,
|
||||
useSNI
|
||||
@@ -513,7 +510,6 @@ defaultSMPClientConfig :: ProtocolClientConfig SMPVersion
|
||||
defaultSMPClientConfig =
|
||||
(defaultClientConfig (Just alpnSupportedSMPHandshakes) False supportedClientSMPRelayVRange)
|
||||
{ defaultTransport = (show defaultSMPPort, transport @TLS),
|
||||
proxiedRelayVRange = supportedClientSMPRelayVRange,
|
||||
agreeSecret = True
|
||||
}
|
||||
{-# INLINE defaultSMPClientConfig #-}
|
||||
@@ -577,7 +573,7 @@ type SMPTransportSession = TransportSession BrokerMsg
|
||||
-- A single queue can be used for multiple 'SMPClient' instances,
|
||||
-- as 'SMPServerTransmission' includes server information.
|
||||
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> NetworkRequestMode -> TransportSession msg -> ProtocolClientConfig v -> [HostName] -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serviceCredentials, serverVRange, proxiedRelayVRange, agreeSecret, proxyServer, useSNI} presetDomains msgQ proxySessTs disconnected = do
|
||||
getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serviceCredentials, serverVRange, agreeSecret, proxyServer, useSNI} presetDomains msgQ proxySessTs disconnected = do
|
||||
case chooseTransportHost networkConfig (host srv) of
|
||||
Right useHost ->
|
||||
(getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost)
|
||||
@@ -602,7 +598,6 @@ getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qS
|
||||
transportHost,
|
||||
tcpConnectTimeout,
|
||||
tcpTimeout,
|
||||
proxiedRelayVRange,
|
||||
sendPings,
|
||||
lastReceived,
|
||||
timeoutErrorCount,
|
||||
@@ -1086,22 +1081,39 @@ directResolveName c nm name
|
||||
r -> throwE $ unexpectedResponse r
|
||||
| otherwise = throwE $ PCETransportError TEVersion
|
||||
|
||||
-- | Names an address owns, via PFWD: a scan links accounts, so hiding the client IP matters more here than for one name.
|
||||
proxyOwnedNames :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> Address -> Word32 -> ExceptT SMPClientError IO (Either ProxyClientError OwnedNames)
|
||||
proxyOwnedNames c nm proxiedRelay addr offset
|
||||
| prVersion proxiedRelay >= nameOwnedSMPVersion =
|
||||
proxySMPCommand c nm proxiedRelay Nothing NoEntity (ROWN addr offset) >>= \case
|
||||
Right (ROWND owned) -> pure $ Right owned
|
||||
Right r -> throwE $ unexpectedResponse r
|
||||
Left e -> pure $ Left e
|
||||
| otherwise = throwE $ PCETransportError TEVersion
|
||||
|
||||
-- | Direct (non-PFWD) owned-names lookup, exposing the client IP.
|
||||
directOwnedNames :: SMPClient -> NetworkRequestMode -> Address -> Word32 -> ExceptT SMPClientError IO OwnedNames
|
||||
directOwnedNames c nm addr offset
|
||||
| thVersion (thParams c) >= nameOwnedSMPVersion =
|
||||
sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (ROWN addr offset)) >>= \case
|
||||
ROWND owned -> pure owned
|
||||
r -> throwE $ unexpectedResponse r
|
||||
| otherwise = throwE $ PCETransportError TEVersion
|
||||
|
||||
resolvedNameOrNotFound :: SimplexDomain -> NameResponse -> Bool
|
||||
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 QueueDrained
|
||||
ackSMPMessage :: SMPClient -> RcvPrivateAuthKey -> QueueId -> MsgId -> ExceptT SMPClientError IO ()
|
||||
ackSMPMessage c rpKey rId msgId =
|
||||
sendSMPCommand c NRMBackground (Just rpKey) rId (ACK msgId) >>= \case
|
||||
OK -> pure True
|
||||
cmd@MSG {} -> liftIO (writeSMPMessage c rId cmd) $> False
|
||||
OK -> return ()
|
||||
cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- | Irreversibly suspend SMP queue.
|
||||
@@ -1127,10 +1139,10 @@ deleteSMPQueues = okSMPCommands DEL
|
||||
-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender
|
||||
-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg
|
||||
connectSMPProxiedRelay :: SMPClient -> NetworkRequestMode -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay
|
||||
connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout, proxiedRelayVRange}} nm relayServ@ProtocolServer {port = relayPort, keyHash = C.KeyHash kh} proxyAuth =
|
||||
connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} nm relayServ@ProtocolServer {port = relayPort, keyHash = C.KeyHash kh} proxyAuth =
|
||||
sendProtocolCommand_ c nm Nothing tOut Nothing NoEntity (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case
|
||||
PKEY sId vr (CertChainPubKey chain key) ->
|
||||
case proxiedRelayVRange `compatibleVersion` vr of
|
||||
case supportedClientSMPRelayVRange `compatibleVersion` vr of
|
||||
Nothing -> throwE $ transportErr TEVersion
|
||||
Just (Compatible v) -> do
|
||||
relayKey <- liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) =<< liftIO (runExceptT $ validateRelay chain key)
|
||||
@@ -1181,7 +1193,7 @@ instance StrEncoding ProxyClientError where
|
||||
-- consider how to process slow responses - is it handled somehow locally or delegated to the caller
|
||||
-- this method is used in the client
|
||||
-- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender
|
||||
-- receives PRES :: Maybe C.CbNonce -> EncResponse -> BrokerMsg -- proxy to client
|
||||
-- receives PRES :: EncResponse -> BrokerMsg -- proxy to client
|
||||
|
||||
-- When client sends message via proxy, there may be one successful scenario and 9 error scenarios
|
||||
-- as shown below (WTF stands for unexpected response, ??? for response that failed to parse).
|
||||
@@ -1234,20 +1246,20 @@ 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 (Just nonce) tForAuth
|
||||
auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth False spKey nonce tForAuth
|
||||
b <- case batchTransmissions serverThParams [Right (auth, tToSend)] of
|
||||
[] -> throwE $ PCETransportError TELargeMsg
|
||||
TBError e _ : _ -> throwE $ PCETransportError e
|
||||
TBTransmission s _ : _ -> pure s
|
||||
TBTransmissions s _ _ : _ -> pure s
|
||||
et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret (encTransmissionNonce v nonce) b paddedProxiedTLength
|
||||
et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedTLength
|
||||
-- proxy interaction errors are wrapped
|
||||
let tOut = Just $ 2 * netTimeoutInt tcpTimeout nm
|
||||
tryE (sendProtocolCommand_ c nm (Just nonce) tOut Nothing (EntityId sessionId) (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case
|
||||
Right r -> case r of
|
||||
PRES nonce_ (EncResponse er) -> do
|
||||
PRES (EncResponse er) -> do
|
||||
-- server interaction errors are thrown directly
|
||||
t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (fromMaybe (C.reverseNonce nonce) nonce_) er
|
||||
t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er
|
||||
case tParse serverThParams t' of
|
||||
t'' :| [] -> case tDecodeClient serverThParams t'' of
|
||||
(_, _, cmd) -> case cmd of
|
||||
@@ -1265,10 +1277,10 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c
|
||||
|
||||
-- this method is used in the proxy
|
||||
-- sends RFWD :: EncFwdTransmission -> Command Sender
|
||||
-- receives RRES :: Maybe C.CbNonce -> EncFwdResponse -> BrokerMsg
|
||||
-- receives RRES :: EncFwdResponse -> BrokerMsg
|
||||
-- proxy should send PRES to the client with EncResponse
|
||||
-- Always uses background timeout mode
|
||||
forwardSMPTransmission :: SMPClient -> C.CorrCbNonce -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO (Maybe C.CbNonce, EncResponse)
|
||||
forwardSMPTransmission :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse
|
||||
forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do
|
||||
-- prepare params
|
||||
sessSecret <- case thAuth thParams of
|
||||
@@ -1280,11 +1292,11 @@ forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorr
|
||||
eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT)
|
||||
-- send
|
||||
sendProtocolCommand_ c NRMBackground (Just nonce) Nothing Nothing NoEntity (Cmd SProxyService (RFWD eft)) >>= \case
|
||||
RRES nonce_ (EncFwdResponse efr) -> do
|
||||
RRES (EncFwdResponse efr) -> do
|
||||
-- unwrap
|
||||
r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr
|
||||
FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r'
|
||||
pure (nonce_, fwdResponse)
|
||||
pure fwdResponse
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
-- get queue information - always sent interactively
|
||||
@@ -1406,7 +1418,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_ (Just nonce) tForAuth
|
||||
auth = authTransmission (thAuth thParams) (useServiceAuth command) pKey_ nonce tForAuth
|
||||
r <- mkRequest (CorrId corrId)
|
||||
pure ((,tToSend) <$> auth, r)
|
||||
where
|
||||
@@ -1425,14 +1437,14 @@ mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentC
|
||||
atomically $ TM.insert corrId r sentCommands
|
||||
pure r
|
||||
|
||||
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_
|
||||
authTransmission :: Maybe (THandleAuth 'TClient) -> Bool -> Maybe C.APrivateAuthKey -> 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, nonce_) of
|
||||
(Just THAuthClient {peerServerPubKey = k}, Just nonce) -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t'
|
||||
_ -> Left TENoServerAuth
|
||||
C.SX25519 -> case thAuth of
|
||||
Just THAuthClient {peerServerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t'
|
||||
Nothing -> Left TENoServerAuth
|
||||
C.SEd25519 -> sign pk
|
||||
C.SEd448 -> sign pk
|
||||
-- When command is signed by both entity key and service key,
|
||||
|
||||
@@ -140,10 +140,8 @@ module Simplex.Messaging.Crypto
|
||||
gcmIV,
|
||||
|
||||
-- * NaCl crypto_box
|
||||
CbNonceI (..),
|
||||
CbNonce,
|
||||
CbNonce (unCbNonce),
|
||||
pattern CbNonce,
|
||||
CorrCbNonce,
|
||||
cbEncrypt,
|
||||
cbEncryptNoPad,
|
||||
cbEncryptMaxLenBS,
|
||||
@@ -154,12 +152,8 @@ module Simplex.Messaging.Crypto
|
||||
sbEncryptNoPad,
|
||||
sbDecryptNoPad,
|
||||
cbNonce,
|
||||
unsafeCbNonce,
|
||||
corrCbNonce,
|
||||
randomCbNonce,
|
||||
reverseNonce,
|
||||
xorNonce,
|
||||
toCbNonce,
|
||||
|
||||
-- * NaCl crypto_secretbox
|
||||
SbKey (unSbKey),
|
||||
@@ -238,7 +232,6 @@ import Data.ASN1.Types
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.Bits (xor)
|
||||
import Data.ByteArray (ByteArrayAccess)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Base64 (decode)
|
||||
@@ -261,7 +254,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 (packZipWith, (<$?>))
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
|
||||
-- | Cryptographic algorithms.
|
||||
data Algorithm = Ed25519 | Ed448 | X25519 | X448
|
||||
@@ -938,8 +931,6 @@ data CryptoError
|
||||
CERatchetEarlierMessage Word32
|
||||
| -- | duplicate message number
|
||||
CERatchetDuplicateMessage
|
||||
| -- | KEM key generation failed, indicating a broken RNG
|
||||
CryptoKEMKeyGenError
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
aesKeySize :: Int
|
||||
@@ -1054,7 +1045,9 @@ md5Hash = BA.convert . (hash :: ByteString -> Digest MD5)
|
||||
|
||||
-- | AEAD-GCM encryption with associated data.
|
||||
--
|
||||
-- Used as part of double ratchet encryption, with a 16-byte IV (see @initAEAD@).
|
||||
-- Used as part of double ratchet encryption.
|
||||
-- This function requires 16 bytes IV, it transforms IV in cryptonite_aes_gcm_init here:
|
||||
-- https://github.com/haskell-crypto/cryptonite/blob/master/cbits/cryptonite_aes.c
|
||||
encryptAEAD :: Key -> IV -> Int -> ByteString -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString)
|
||||
encryptAEAD aesKey ivBytes paddedLen ad msg = do
|
||||
aead <- initAEAD @AES256 aesKey ivBytes
|
||||
@@ -1074,7 +1067,10 @@ encryptAEADNoPad aesKey ivBytes ad msg = do
|
||||
|
||||
-- | AEAD-GCM decryption with associated data.
|
||||
--
|
||||
-- Used as part of double ratchet encryption, with a 16-byte IV (see @initAEAD@).
|
||||
-- Used as part of double ratchet encryption.
|
||||
-- This function requires 16 bytes IV, it transforms IV in cryptonite_aes_gcm_init here:
|
||||
-- https://github.com/haskell-crypto/cryptonite/blob/master/cbits/cryptonite_aes.c
|
||||
-- To make it compatible with WebCrypto we will need to start using initAEADGCM.
|
||||
decryptAEAD :: Key -> IV -> ByteString -> ByteString -> AuthTag -> ExceptT CryptoError IO ByteString
|
||||
decryptAEAD aesKey ivBytes ad msg (AuthTag authTag) = do
|
||||
aead <- initAEAD @AES256 aesKey ivBytes
|
||||
@@ -1152,9 +1148,9 @@ maxLength :: forall i. KnownNat i => Int
|
||||
maxLength = fromIntegral (natVal $ Proxy @i)
|
||||
{-# INLINE maxLength #-}
|
||||
|
||||
-- The 16-byte double ratchet IV is intentionally not the 96-bit IV recommended by NIST SP 800-38D, so GCM derives J0 = GHASH(IV || 0^64 || [128]_64),
|
||||
-- as in crypton_aes_gcm_init: https://hackage.haskell.org/package/crypton-0.34/src/cbits/crypton_aes.c
|
||||
-- WebCrypto and other SP 800-38D implementations interoperate only when given all 16 IV bytes.
|
||||
-- this function requires 16 bytes IV, it transforms IV in cryptonite_aes_gcm_init here:
|
||||
-- https://github.com/haskell-crypto/cryptonite/blob/master/cbits/cryptonite_aes.c
|
||||
-- This is used for double ratchet encryption, so to make it compatible with WebCrypto we will need to deprecate it and start using initAEADGCM
|
||||
initAEAD :: forall c. AES.BlockCipher c => Key -> IV -> ExceptT CryptoError IO (AES.AEAD c)
|
||||
initAEAD (Key aesKey) (IV ivBytes) = do
|
||||
iv <- makeIV @c ivBytes
|
||||
@@ -1331,8 +1327,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, CbNonceI n) => DhSecret X25519 -> n -> MaxLenBS i -> ByteString
|
||||
cbEncryptMaxLenBS (DhSecretX25519 secret) nonce = cryptoBox secret (unCbNonce nonce) . unMaxLenBS . padMaxLenBS
|
||||
cbEncryptMaxLenBS :: KnownNat i => DhSecret X25519 -> CbNonce -> MaxLenBS i -> ByteString
|
||||
cbEncryptMaxLenBS (DhSecretX25519 secret) (CbNonce nonce) = cryptoBox secret nonce . unMaxLenBS . padMaxLenBS
|
||||
{-# INLINE cbEncryptMaxLenBS #-}
|
||||
|
||||
cryptoBox :: ByteArrayAccess key => key -> ByteString -> ByteString -> ByteString
|
||||
@@ -1342,8 +1338,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 :: CbNonceI n => DhSecret X25519 -> n -> ByteString -> Either CryptoError ByteString
|
||||
cbDecrypt (DhSecretX25519 secret) = sbDecrypt_ secret . toCbNonce
|
||||
cbDecrypt :: DhSecret X25519 -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
cbDecrypt (DhSecretX25519 secret) = sbDecrypt_ secret
|
||||
{-# INLINE cbDecrypt #-}
|
||||
|
||||
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce (without unpadding).
|
||||
@@ -1390,16 +1386,9 @@ 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)
|
||||
|
||||
class CbNonceI n where
|
||||
unCbNonce :: n -> ByteString
|
||||
|
||||
newtype CbNonce = CryptoBoxNonce ByteString
|
||||
newtype CbNonce = CryptoBoxNonce {unCbNonce :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance CbNonceI CbNonce where
|
||||
unCbNonce (CryptoBoxNonce s) = s
|
||||
|
||||
instance FromField CbNonce where fromField = blobFieldDecoder cbNonce
|
||||
deriving newtype (FromField)
|
||||
|
||||
instance ToField CbNonce where toField (CryptoBoxNonce s) = toField $ Binary s
|
||||
|
||||
@@ -1410,7 +1399,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
|
||||
@@ -1419,16 +1408,13 @@ instance ToJSON CbNonce where
|
||||
instance FromJSON CbNonce where
|
||||
parseJSON = strParseJSON "CbNonce"
|
||||
|
||||
cbNonce :: ByteString -> Either String CbNonce
|
||||
cbNonce :: ByteString -> CbNonce
|
||||
cbNonce 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
|
||||
| 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
|
||||
|
||||
randomCbNonce :: TVar ChaChaDRG -> STM CbNonce
|
||||
randomCbNonce = fmap CryptoBoxNonce . randomBytes 24
|
||||
@@ -1436,30 +1422,13 @@ randomCbNonce = fmap CryptoBoxNonce . randomBytes 24
|
||||
randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString
|
||||
randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n
|
||||
|
||||
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
|
||||
reverseNonce :: CbNonce -> CbNonce
|
||||
reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s)
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | BIP-32 HD derivation over secp256k1, private only: we hold the seed, so CKDpub, xpub and fingerprints are not implemented.
|
||||
module Simplex.Messaging.Crypto.BIP32
|
||||
( ExtendedKey (..),
|
||||
masterKey,
|
||||
derivePath,
|
||||
renderPath,
|
||||
hardenedOffset,
|
||||
hardened,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (foldM)
|
||||
import qualified Crypto.Hash as H
|
||||
import qualified Crypto.MAC.HMAC as HMAC
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
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)
|
||||
|
||||
-- | An extended private key: the key plus its chain code, secret too, as it and one child derive the siblings.
|
||||
data ExtendedKey = ExtendedKey
|
||||
{ xkKey :: S.Secp256k1PrivateKey,
|
||||
xkChainCode :: ByteString
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
instance Show ExtendedKey where
|
||||
show _ = "ExtendedKey <redacted>"
|
||||
|
||||
-- | Child indexes at or above this are hardened.
|
||||
hardenedOffset :: Word32
|
||||
hardenedOffset = 0x80000000
|
||||
|
||||
-- | @hardened 44 == 44'@. An index at or above 'hardenedOffset' is returned unchanged, so this is idempotent rather than overflowing.
|
||||
hardened :: Word32 -> Word32
|
||||
hardened i
|
||||
| isHardened i = i
|
||||
| otherwise = i + hardenedOffset
|
||||
|
||||
isHardened :: Word32 -> Bool
|
||||
isHardened i = i >= hardenedOffset
|
||||
|
||||
-- | Derive the master key from a BIP-39 seed (BIP-32 allows 16 to 64 bytes).
|
||||
masterKey :: ByteString -> Either String ExtendedKey
|
||||
masterKey seed
|
||||
| seedLen < 16 || seedLen > 64 =
|
||||
Left $ "seed: expected 16 to 64 bytes, got " <> show seedLen
|
||||
| otherwise = case S.mkPrivateKey il of
|
||||
Left _ -> Left "seed: invalid master key, use a different seed"
|
||||
Right k -> Right ExtendedKey {xkKey = k, xkChainCode = ir}
|
||||
where
|
||||
seedLen = B.length seed
|
||||
i = hmacSHA512 "Bitcoin seed" seed
|
||||
il = B.take 32 i
|
||||
ir = B.drop 32 i
|
||||
|
||||
-- | CKDpriv. 'Left' only in the negligible case BIP-32 calls "proceed with the next index", which no real seed reaches.
|
||||
deriveChild :: ExtendedKey -> Word32 -> Either String ExtendedKey
|
||||
deriveChild xk i =
|
||||
case S.privateKeyTweakAdd (xkKey xk) il of
|
||||
Nothing -> Left $ "derivation: invalid child at index " <> show i <> ", use the next index"
|
||||
Just k -> Right ExtendedKey {xkKey = k, xkChainCode = ir}
|
||||
where
|
||||
dat
|
||||
| isHardened i = B.singleton 0 <> S.unPrivateKey (xkKey xk) <> smpEncode i
|
||||
| otherwise = S.serializePublicKey S.Compressed (S.secp256k1PublicKey (xkKey xk)) <> smpEncode i
|
||||
hm = hmacSHA512 (xkChainCode xk) dat
|
||||
il = B.take 32 hm
|
||||
ir = B.drop 32 hm
|
||||
|
||||
derivePath :: ExtendedKey -> [Word32] -> Either String ExtendedKey
|
||||
derivePath = foldM deriveChild
|
||||
|
||||
|
||||
|
||||
renderPath :: [Word32] -> ByteString
|
||||
renderPath is = BC.pack $ intercalate "/" ("m" : map component is)
|
||||
where
|
||||
component i
|
||||
| isHardened i = show (i - hardenedOffset) <> "'"
|
||||
| otherwise = show i
|
||||
|
||||
hmacSHA512 :: ByteString -> ByteString -> ByteString
|
||||
hmacSHA512 key msg = BA.convert (HMAC.hmac key msg :: HMAC.HMAC H.SHA512)
|
||||
@@ -0,0 +1,161 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | BIP-39 mnemonics, English only: every word is ASCII, so BIP-39's NFKD normalization is a no-op; a passphrase is bytes the caller normalizes.
|
||||
module Simplex.Messaging.Crypto.BIP39
|
||||
( Mnemonic,
|
||||
MnemonicStrength (..),
|
||||
mnemonicWords,
|
||||
mnemonicPhrase,
|
||||
entropyToMnemonic,
|
||||
mnemonicToEntropy,
|
||||
parseMnemonic,
|
||||
mnemonicToSeed,
|
||||
randomMnemonic,
|
||||
strengthWordCount,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Crypto.Hash (SHA512 (..))
|
||||
import qualified Crypto.KDF.PBKDF2 as PBKDF2
|
||||
import Crypto.Number.Serialize (i2ospOf_, os2ip)
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import Data.Char (isSpace, toLower)
|
||||
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 qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.BIP39.English (englishWordList)
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
|
||||
-- | A BIP-39 mnemonic: the word indexes and the words they name.
|
||||
data Mnemonic = Mnemonic
|
||||
{ mnemonicIndexes :: [Int],
|
||||
mnemonicWords :: [ByteString]
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
instance Show Mnemonic where
|
||||
show m = "Mnemonic <" <> show (length (mnemonicIndexes m)) <> " words, redacted>"
|
||||
|
||||
-- | Entropy size, named by bit length as BIP-39 does.
|
||||
data MnemonicStrength = MS128 | MS160 | MS192 | MS224 | MS256
|
||||
deriving (Eq, Show, Bounded, Enum)
|
||||
|
||||
strengthBytes :: MnemonicStrength -> Int
|
||||
strengthBytes = \case
|
||||
MS128 -> 16
|
||||
MS160 -> 20
|
||||
MS192 -> 24
|
||||
MS224 -> 28
|
||||
MS256 -> 32
|
||||
|
||||
-- | 12, 15, 18, 21 or 24.
|
||||
strengthWordCount :: MnemonicStrength -> Int
|
||||
strengthWordCount s = (entBits + entBits `div` 32) `div` 11
|
||||
where
|
||||
entBits = strengthBytes s * 8
|
||||
|
||||
-- | BIP-39 seeds are always 64 bytes, whatever the entropy size.
|
||||
seedSize :: Int
|
||||
seedSize = 64
|
||||
|
||||
validEntropySizes :: [Int]
|
||||
validEntropySizes = map strengthBytes [minBound .. maxBound]
|
||||
|
||||
validWordCounts :: [Int]
|
||||
validWordCounts = map strengthWordCount [minBound .. maxBound]
|
||||
|
||||
-- Wordlist indexes
|
||||
|
||||
wordByIndex :: IntMap ByteString
|
||||
wordByIndex = IM.fromList $ zip [0 ..] englishWordList
|
||||
{-# NOINLINE wordByIndex #-}
|
||||
|
||||
indexByWord :: Map ByteString Int
|
||||
indexByWord = M.fromList $ zip englishWordList [0 ..]
|
||||
{-# NOINLINE indexByWord #-}
|
||||
|
||||
-- | The mnemonic as one space-separated phrase, the exact bytes BIP-39 feeds to PBKDF2.
|
||||
mnemonicPhrase :: Mnemonic -> ByteString
|
||||
mnemonicPhrase = BC.unwords . mnemonicWords
|
||||
|
||||
-- | Build a mnemonic from raw entropy of 16, 20, 24, 28 or 32 bytes.
|
||||
entropyToMnemonic :: ByteString -> Either String Mnemonic
|
||||
entropyToMnemonic ent
|
||||
| entLen `notElem` validEntropySizes =
|
||||
Left $ "entropy: expected 16, 20, 24, 28 or 32 bytes, got " <> show entLen
|
||||
| otherwise = Right $ mnemonicFromIndexes $ entropyToIndexes ent
|
||||
where
|
||||
entLen = B.length ent
|
||||
|
||||
-- | Entropy to 11-bit word indexes: assumes a validated length, and masks every result into @[0, 2047]@.
|
||||
entropyToIndexes :: ByteString -> [Int]
|
||||
entropyToIndexes ent =
|
||||
[fromIntegral ((combined `shiftR` (11 * (n - 1 - i))) .&. 0x7FF) | i <- [0 .. n - 1]]
|
||||
where
|
||||
entBits = B.length ent * 8
|
||||
csBits = entBits `div` 32
|
||||
-- csBits is at most 8 (256/32), so the first checksum byte always suffices.
|
||||
csByte = B.head (C.sha256Hash ent)
|
||||
combined = os2ip ent `shiftL` csBits .|. fromIntegral (csByte `shiftR` (8 - csBits))
|
||||
n = (entBits + csBits) `div` 11
|
||||
|
||||
-- | The entropy the mnemonic encodes.
|
||||
mnemonicToEntropy :: Mnemonic -> ByteString
|
||||
mnemonicToEntropy m = i2ospOf_ (entBits `div` 8) (combined `shiftR` csBits)
|
||||
where
|
||||
idxs = mnemonicIndexes m
|
||||
totalBits = length idxs * 11
|
||||
entBits = totalBits * 32 `div` 33
|
||||
csBits = totalBits - entBits
|
||||
combined = foldl' (\acc i -> acc `shiftL` 11 .|. fromIntegral i) (0 :: Integer) idxs
|
||||
|
||||
-- | Parse and fully validate a phrase: word count, wordlist membership, checksum. Any whitespace separates, and lower-casing does not change the seed.
|
||||
parseMnemonic :: ByteString -> Either String Mnemonic
|
||||
parseMnemonic = parseAll mnemonicP
|
||||
|
||||
mnemonicP :: A.Parser Mnemonic
|
||||
mnemonicP = do
|
||||
ws <- A.skipWhile isSpace *> (wordP `A.sepBy'` A.takeWhile1 isSpace) <* A.skipWhile isSpace
|
||||
let n = length ws
|
||||
if n `notElem` validWordCounts
|
||||
then fail $ "mnemonic: expected 12, 15, 18, 21 or 24 words, got " <> show n
|
||||
else do
|
||||
idxs <- traverse lookupWord ws
|
||||
let m = mnemonicFromIndexes idxs
|
||||
-- stripping the checksum bits and recomputing them is the check: wrong bits cannot survive the round trip
|
||||
if entropyToIndexes (mnemonicToEntropy m) == idxs
|
||||
then pure m
|
||||
else fail "mnemonic: checksum mismatch"
|
||||
where
|
||||
wordP = BC.map toLower <$> A.takeWhile1 (not . isSpace)
|
||||
lookupWord w = maybe (fail $ "mnemonic: not in wordlist: " <> BC.unpack w) pure $ M.lookup w indexByWord
|
||||
|
||||
-- | PBKDF2-HMAC-SHA512, 2048 iterations, salt @\"mnemonic\" <> passphrase@; pass an empty passphrase for the common case.
|
||||
mnemonicToSeed :: Mnemonic -> ByteString -> ByteString
|
||||
mnemonicToSeed m passphrase =
|
||||
PBKDF2.generate
|
||||
(PBKDF2.prfHMAC SHA512)
|
||||
PBKDF2.Parameters {PBKDF2.iterCounts = 2048, PBKDF2.outputLength = seedSize}
|
||||
(mnemonicPhrase m)
|
||||
("mnemonic" <> passphrase :: ByteString)
|
||||
|
||||
-- | Generate a fresh mnemonic from the agent's DRG.
|
||||
randomMnemonic :: MnemonicStrength -> TVar ChaChaDRG -> STM Mnemonic
|
||||
randomMnemonic s gVar = mnemonicFromIndexes . entropyToIndexes <$> C.randomBytes (strengthBytes s) gVar
|
||||
|
||||
-- Internal
|
||||
|
||||
-- | Indexes are in @[0, 2047]@: an 11-bit mask, or a lookup in the wordlist itself.
|
||||
mnemonicFromIndexes :: [Int] -> Mnemonic
|
||||
mnemonicFromIndexes idxs =
|
||||
Mnemonic {mnemonicIndexes = idxs, mnemonicWords = map (wordByIndex IM.!) idxs}
|
||||
@@ -0,0 +1,12 @@
|
||||
{-# 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)
|
||||
|
||||
-- | All 2048 words, in BIP-39 index order (index 0 is @abandon@).
|
||||
englishWordList :: [ByteString]
|
||||
englishWordList = BC.lines $(embedFile "src/Simplex/Messaging/Crypto/BIP39/english.txt")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,6 @@ module Simplex.Messaging.Crypto.Ratchet
|
||||
rcEncryptHeader,
|
||||
rcEncryptMsg,
|
||||
rcDecrypt,
|
||||
maxSkippedMsgKeys,
|
||||
-- used in tests
|
||||
MsgHeader (..),
|
||||
RatchetInitParams (..),
|
||||
@@ -120,7 +119,7 @@ import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, parseE, parseE')
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (($>>=), (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
import UnliftIO.STM
|
||||
@@ -328,13 +327,9 @@ instance StrEncoding AnyE2ERatchetParamsUri where
|
||||
Nothing -> pure $ AnyE2ERatchetParamsUri SRKSProposed a $ E2ERatchetParamsUri vr k1 k2 Nothing
|
||||
_ -> fail "bad e2e params"
|
||||
where
|
||||
kemP query = do
|
||||
k_ <- queryParam_ "kem_key" query
|
||||
ct_ <- queryParam_ "kem_ct" query
|
||||
case (k_, ct_) of
|
||||
(Just k, _) -> pure $ Just $ kemParams k ct_
|
||||
(Nothing, Nothing) -> pure Nothing
|
||||
(Nothing, Just _) -> fail "bad e2e params: kem_ct without kem_key"
|
||||
kemP query =
|
||||
queryParam_ "kem_key" query
|
||||
$>>= \k -> Just . kemParams k <$> queryParam_ "kem_ct" query
|
||||
kemParams k = \case
|
||||
Nothing -> ARKP SRKSProposed $ RKParamsProposed k
|
||||
Just ct -> ARKP SRKSAccepted $ RKParamsAccepted ct k
|
||||
@@ -456,7 +451,6 @@ generateSndE2EParams g v = \case
|
||||
|
||||
data RatchetInitParams = RatchetInitParams
|
||||
{ assocData :: Str,
|
||||
rcVerifyCodePQ :: Str,
|
||||
ratchetKey :: RatchetKey,
|
||||
sndHK :: HeaderKey,
|
||||
rcvNextHK :: HeaderKey,
|
||||
@@ -499,14 +493,14 @@ pqX3dhRcv (rpk1, rpk2, rpKem_) (E2ERatchetParams _ sk1 sk2 sKem_) = do
|
||||
|
||||
pqX3dh :: DhAlgorithm a => (PublicKey a, PublicKey a) -> DhSecret a -> DhSecret a -> DhSecret a -> Maybe RatchetKEMAccepted -> RatchetInitParams
|
||||
pqX3dh (sk1, rk1) dh1 dh2 dh3 kemAccepted =
|
||||
RatchetInitParams {assocData, rcVerifyCodePQ = Str vcPQ, ratchetKey = RatchetKey sk, sndHK = Key hk, rcvNextHK = Key nhk, kemAccepted}
|
||||
RatchetInitParams {assocData, ratchetKey = RatchetKey sk, sndHK = Key hk, rcvNextHK = Key nhk, kemAccepted}
|
||||
where
|
||||
assocData = Str $ pubKeyBytes sk1 <> pubKeyBytes rk1
|
||||
dhs = dhBytes' dh1 <> dhBytes' dh2 <> dhBytes' dh3 <> pq
|
||||
pq = maybe "" (\RatchetKEMAccepted {rcPQRss = KEMSharedKey ss} -> BA.convert ss) kemAccepted
|
||||
salt = B.replicate 64 '\0'
|
||||
(hk, nhk, sk) = hkdf3 salt dhs "SimpleXX3DH"
|
||||
vcPQ = hkdf salt dhs "SimpleXVerifyCode" 32
|
||||
(hk, nhk, sk) =
|
||||
let salt = B.replicate 64 '\0'
|
||||
in hkdf3 salt dhs "SimpleXX3DH"
|
||||
|
||||
type RatchetX448 = Ratchet 'X448
|
||||
|
||||
@@ -515,8 +509,6 @@ data Ratchet a = Ratchet
|
||||
rcVersion :: RatchetVersions,
|
||||
-- associated data - must be the same in both parties ratchets
|
||||
rcAD :: Str,
|
||||
-- verification code covering all handshake keys, absent in the ratchets created before it was added
|
||||
rcVCPQ :: Maybe Str,
|
||||
rcDHRs :: PrivateKey a,
|
||||
rcKEM :: Maybe RatchetKEM,
|
||||
rcSupportKEM :: PQSupport, -- defines header size, can only be enabled once
|
||||
@@ -645,14 +637,13 @@ instance FromField MessageKey where fromField = blobFieldDecoder smpDecode
|
||||
-- @
|
||||
initSndRatchet ::
|
||||
forall a. (AlgorithmI a, DhAlgorithm a) => RatchetVersions -> PublicKey a -> PrivateKey a -> (RatchetInitParams, Maybe KEMKeyPair) -> Ratchet a
|
||||
initSndRatchet rcVersion rcDHRr rcDHRs (RatchetInitParams {assocData, rcVerifyCodePQ, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) = do
|
||||
initSndRatchet rcVersion rcDHRr rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) = do
|
||||
-- state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss)
|
||||
let (rcRK, rcCKs, rcNHKs) = rootKdf ratchetKey rcDHRr rcDHRs (rcPQRss <$> kemAccepted)
|
||||
pqOn = isJust rcPQRs_
|
||||
in Ratchet
|
||||
{ rcVersion,
|
||||
rcAD = assocData,
|
||||
rcVCPQ = Just rcVerifyCodePQ,
|
||||
rcDHRs,
|
||||
rcKEM = (`RatchetKEM` kemAccepted) <$> rcPQRs_,
|
||||
rcSupportKEM = PQSupport pqOn,
|
||||
@@ -677,11 +668,10 @@ initSndRatchet rcVersion rcDHRr rcDHRs (RatchetInitParams {assocData, rcVerifyCo
|
||||
-- as part of the connection request and random salt was received from the sender.
|
||||
initRcvRatchet ::
|
||||
forall a. (AlgorithmI a, DhAlgorithm a) => RatchetVersions -> PrivateKey a -> (RatchetInitParams, Maybe KEMKeyPair) -> PQSupport -> Ratchet a
|
||||
initRcvRatchet rcVersion rcDHRs (RatchetInitParams {assocData, rcVerifyCodePQ, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) pqSupport =
|
||||
initRcvRatchet rcVersion rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) pqSupport =
|
||||
Ratchet
|
||||
{ rcVersion,
|
||||
rcAD = assocData,
|
||||
rcVCPQ = Just rcVerifyCodePQ,
|
||||
rcDHRs,
|
||||
-- rcKEM:
|
||||
-- state.PQRs = bob_pq_kem_key_pair
|
||||
@@ -963,9 +953,6 @@ type DecryptResult a = (Either CryptoError ByteString, Ratchet a, SkippedMsgDiff
|
||||
maxSkip :: Word32
|
||||
maxSkip = 512
|
||||
|
||||
maxSkippedMsgKeys :: Int
|
||||
maxSkippedMsgKeys = 2000
|
||||
|
||||
rcDecrypt ::
|
||||
forall a.
|
||||
(AlgorithmI a, DhAlgorithm a) =>
|
||||
|
||||
@@ -16,8 +16,6 @@ module Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
) where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (throwIO)
|
||||
import Control.Monad (when)
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Bifunctor (bimap)
|
||||
@@ -25,7 +23,6 @@ import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG (rngFuncPtr, withDRG)
|
||||
@@ -58,13 +55,14 @@ pattern KEMSharedKey s <- KEMSharedKey_ s
|
||||
type KEMKeyPair = (KEMPublicKey, KEMSecretKey)
|
||||
|
||||
sntrup761Keypair :: TVar ChaChaDRG -> IO KEMKeyPair
|
||||
sntrup761Keypair drg = do
|
||||
((r, pk), sk) <-
|
||||
BA.allocRet c_SNTRUP761_SECRETKEY_SIZE $ \skPtr ->
|
||||
BA.allocRet c_SNTRUP761_PUBLICKEY_SIZE $ \pkPtr ->
|
||||
withDRG drg $ \cxtPtr -> c_sntrup761_keypair pkPtr skPtr cxtPtr rngFuncPtr
|
||||
when (r /= 0) $ throwIO C.CryptoKEMKeyGenError
|
||||
pure (KEMPublicKey_ pk, KEMSecretKey sk)
|
||||
sntrup761Keypair drg =
|
||||
bimap KEMPublicKey_ KEMSecretKey
|
||||
<$> BA.allocRet
|
||||
c_SNTRUP761_SECRETKEY_SIZE
|
||||
( \skPtr ->
|
||||
BA.alloc c_SNTRUP761_PUBLICKEY_SIZE $ \pkPtr ->
|
||||
withDRG drg $ \cxtPtr -> c_sntrup761_keypair pkPtr skPtr cxtPtr rngFuncPtr
|
||||
)
|
||||
|
||||
sntrup761Enc :: TVar ChaChaDRG -> KEMPublicKey -> IO (KEMCiphertext, KEMSharedKey)
|
||||
sntrup761Enc drg (KEMPublicKey pk) =
|
||||
|
||||
@@ -10,9 +10,9 @@ import Foreign
|
||||
import Foreign.C
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG (RNGContext, RNGFunc)
|
||||
|
||||
-- int sntrup761_keypair (uint8_t *pk, uint8_t *sk, void *random_ctx, sntrup761_random_func *random);
|
||||
-- void sntrup761_keypair (uint8_t *pk, uint8_t *sk, void *random_ctx, sntrup761_random_func *random);
|
||||
foreign import ccall "sntrup761_keypair"
|
||||
c_sntrup761_keypair :: Ptr Word8 -> Ptr Word8 -> Ptr RNGContext -> FunPtr RNGFunc -> IO CInt
|
||||
c_sntrup761_keypair :: Ptr Word8 -> Ptr Word8 -> Ptr RNGContext -> FunPtr RNGFunc -> IO ()
|
||||
|
||||
-- void sntrup761_enc (uint8_t *c, uint8_t *k, const uint8_t *pk, void *random_ctx, sntrup761_random_func *random);
|
||||
foreign import ccall "sntrup761_enc"
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
-- | FFI bindings to libsecp256k1. Every operation is a pure function of its arguments, hence the pure API over 'unsafePerformIO'.
|
||||
module Simplex.Messaging.Crypto.Secp256k1
|
||||
( Secp256k1PrivateKey,
|
||||
Secp256k1PublicKey,
|
||||
PubKeyFormat (..),
|
||||
mkPrivateKey,
|
||||
unPrivateKey,
|
||||
secp256k1PublicKey,
|
||||
serializePublicKey,
|
||||
privateKeyTweakAdd,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (when)
|
||||
import Crypto.Random (drgNew, randomBytesGenerate)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Unsafe as BU
|
||||
import Foreign
|
||||
import Foreign.C
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
-- Sizes
|
||||
|
||||
-- | A secp256k1 scalar is 32 bytes, big-endian.
|
||||
privateKeySize :: Int
|
||||
privateKeySize = 32
|
||||
|
||||
-- | SEC1 compressed point: @0x02@/@0x03@ prefix and the x coordinate.
|
||||
compressedSize :: Int
|
||||
compressedSize = 33
|
||||
|
||||
-- | SEC1 uncompressed point: @0x04@ prefix, x, y.
|
||||
uncompressedSize :: Int
|
||||
uncompressedSize = 65
|
||||
|
||||
|
||||
-- | Internal size of @secp256k1_pubkey@ (opaque, not a serialization).
|
||||
pubKeyInternalSize :: Int
|
||||
pubKeyInternalSize = 64
|
||||
|
||||
|
||||
-- Types
|
||||
|
||||
-- | A validated private key, 32 bytes in @[1, n-1]@: no 'Show', so it cannot be logged by accident, and constant-time 'Eq'.
|
||||
newtype Secp256k1PrivateKey = Secp256k1PrivateKey ByteString
|
||||
|
||||
instance Eq Secp256k1PrivateKey where
|
||||
Secp256k1PrivateKey a == Secp256k1PrivateKey b = BA.constEq a b
|
||||
|
||||
-- | A public key in libsecp256k1's opaque 64-byte form; 'serializePublicKey' gives the SEC1 bytes.
|
||||
newtype Secp256k1PublicKey = Secp256k1PublicKey ByteString
|
||||
deriving newtype (Eq, Show)
|
||||
|
||||
-- | SEC1 output format for 'serializePublicKey'.
|
||||
data PubKeyFormat = Compressed | Uncompressed
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- FFI
|
||||
|
||||
data Ctx
|
||||
|
||||
data PubKeyRaw
|
||||
|
||||
foreign import ccall "secp256k1_context_create"
|
||||
c_context_create :: CUInt -> IO (Ptr Ctx)
|
||||
|
||||
foreign import ccall "secp256k1_context_randomize"
|
||||
c_context_randomize :: Ptr Ctx -> Ptr Word8 -> IO CInt
|
||||
|
||||
foreign import ccall "secp256k1_ec_seckey_verify"
|
||||
c_ec_seckey_verify :: Ptr Ctx -> Ptr Word8 -> IO CInt
|
||||
|
||||
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
|
||||
|
||||
-- SECP256K1_CONTEXT_NONE = SECP256K1_FLAGS_TYPE_CONTEXT
|
||||
contextNone :: CUInt
|
||||
contextNone = 1
|
||||
|
||||
-- | The process-wide context, created and blinded once. Blinding affects no output and nothing mutates it, so sharing it across threads is safe.
|
||||
secp256k1Ctx :: Ptr Ctx
|
||||
secp256k1Ctx = unsafePerformIO $ do
|
||||
ctx <- c_context_create contextNone
|
||||
when (ctx == nullPtr) $ ioError (userError "secp256k1_context_create failed")
|
||||
drg <- drgNew
|
||||
let (seed :: ByteString, _) = randomBytesGenerate 32 drg
|
||||
rc <- BU.unsafeUseAsCString seed $ \p -> c_context_randomize ctx (castPtr p)
|
||||
when (rc /= 1) $ ioError (userError "secp256k1_context_randomize failed")
|
||||
pure ctx
|
||||
{-# NOINLINE secp256k1Ctx #-}
|
||||
|
||||
-- Helpers
|
||||
|
||||
withBS :: ByteString -> (Ptr Word8 -> IO a) -> IO a
|
||||
withBS bs f = BU.unsafeUseAsCString bs $ f . castPtr
|
||||
|
||||
packPtr :: Ptr Word8 -> Int -> IO ByteString
|
||||
packPtr p n = B.packCStringLen (castPtr p, n)
|
||||
|
||||
-- | Marshal a 'Secp256k1PublicKey' back into its opaque C representation.
|
||||
withPubKeyRaw :: Secp256k1PublicKey -> (Ptr PubKeyRaw -> IO a) -> IO a
|
||||
withPubKeyRaw (Secp256k1PublicKey bs) f = withBS bs $ f . castPtr
|
||||
|
||||
|
||||
-- Public API
|
||||
|
||||
-- | Reject zero and anything at or above the group order, which is what makes 'secp256k1PublicKey' total.
|
||||
mkPrivateKey :: ByteString -> Either String Secp256k1PrivateKey
|
||||
mkPrivateKey bs
|
||||
| B.length bs /= privateKeySize = Left $ "private key: expected 32 bytes, got " <> show (B.length bs)
|
||||
| otherwise = unsafePerformIO $ withBS bs $ \p -> do
|
||||
rc <- c_ec_seckey_verify secp256k1Ctx p
|
||||
pure $ if rc == 1 then Right (Secp256k1PrivateKey bs) else Left "private key: not in [1, n-1]"
|
||||
|
||||
unPrivateKey :: Secp256k1PrivateKey -> ByteString
|
||||
unPrivateKey (Secp256k1PrivateKey bs) = bs
|
||||
|
||||
-- | Derive the public key. Total, because 'Secp256k1PrivateKey' is validated.
|
||||
secp256k1PublicKey :: Secp256k1PrivateKey -> Secp256k1PublicKey
|
||||
secp256k1PublicKey (Secp256k1PrivateKey sk) = unsafePerformIO $
|
||||
allocaBytes pubKeyInternalSize $ \pkPtr ->
|
||||
withBS sk $ \skPtr -> do
|
||||
rc <- c_ec_pubkey_create secp256k1Ctx pkPtr skPtr
|
||||
-- Cannot fail: the key was verified by mkPrivateKey.
|
||||
when (rc /= 1) $ ioError (userError "secp256k1_ec_pubkey_create failed on a validated key")
|
||||
Secp256k1PublicKey <$> packPtr (castPtr pkPtr) pubKeyInternalSize
|
||||
|
||||
|
||||
serializePublicKey :: PubKeyFormat -> Secp256k1PublicKey -> ByteString
|
||||
serializePublicKey fmt pk = unsafePerformIO $
|
||||
allocaBytes outLen $ \outPtr ->
|
||||
alloca $ \lenPtr ->
|
||||
withPubKeyRaw pk $ \pkPtr -> do
|
||||
poke lenPtr (fromIntegral outLen)
|
||||
rc <- c_ec_pubkey_serialize secp256k1Ctx outPtr lenPtr pkPtr flag
|
||||
when (rc /= 1) $ ioError (userError "secp256k1_ec_pubkey_serialize failed")
|
||||
written <- peek lenPtr
|
||||
packPtr outPtr (fromIntegral written)
|
||||
where
|
||||
-- SECP256K1_EC_COMPRESSED = FLAGS_TYPE_COMPRESSION | FLAGS_BIT_COMPRESSION, SECP256K1_EC_UNCOMPRESSED = FLAGS_TYPE_COMPRESSION
|
||||
(flag, outLen) = case fmt of
|
||||
Compressed -> (2 .|. 256, compressedSize)
|
||||
Uncompressed -> (2, uncompressedSize)
|
||||
|
||||
-- | @sk + tweak mod n@, as BIP-32 child derivation needs. 'Nothing' when the result is zero or the tweak is out of range.
|
||||
privateKeyTweakAdd :: Secp256k1PrivateKey -> ByteString -> Maybe Secp256k1PrivateKey
|
||||
privateKeyTweakAdd (Secp256k1PrivateKey sk) tweak
|
||||
| B.length tweak /= privateKeySize = Nothing
|
||||
| otherwise = unsafePerformIO $
|
||||
allocaBytes privateKeySize $ \skPtr ->
|
||||
withBS tweak $ \twPtr -> do
|
||||
withBS sk $ \src -> copyBytes skPtr src privateKeySize
|
||||
rc <- c_ec_seckey_tweak_add secp256k1Ctx skPtr twPtr
|
||||
if rc == 1
|
||||
then Just . Secp256k1PrivateKey <$> packPtr skPtr privateKeySize
|
||||
else pure Nothing
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
-- | Ethereum addresses: derivation from a public key, and EIP-55 mixed-case checksum encoding.
|
||||
module Simplex.Messaging.Eth.Address
|
||||
( Address,
|
||||
addressFromPrivateKey,
|
||||
ethereumPath,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
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 (isDigit, isHexDigit, isLower, isUpper, toLower)
|
||||
import Data.Word (Word32, Word8)
|
||||
import Simplex.Messaging.Crypto.BIP32 (hardened)
|
||||
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Eth.Keccak (keccak256)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
|
||||
-- | A 20-byte Ethereum address. 'Show' renders the EIP-55 checksummed form, as pasted into a block explorer.
|
||||
newtype Address = Address ByteString
|
||||
deriving (Eq, Ord)
|
||||
|
||||
instance Show Address where
|
||||
show = BC.unpack . checksumAddress
|
||||
|
||||
-- | EIP-55 checksummed hex. Parsing accepts bare or @0x@-prefixed hex and verifies a mixed-case checksum.
|
||||
instance StrEncoding Address where
|
||||
strEncode = checksumAddress
|
||||
strP = do
|
||||
_ <- optional $ A.string "0x" <|> A.string "0X"
|
||||
body <- A.takeWhile isHexDigit
|
||||
if B.length body /= addressSize * 2
|
||||
then fail $ "address: expected 40 hex digits, got " <> show (B.length body)
|
||||
else case fromHex (BC.map toLower body) of
|
||||
Left _ -> fail "address: not hexadecimal"
|
||||
Right bs
|
||||
| mixedCase body && checksumAddress (Address bs) /= "0x" <> body ->
|
||||
fail "address: EIP-55 checksum mismatch"
|
||||
| otherwise -> pure $ Address bs
|
||||
where
|
||||
mixedCase body = BC.any isUpper letters && BC.any isLower letters
|
||||
where
|
||||
letters = BC.filter (not . isDigit) body
|
||||
|
||||
-- | The EIP-55 hex, length-prefixed, so a field following it is not swallowed by the hex parser.
|
||||
instance Encoding Address where
|
||||
smpEncode = smpEncode . strEncode
|
||||
smpP = strDecode <$?> smpP
|
||||
|
||||
addressSize :: Int
|
||||
addressSize = 20
|
||||
|
||||
-- | The low 20 bytes of @keccak256@ of the uncompressed public key with its @0x04@ SEC1 prefix removed.
|
||||
addressFromPublicKey :: S.Secp256k1PublicKey -> Address
|
||||
addressFromPublicKey pk =
|
||||
Address . B.drop 12 . keccak256 . B.drop 1 $ S.serializePublicKey S.Uncompressed pk
|
||||
|
||||
addressFromPrivateKey :: S.Secp256k1PrivateKey -> Address
|
||||
addressFromPrivateKey = addressFromPublicKey . S.secp256k1PublicKey
|
||||
|
||||
-- | EIP-55: @0x@ and 40 hex digits whose case encodes a checksum over the lowercase hex form.
|
||||
checksumAddress :: Address -> ByteString
|
||||
checksumAddress (Address bs) = "0x" <> B.pack (zipWith adjust [0 ..] (B.unpack lowerHex))
|
||||
where
|
||||
lowerHex = toHex bs
|
||||
hashed = keccak256 lowerHex
|
||||
adjust :: Int -> Word8 -> Word8
|
||||
adjust i c
|
||||
| isHexLetter c && nibbleAt i >= 8 = upper c
|
||||
| otherwise = c
|
||||
nibbleAt i =
|
||||
let byte = B.index hashed (i `div` 2)
|
||||
in if even i then byte `shiftR` 4 else byte .&. 0x0F
|
||||
isHexLetter c = c >= 0x61 && c <= 0x66 -- 'a'..'f'
|
||||
upper c = c - 0x20
|
||||
|
||||
|
||||
-- | BIP-44 path for Ethereum account @i@, address @k@: @m\/44'\/60'\/i'\/0\/k@. The account must be below @hardenedOffset@.
|
||||
ethereumPath :: Word32 -> Word32 -> [Word32]
|
||||
ethereumPath account address = [hardened 44, hardened 60, hardened account, 0, address]
|
||||
|
||||
-- hex via memory's Base16, which emits the lowercase form EIP-55 starts from; 'checksumAddress' is what introduces case
|
||||
|
||||
toHex :: ByteString -> ByteString
|
||||
toHex = BAE.convertToBase BAE.Base16
|
||||
|
||||
-- | Decodes base16; a non-hex or odd-length input is a Left.
|
||||
fromHex :: ByteString -> Either String ByteString
|
||||
fromHex = BAE.convertFromBase BAE.Base16
|
||||
@@ -0,0 +1,12 @@
|
||||
-- | Keccak-256, the hash Ethereum uses. Not SHA3-256: they differ in the padding byte and produce entirely different digests.
|
||||
module Simplex.Messaging.Eth.Keccak
|
||||
( keccak256,
|
||||
)
|
||||
where
|
||||
|
||||
import Crypto.Hash (Digest, Keccak_256, hash)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
|
||||
keccak256 :: ByteString -> ByteString
|
||||
keccak256 bs = BA.convert (hash bs :: Digest Keccak_256)
|
||||
@@ -13,6 +13,7 @@ module Simplex.Messaging.Names.Record
|
||||
NamePricing (..),
|
||||
USDCents (..),
|
||||
NameReservedReason (..),
|
||||
OwnedNames (..),
|
||||
)
|
||||
where
|
||||
|
||||
@@ -90,6 +91,17 @@ data NameRegistration
|
||||
NRReserved {reservedReason :: NameReservedReason}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | What the registry holds for an address: the names it owns, each as the 'NameResponse' resolving it gives, and whether the account is in use
|
||||
data OwnedNames = OwnedNames
|
||||
{ -- | the oldest block any of the reads behind this answer saw, so a lagging registry shows even where no name does
|
||||
ownLastBlockTs :: Maybe SystemSeconds,
|
||||
ownNames :: [NameResponse],
|
||||
ownInUse :: Bool,
|
||||
-- | the cursor to resume from, absent when the listing is complete
|
||||
ownNextOffset :: Maybe Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Enough to price the name locally, which the router cannot do behind a hash.
|
||||
data NamePricing = NamePricing
|
||||
{ -- | US cents per year, for the lengths the registry prices specially
|
||||
@@ -142,3 +154,5 @@ $(JQ.deriveJSON defaultJSON ''NamePricing)
|
||||
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "NR") ''NameRegistration)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''NameResponse)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON {J.fieldLabelModifier = dropPrefix "own"} ''OwnedNames)
|
||||
|
||||
@@ -145,10 +145,7 @@ module Simplex.Messaging.Protocol
|
||||
RcvNtfDhSecret,
|
||||
Message (..),
|
||||
RcvMessage (..),
|
||||
MsgId (unMsgId),
|
||||
mkMsgId,
|
||||
unsafeMsgId,
|
||||
randomMsgId,
|
||||
MsgId,
|
||||
MsgBody,
|
||||
IdsHash (..),
|
||||
ServiceSub (..),
|
||||
@@ -171,7 +168,6 @@ module Simplex.Messaging.Protocol
|
||||
EncFwdTransmission (..),
|
||||
EncResponse (..),
|
||||
EncTransmission (..),
|
||||
encTransmissionNonce,
|
||||
FwdResponse (..),
|
||||
FwdTransmission (..),
|
||||
NameRecord (..),
|
||||
@@ -236,10 +232,8 @@ 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
|
||||
@@ -247,7 +241,6 @@ import Data.Attoparsec.ByteString.Char8 (Parser, (<?>))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.Bits (xor)
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -268,16 +261,17 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word8, Word16)
|
||||
import Data.Word (Word8, Word16, Word32)
|
||||
import GHC.TypeLits (ErrorMessage (..), TypeError, type (+))
|
||||
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 (..), blobFieldDecoder)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Eth.Address (Address)
|
||||
import Simplex.Messaging.Names.Record
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol.Types
|
||||
@@ -617,6 +611,8 @@ data Command (p :: Party) where
|
||||
RFWD :: EncFwdTransmission -> Command ProxyService -- use CorrId as CbNonce, proxy to relay
|
||||
-- Resolve SimpleX name.
|
||||
RSLV :: NameQuery -> Command Resolver
|
||||
-- Names an address owns, and whether the account has been used at all.
|
||||
ROWN :: Address -> Word32 -> Command Resolver
|
||||
|
||||
deriving instance Show (Command p)
|
||||
|
||||
@@ -708,24 +704,19 @@ instance Encoding NewNtfCreds where
|
||||
newtype EncTransmission = EncTransmission ByteString
|
||||
deriving (Show)
|
||||
|
||||
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 :: C.CorrCbNonce,
|
||||
{ fwdCorrId :: CorrId,
|
||||
fwdVersion :: VersionSMP,
|
||||
fwdKey :: C.PublicKeyX25519,
|
||||
fwdTransmission :: EncTransmission
|
||||
}
|
||||
|
||||
instance Encoding FwdTransmission where
|
||||
smpEncode FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} =
|
||||
smpEncode (fwdCorrId, fwdVersion, fwdKey, Tail t)
|
||||
smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} =
|
||||
smpEncode (corrId, fwdVersion, fwdKey, Tail t)
|
||||
smpP = do
|
||||
(fwdCorrId, fwdVersion, fwdKey, Tail t) <- smpP
|
||||
pure FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t}
|
||||
(corrId, fwdVersion, fwdKey, Tail t) <- smpP
|
||||
pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t}
|
||||
|
||||
newtype EncFwdTransmission = EncFwdTransmission ByteString
|
||||
deriving (Show)
|
||||
@@ -748,8 +739,8 @@ data BrokerMsg where
|
||||
NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg
|
||||
-- Should include certificate chain
|
||||
PKEY :: SessionId -> VersionRangeSMP -> CertChainPubKey -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key
|
||||
RRES :: Maybe C.CbNonce -> EncFwdResponse -> BrokerMsg -- relay to proxy
|
||||
PRES :: Maybe C.CbNonce -> EncResponse -> BrokerMsg -- proxy to client
|
||||
RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy
|
||||
PRES :: EncResponse -> BrokerMsg -- proxy to client
|
||||
END :: BrokerMsg
|
||||
ENDS :: Int64 -> IdsHash -> BrokerMsg
|
||||
DELD :: BrokerMsg
|
||||
@@ -759,6 +750,8 @@ data BrokerMsg where
|
||||
PONG :: BrokerMsg
|
||||
-- What the router knows about a SimpleX name.
|
||||
RNAME :: NameResponse -> BrokerMsg
|
||||
-- What the router knows about the names an address owns.
|
||||
ROWND :: OwnedNames -> BrokerMsg
|
||||
deriving (Eq, Show)
|
||||
|
||||
data RcvMessage = RcvMessage
|
||||
@@ -771,16 +764,16 @@ newtype EncFwdResponse = EncFwdResponse ByteString
|
||||
deriving (Eq, Show)
|
||||
|
||||
data FwdResponse = FwdResponse
|
||||
{ fwdCorrId :: C.CorrCbNonce,
|
||||
{ fwdCorrId :: CorrId,
|
||||
fwdResponse :: EncResponse
|
||||
}
|
||||
|
||||
instance Encoding FwdResponse where
|
||||
smpEncode FwdResponse {fwdCorrId, fwdResponse = EncResponse t} =
|
||||
smpEncode (fwdCorrId, Tail t)
|
||||
smpEncode FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} =
|
||||
smpEncode (corrId, Tail t)
|
||||
smpP = do
|
||||
(fwdCorrId, Tail t) <- smpP
|
||||
pure FwdResponse {fwdCorrId, fwdResponse = EncResponse t}
|
||||
(corrId, Tail t) <- smpP
|
||||
pure FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t}
|
||||
|
||||
newtype EncResponse = EncResponse ByteString
|
||||
deriving (Eq, Show)
|
||||
@@ -803,7 +796,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 $ unMsgId msgId, msgTs = systemToUTCTime msgTs, msgType}
|
||||
msgInfo msgId msgTs msgType = MsgInfo {msgId = decodeLatin1 $ B64.encode msgId, msgTs = systemToUTCTime msgTs, msgType}
|
||||
|
||||
messageId :: Message -> MsgId
|
||||
messageId = \case
|
||||
@@ -970,6 +963,7 @@ data CommandTag (p :: Party) where
|
||||
NSUB_ :: CommandTag Notifier
|
||||
NSUBS_ :: CommandTag NotifierService
|
||||
RSLV_ :: CommandTag Resolver
|
||||
ROWN_ :: CommandTag Resolver
|
||||
|
||||
data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p)
|
||||
|
||||
@@ -997,6 +991,7 @@ data BrokerMsgTag
|
||||
| ERR_
|
||||
| PONG_
|
||||
| RNAME_
|
||||
| ROWND_
|
||||
deriving (Show)
|
||||
|
||||
class ProtocolMsgTag t where
|
||||
@@ -1034,6 +1029,7 @@ instance PartyI p => Encoding (CommandTag p) where
|
||||
NSUB_ -> "NSUB"
|
||||
NSUBS_ -> "NSUBS"
|
||||
RSLV_ -> "RSLV"
|
||||
ROWN_ -> "ROWN"
|
||||
smpP = messageTagP
|
||||
|
||||
instance ProtocolMsgTag CmdTag where
|
||||
@@ -1063,6 +1059,7 @@ instance ProtocolMsgTag CmdTag where
|
||||
"NSUB" -> Just $ CT SNotifier NSUB_
|
||||
"NSUBS" -> Just $ CT SNotifierService NSUBS_
|
||||
"RSLV" -> Just $ CT SResolver RSLV_
|
||||
"ROWN" -> Just $ CT SResolver ROWN_
|
||||
_ -> Nothing
|
||||
|
||||
instance Encoding CmdTag where
|
||||
@@ -1093,6 +1090,7 @@ instance Encoding BrokerMsgTag where
|
||||
ERR_ -> "ERR"
|
||||
PONG_ -> "PONG"
|
||||
RNAME_ -> "RNAME"
|
||||
ROWND_ -> "ROWND"
|
||||
smpP = messageTagP
|
||||
|
||||
instance ProtocolMsgTag BrokerMsgTag where
|
||||
@@ -1116,6 +1114,7 @@ instance ProtocolMsgTag BrokerMsgTag where
|
||||
"ERR" -> Just ERR_
|
||||
"PONG" -> Just PONG_
|
||||
"RNAME" -> Just RNAME_
|
||||
"ROWND" -> Just ROWND_
|
||||
_ -> Nothing
|
||||
|
||||
-- | SMP message body format
|
||||
@@ -1335,10 +1334,7 @@ instance ProtocolTypeI p => FromJSON (ProtocolServer p) where
|
||||
parseJSON = strParseJSON "ProtocolServer"
|
||||
|
||||
newtype BasicAuth = BasicAuth {unBasicAuth :: ByteString}
|
||||
deriving (Ord, Show)
|
||||
|
||||
instance Eq BasicAuth where
|
||||
BasicAuth s == BasicAuth s' = BA.constEq s s'
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
instance IsString BasicAuth where fromString = BasicAuth . B.pack
|
||||
|
||||
@@ -1504,34 +1500,7 @@ type RcvNtfPublicDhKey = C.PublicKeyX25519
|
||||
type RcvNtfDhSecret = C.DhSecretX25519
|
||||
|
||||
-- | SMP message server ID.
|
||||
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)
|
||||
type MsgId = ByteString
|
||||
|
||||
-- | SMP message body.
|
||||
type MsgBody = ByteString
|
||||
@@ -1856,7 +1825,8 @@ 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)
|
||||
| otherwise -> new <> e (subMode, queueReqData)
|
||||
| v >= shortLinksSMPVersion -> new <> e (subMode, queueReqData)
|
||||
| otherwise -> new <> e (subMode, senderCanSecure (queueReqMode <$> queueReqData))
|
||||
where
|
||||
new = e (NEW_, ' ', rKey, dhKey, auth_)
|
||||
SUB -> e SUB_
|
||||
@@ -1887,6 +1857,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
|
||||
PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s)
|
||||
RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s)
|
||||
RSLV q -> e (RSLV_, ' ', if v >= nameAvailSMPVersion then hashedQuery q else q)
|
||||
ROWN addr offset -> e (ROWN_, ' ', addr, offset)
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
@@ -1912,6 +1883,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
|
||||
PFWD {} -> entityCmd
|
||||
RFWD _ -> noAuthCmd
|
||||
RSLV _ -> noAuthCmd
|
||||
ROWN _ _ -> noAuthCmd
|
||||
SUB -> serviceCmd
|
||||
NSUB -> serviceCmd
|
||||
-- other client commands must have both signature and queue ID
|
||||
@@ -1943,18 +1915,20 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
|
||||
CT SCreator NEW_ -> Cmd SCreator <$> newCmd
|
||||
where
|
||||
newCmd
|
||||
| v >= newNtfCredsSMPVersion = new smpP
|
||||
| otherwise = new nothing
|
||||
| v >= newNtfCredsSMPVersion = new smpP smpP
|
||||
| v >= shortLinksSMPVersion = new smpP nothing
|
||||
| otherwise = new (qReq <$> smpP) nothing
|
||||
where
|
||||
nothing = pure Nothing
|
||||
new p3 = NEW <$> do
|
||||
new p2 p3 = NEW <$> do
|
||||
rcvAuthKey <- _smpP
|
||||
rcvDhKey <- smpP
|
||||
auth_ <- smpP
|
||||
subMode <- smpP
|
||||
queueReqData <- smpP
|
||||
queueReqData <- p2
|
||||
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
|
||||
@@ -1992,6 +1966,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
|
||||
| v >= rcvServiceSMPVersion -> Cmd SNotifierService <$> (NSUBS <$> _smpP <*> smpP)
|
||||
| otherwise -> pure $ Cmd SNotifierService $ NSUBS (-1) mempty
|
||||
CT SResolver RSLV_ -> Cmd SResolver . RSLV <$> _smpP
|
||||
CT SResolver ROWN_ -> Cmd SResolver <$> (ROWN <$> _smpP <*> smpP)
|
||||
|
||||
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
||||
{-# INLINE fromProtocolError #-}
|
||||
@@ -2005,7 +1980,8 @@ 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)
|
||||
| otherwise -> ids <> e (queueMode, linkId)
|
||||
| v >= shortLinksSMPVersion -> ids <> e (queueMode, linkId)
|
||||
| otherwise -> ids <> e (senderCanSecure queueMode)
|
||||
where
|
||||
ids = e (IDS_, ' ', rcvId, sndId, srvDh)
|
||||
LNK sId d -> e (LNK_, ' ', sId, d)
|
||||
@@ -2019,8 +1995,8 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh)
|
||||
NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta)
|
||||
PKEY sid vr certKey -> e (PKEY_, ' ', sid, vr, certKey)
|
||||
RRES nonce_ (EncFwdResponse encBlock) -> fwdResp RRES_ nonce_ encBlock
|
||||
PRES nonce_ (EncResponse encBlock) -> fwdResp PRES_ nonce_ encBlock
|
||||
RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock)
|
||||
PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock)
|
||||
END -> e END_
|
||||
ENDS n idsHash -> serviceResp ENDS_ n idsHash
|
||||
DELD -> e DELD_
|
||||
@@ -2033,6 +2009,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
| v < clientNoticesSMPVersion -> BLOCKED info {notice = Nothing}
|
||||
_ -> err
|
||||
PONG -> e PONG_
|
||||
ROWND owned -> e (ROWND_, ' ', Tail $ LB.toStrict $ J.encode owned)
|
||||
RNAME res
|
||||
| v >= nameAvailSMPVersion -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode res)
|
||||
| otherwise -> case registration res of
|
||||
@@ -2044,9 +2021,6 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
serviceResp tag n idsHash
|
||||
| v >= rcvServiceSMPVersion = e (tag, ' ', n, idsHash)
|
||||
| otherwise = e (tag, ' ', n)
|
||||
fwdResp tag nonce_ encBlock
|
||||
| v >= fwdNoncesSMPVersion = e (tag, ' ', nonce_, Tail encBlock)
|
||||
| otherwise = e (tag, ' ', Tail encBlock)
|
||||
|
||||
protocolP v = \case
|
||||
MSG_ -> do
|
||||
@@ -2056,17 +2030,19 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
bodyP = EncRcvMsgBody . unTail <$> smpP
|
||||
ALLS_ -> pure ALLS
|
||||
IDS_
|
||||
| v >= newNtfCredsSMPVersion -> ids smpP smpP
|
||||
| v >= serviceCertsSMPVersion -> ids smpP nothing
|
||||
| otherwise -> ids nothing nothing
|
||||
| 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
|
||||
where
|
||||
qm sndSecure = Just $ if sndSecure then QMMessaging else QMContact
|
||||
nothing = pure Nothing
|
||||
ids p3 p4 = do
|
||||
ids p1 p2 p3 p4 = do
|
||||
rcvId <- _smpP
|
||||
sndId <- smpP
|
||||
rcvPublicDhKey <- smpP
|
||||
queueMode <- smpP
|
||||
linkId <- smpP
|
||||
queueMode <- p1
|
||||
linkId <- p2
|
||||
serviceId <- p3
|
||||
serverNtfCreds <- p4
|
||||
pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId, serviceId, serverNtfCreds}
|
||||
@@ -2076,8 +2052,8 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
NID_ -> NID <$> _smpP <*> smpP
|
||||
NMSG_ -> NMSG <$> _smpP <*> smpP
|
||||
PKEY_ -> PKEY <$> _smpP <*> smpP <*> smpP
|
||||
RRES_ -> fwdRespP RRES EncFwdResponse
|
||||
PRES_ -> fwdRespP PRES EncResponse
|
||||
RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP)
|
||||
PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP)
|
||||
END_ -> pure END
|
||||
ENDS_ -> serviceRespP ENDS
|
||||
DELD_ -> pure DELD
|
||||
@@ -2085,6 +2061,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
OK_ -> pure OK
|
||||
ERR_ -> ERR <$> _smpP
|
||||
PONG_ -> pure PONG
|
||||
ROWND_ -> fmap ROWND . J.eitherDecodeStrict . unTail <$?> _smpP
|
||||
RNAME_
|
||||
| v >= nameAvailSMPVersion -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP
|
||||
| otherwise -> fmap (RNAME . oldResponse) . J.eitherDecodeStrict . unTail <$?> _smpP
|
||||
@@ -2094,9 +2071,6 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
serviceRespP resp
|
||||
| v >= rcvServiceSMPVersion = resp <$> _smpP <*> smpP
|
||||
| otherwise = resp <$> _smpP <*> pure mempty
|
||||
fwdRespP :: (Maybe C.CbNonce -> a -> BrokerMsg) -> (ByteString -> a) -> Parser BrokerMsg
|
||||
fwdRespP resp enc = resp <$> (A.space *> nonceP) <*> (enc <$> A.takeByteString)
|
||||
nonceP = if v >= fwdNoncesSMPVersion then smpP else pure Nothing
|
||||
|
||||
fromProtocolError = \case
|
||||
PECmdSyntax -> CMD SYNTAX
|
||||
@@ -2113,9 +2087,10 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
-- PONG response must not have queue ID
|
||||
PONG -> noEntityMsg
|
||||
PKEY {} -> noEntityMsg
|
||||
RRES {} -> noEntityMsg
|
||||
RRES _ -> noEntityMsg
|
||||
ALLS -> noEntityMsg
|
||||
RNAME {} -> noEntityMsg
|
||||
ROWND {} -> noEntityMsg
|
||||
-- other broker responses must have queue ID
|
||||
_
|
||||
| B.null entId -> Left $ CMD NO_ENTITY
|
||||
@@ -2449,9 +2424,7 @@ tDecodeServer THandleParams {sessionId, thVersion = v, implySessId} = \case
|
||||
where
|
||||
cmdOrErr = parseProtocol @v @err @cmd v command >>= checkCredentials tAuth entityId
|
||||
t :: a -> (CorrId, EntityId, a)
|
||||
-- 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,)
|
||||
t = (corrId,entityId,)
|
||||
Left _ -> tError corrId PEBlock
|
||||
| otherwise -> tError corrId PESession
|
||||
Left _ -> tError "" PEBlock
|
||||
|
||||
@@ -88,6 +88,7 @@ import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (cast)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import GHC.Conc.Signal
|
||||
@@ -103,13 +104,14 @@ import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClient
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Eth.Address (Address)
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.Control
|
||||
import Simplex.Messaging.Server.Env.STM as Env
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.MsgStore
|
||||
import Simplex.Messaging.Server.MsgStore.Journal (JournalMsgStore, JournalQueue (..), getJournalQueueMessages)
|
||||
import Simplex.Messaging.Server.Names (NamesEnv, closeNamesEnv, resolveName)
|
||||
import Simplex.Messaging.Server.Names (NamesEnv, closeNamesEnv, ownedNames, resolveName)
|
||||
import Simplex.Messaging.Server.MsgStore.STM
|
||||
import Simplex.Messaging.Server.MsgStore.Types
|
||||
import Simplex.Messaging.Server.NtfStore
|
||||
@@ -1092,7 +1094,6 @@ controlPortAuth h user admin role auth = do
|
||||
readTVarIO role >>= \case
|
||||
CPRNone -> do
|
||||
atomically $ writeTVar role $! newRole
|
||||
when (newRole == CPRNone) $ logWarn "ControlPort: failed auth"
|
||||
hPutStrLn h $ currentRole newRole
|
||||
r -> hPutStrLn h $ currentRole r <> if r == newRole then "" else ", start new session to change."
|
||||
where
|
||||
@@ -1277,6 +1278,7 @@ verifyQueueTransmission service thAuth (tAuth, authorized, (corrId, entId, comma
|
||||
vc SProxiedClient _ = VRVerified Nothing
|
||||
vc SProxyService (RFWD _) = VRVerified Nothing
|
||||
vc SResolver (RSLV _) = VRVerified Nothing
|
||||
vc SResolver (ROWN _ _) = VRVerified Nothing
|
||||
checkRole = case (service, partyClientRole p) of
|
||||
(Just THClientService {serviceRole}, Just role) -> serviceRole == role
|
||||
_ -> True
|
||||
@@ -1319,6 +1321,11 @@ isContactQueue QueueRec {queueMode, senderKey} = case queueMode of
|
||||
Just QMContact -> True
|
||||
Nothing -> isNothing senderKey -- for backward compatibility with pre-SKEY contact addresses
|
||||
|
||||
isSecuredMsgQueue :: QueueRec -> Bool
|
||||
isSecuredMsgQueue QueueRec {queueMode, senderKey} = case queueMode of
|
||||
Just QMContact -> False
|
||||
_ -> isJust senderKey
|
||||
|
||||
-- Random correlation ID is used as a nonce in case crypto_box authenticator is used to authorize transmission
|
||||
verifyCmdAuthorization :: Maybe (THandleAuth 'TServer) -> Maybe TAuthorizations -> ByteString -> CorrId -> C.APublicAuthKey -> Bool
|
||||
verifyCmdAuthorization thAuth tAuth authorized corrId key = maybe False (verify key) tAuth
|
||||
@@ -1334,7 +1341,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} -> either (const False) (\nonce -> C.cbVerify k pk nonce authenticator authorized) $ C.cbNonce corrId
|
||||
Just THAuthServer {serverPrivKey = pk} -> C.cbVerify k pk (C.cbNonce corrId) authenticator authorized
|
||||
Nothing -> False
|
||||
|
||||
dummyVerifyCmd :: Maybe (THandleAuth 'TServer) -> Maybe TAuthorizations -> ByteString -> CorrId -> Bool
|
||||
@@ -1453,15 +1460,14 @@ client
|
||||
ServerStats {pMsgFwds, pMsgFwdsOwn} <- asks serverStats
|
||||
let inc = mkIncProxyStats pMsgFwds pMsgFwdsOwn
|
||||
liftIO (lookupSMPServerClient a sessId) >>= \case
|
||||
Just (own, smp) | Right fwdCorrId <- C.corrCbNonce (bs corrId) -> do
|
||||
Just (own, smp) -> do
|
||||
inc own pRequests
|
||||
forkProxiedCmd $ do
|
||||
liftIO (runExceptT (forwardSMPTransmission smp fwdCorrId fwdV pubKey encBlock) `E.catches` clientHandlers) >>= \case
|
||||
Right (nonce_, r) -> PRES nonce_ r <$ inc own pSuccesses
|
||||
liftIO (runExceptT (forwardSMPTransmission smp corrId fwdV pubKey encBlock) `E.catches` clientHandlers) >>= \case
|
||||
Right r -> PRES 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)
|
||||
@@ -1503,6 +1509,18 @@ client
|
||||
answered = \case
|
||||
NRRegistered {} -> True
|
||||
_ -> v >= nameAvailSMPVersion
|
||||
resolverMsg :: VersionSMP -> NamesEnv -> Command Resolver -> M s BrokerMsg
|
||||
resolverMsg v nenv = \case
|
||||
RSLV d -> resolveNameMsg v nenv d
|
||||
ROWN addr offset -> ownedNamesMsg nenv addr offset
|
||||
ownedNamesMsg :: NamesEnv -> Address -> Word32 -> M s BrokerMsg
|
||||
ownedNamesMsg nenv addr offset = do
|
||||
st <- asks (rslvStats . serverStats)
|
||||
(selector, msg) <-
|
||||
liftIO (ownedNames nenv addr offset) <&> \case
|
||||
Right owned -> (rslvSucc, ROWND owned)
|
||||
Left e -> (rslvResolverErrs, ERR $ NAME e)
|
||||
incStat (selector st) $> msg
|
||||
transportErr :: TransportError -> ErrorType
|
||||
transportErr = PROXY . BROKER . TRANSPORT
|
||||
mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> IORef Int) -> m ()
|
||||
@@ -1517,9 +1535,9 @@ client
|
||||
SEND flags msgBody -> response <$> withQueue_ False err (sendMessage flags msgBody)
|
||||
Cmd SIdleClient PING -> pure $ response (corrId, NoEntity, PONG)
|
||||
Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock
|
||||
Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case
|
||||
Cmd SResolver command -> rslvNamesEnv >>= \case
|
||||
Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER))
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolveNameMsg (thVersion thParams') nenv d)
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolverMsg (thVersion thParams') nenv command)
|
||||
Cmd SSenderLink command -> case command of
|
||||
LKEY k -> withQueue $ \q qr -> checkMode QMMessaging qr $ secureQueue_ q k $>> getQueueLink_ q qr
|
||||
LGET -> withQueue $ \q qr -> checkContact qr $ getQueueLink_ q qr
|
||||
@@ -1951,7 +1969,7 @@ client
|
||||
getDelivered :: Sub -> STM (Maybe (ServerSub, SystemSeconds))
|
||||
getDelivered Sub {delivered, subThread} = do
|
||||
readTVar delivered $>>= \(msgId', ts) ->
|
||||
if msgId == msgId'
|
||||
if msgId == msgId' || B.null msgId
|
||||
then writeTVar delivered Nothing $> Just (subThread, ts)
|
||||
else pure Nothing
|
||||
updateStats :: ServerStats -> Bool -> SystemSeconds -> Message -> IO ()
|
||||
@@ -1997,7 +2015,7 @@ client
|
||||
when (isJust (queueData qr) && isSecuredMsgQueue qr) $ void $ liftIO $
|
||||
deleteQueueLinkData (queueStore ms) q
|
||||
ServerConfig {messageExpiration, expireMessagesOnSend, msgIdBytes} <- asks config
|
||||
msgId <- atomically . randomMsgId msgIdBytes =<< asks random
|
||||
msgId <- randomId' msgIdBytes
|
||||
msg_ <- liftIO $ runExceptT $ do
|
||||
when expireMessagesOnSend $ mapM_ (expireMessages stats) messageExpiration
|
||||
msg <- liftIO $ mkMessage msgId body
|
||||
@@ -2110,7 +2128,7 @@ client
|
||||
liftIO $ storeNtf ns nId ntf
|
||||
incStat . ntfCount =<< asks serverStats
|
||||
|
||||
mkMessageNotification :: MsgId -> SystemTime -> RcvNtfDhSecret -> M s MsgNtf
|
||||
mkMessageNotification :: ByteString -> SystemTime -> RcvNtfDhSecret -> M s MsgNtf
|
||||
mkMessageNotification msgId msgTs rcvNtfDhSecret = do
|
||||
ntfNonce <- atomically . C.randomCbNonce =<< asks random
|
||||
let msgMeta = NMsgMeta {msgId, msgTs}
|
||||
@@ -2121,13 +2139,12 @@ client
|
||||
processForwardedCommand (EncFwdTransmission s) = fmap (either (Just . ERR) id) . runExceptT $ do
|
||||
THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE $ transportErr TENoServerAuth) pure (thAuth thParams')
|
||||
sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret'
|
||||
proxyNonce <- liftEitherWith (const CRYPTO) $ C.cbNonce $ bs corrId
|
||||
let proxyNonce = 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 = fwdCorrId
|
||||
b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret (encTransmissionNonce fwdVersion clientNonce) et
|
||||
clientNonce = C.cbNonce $ bs fwdCorrId
|
||||
b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et
|
||||
let clntTHParams = smpTHParamsSetVersion fwdVersion thParams'
|
||||
-- only allowing single forwarded transactions
|
||||
t' <- case tParse clntTHParams b of
|
||||
@@ -2140,22 +2157,18 @@ client
|
||||
TBError _ _ : _ -> throwE BLOCK
|
||||
TBTransmission b' _ : _ -> pure b'
|
||||
TBTransmissions b' _ _ : _ -> pure b'
|
||||
nonce_ <-
|
||||
if fwdVersion >= fwdNoncesSMPVersion
|
||||
then Just <$> (atomically . C.randomCbNonce =<< asks random)
|
||||
else pure Nothing
|
||||
r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (fromMaybe (C.reverseNonce clientNonce) nonce_) r' paddedProxiedTLength
|
||||
r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedTLength
|
||||
let fr = FwdResponse {fwdCorrId, fwdResponse = r2}
|
||||
pure $ RRES nonce_ $ EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr)
|
||||
pure $ RRES $ EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr)
|
||||
-- the inner response, or Nothing if forked (RSLV).
|
||||
r_ <- lift (rejectOrVerify clntThAuth t') >>= \case
|
||||
-- rejectOrVerify filters allowed commands, no need to repeat it here.
|
||||
Left r -> pure $ Just r
|
||||
Right t''@(_, (corrId', entId', cmd')) -> case cmd' of
|
||||
Cmd SResolver (RSLV d) -> lift $ rslvNamesEnv >>= \case
|
||||
Cmd SResolver command -> lift $ rslvNamesEnv >>= \case
|
||||
Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER))
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity $ do
|
||||
msg <- resolveNameMsg (thVersion clntTHParams) nenv d
|
||||
msg <- resolverMsg (thVersion clntTHParams) nenv command
|
||||
either ERR id <$> runExceptT (encodeResp (corrId', entId', msg))
|
||||
-- INTERNAL because processCommand never returns Nothing for sender commands;
|
||||
-- `fst` drops the empty message only returned for SUB.
|
||||
@@ -2177,6 +2190,7 @@ client
|
||||
Cmd SSenderLink (LKEY _) -> True
|
||||
Cmd SSenderLink LGET -> True
|
||||
Cmd SResolver (RSLV _) -> True
|
||||
Cmd SResolver (ROWN _ _) -> True
|
||||
_ -> False
|
||||
verified = \case
|
||||
VRVerified q -> Right (q, t'')
|
||||
@@ -2188,7 +2202,7 @@ client
|
||||
MessageQuota {} -> RcvMsgQuota msgTs'
|
||||
where
|
||||
encrypt :: KnownNat i => C.MaxLenBS i -> RcvMessage
|
||||
encrypt body = RcvMessage msgId' . EncRcvMsgBody $ C.cbEncryptMaxLenBS (rcvDhSecret qr) msgId' body
|
||||
encrypt body = RcvMessage msgId' . EncRcvMsgBody $ C.cbEncryptMaxLenBS (rcvDhSecret qr) (C.cbNonce msgId') body
|
||||
msgId' = messageId msg
|
||||
msgTs' = messageTs msg
|
||||
|
||||
@@ -2237,7 +2251,7 @@ client
|
||||
SubPending -> QSubPending
|
||||
SubThread _ -> QSubThread
|
||||
ProhibitSub -> pure QProhibitSub
|
||||
qDelivered <- decodeLatin1 . encode . unMsgId . fst <$$> readTVarIO delivered
|
||||
qDelivered <- decodeLatin1 . encode . fst <$$> readTVarIO delivered
|
||||
pure QSub {qSubThread, qDelivered}
|
||||
|
||||
ok :: Transmission BrokerMsg
|
||||
|
||||
@@ -571,10 +571,7 @@ 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, msgIdBytes} = do
|
||||
when (msgIdBytes < 24) $ do
|
||||
logError "Error: msgIdBytes must be at least 24"
|
||||
exitFailure
|
||||
newEnv config@ServerConfig {smpCredentials, httpCredentials, serverStoreCfg, smpAgentCfg, information, messageExpiration, idleQueueInterval, msgQueueQuota, maxJournalMsgCount, maxJournalStateLines, namesConfig} = do
|
||||
serverActive <- newTVarIO True
|
||||
server <- newServer
|
||||
msgStore_ <- case serverStoreCfg of
|
||||
|
||||
@@ -114,9 +114,7 @@ instance MsgStoreClass PostgresMsgStore where
|
||||
where
|
||||
st = dbStore $ queueStore_ ms
|
||||
oldMsg = now - ttl
|
||||
-- expired messages read per page in expire_old_messages, and the page is one
|
||||
-- transaction: queues in it stay row-locked against SEND and ACK until it commits.
|
||||
batchSize = 100 :: Int
|
||||
batchSize = 10000 :: Int
|
||||
toMessageStats (expiredMsgsCount, storedMsgsCount, storedQueues) =
|
||||
MessageStats {expiredMsgsCount, storedMsgsCount, storedQueues}
|
||||
|
||||
@@ -202,7 +200,7 @@ instance MsgStoreClass PostgresMsgStore where
|
||||
DB.query
|
||||
db
|
||||
"SELECT quota_written, was_empty FROM write_message(?,?,?,?,?,?,?)"
|
||||
(recipientId' q, messageId msg, systemSeconds (messageTs msg), msgQuota, ntf, Binary body, quota)
|
||||
(recipientId' q, Binary (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 +269,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, 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, Binary 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, 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, Binary msgId)
|
||||
where
|
||||
toResult = \case
|
||||
[] -> (Nothing, Nothing)
|
||||
@@ -297,13 +295,13 @@ uninterruptibleMask_ :: ExceptT ErrorType IO a -> ExceptT ErrorType IO a
|
||||
uninterruptibleMask_ = ExceptT . E.uninterruptibleMask_ . runExceptT
|
||||
{-# INLINE uninterruptibleMask_ #-}
|
||||
|
||||
toMaybeMessage :: (Maybe MsgId, Maybe Int64, Maybe Bool, Maybe Bool, Maybe (Binary MsgBody)) -> Maybe Message
|
||||
toMaybeMessage :: (Maybe (Binary 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 :: (MsgId, Int64, Bool, Bool, Binary MsgBody) -> Message
|
||||
toMessage (msgId, ts, msgQuota, ntf, Binary body)
|
||||
toMessage :: (Binary MsgId, Int64, Bool, Bool, Binary MsgBody) -> Message
|
||||
toMessage (Binary 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
|
||||
@@ -365,8 +363,8 @@ deleteAllMessages ms =
|
||||
db
|
||||
[sql|
|
||||
UPDATE msg_queues
|
||||
SET msg_queue_size = 0, msg_can_write = TRUE
|
||||
WHERE msg_queue_size != 0 OR msg_can_write = FALSE
|
||||
SET msg_queue_size = 0, msg_can_write = TRUE, msg_queue_expire = FALSE
|
||||
WHERE msg_queue_size != 0 OR msg_can_write = FALSE OR msg_queue_expire = TRUE
|
||||
|]
|
||||
|
||||
updateQueueCounts :: PostgresMsgStore -> IO ()
|
||||
@@ -386,15 +384,16 @@ updateQueueCounts ms =
|
||||
db
|
||||
[sql|
|
||||
UPDATE msg_queues
|
||||
SET msg_queue_size = 0, msg_can_write = TRUE
|
||||
WHERE msg_queue_size != 0 OR msg_can_write = FALSE
|
||||
SET msg_queue_size = 0, msg_can_write = TRUE, msg_queue_expire = FALSE
|
||||
WHERE msg_queue_size != 0 OR msg_can_write = FALSE OR msg_queue_expire = TRUE
|
||||
|]
|
||||
void $ DB.execute_
|
||||
db
|
||||
[sql|
|
||||
UPDATE msg_queues q
|
||||
SET msg_queue_size = s.size,
|
||||
msg_can_write = s.quota_count = 0
|
||||
msg_can_write = s.quota_count = 0,
|
||||
msg_queue_expire = s.size > s.quota_count
|
||||
FROM queue_stats s
|
||||
WHERE q.recipient_id = s.recipient_id
|
||||
|]
|
||||
@@ -429,7 +428,7 @@ messageRecToText rId msg =
|
||||
tabFields = BB.char7 ',' `intersperse` fields
|
||||
fields =
|
||||
[ renderField (toField rId),
|
||||
renderField (toField $ messageId msg),
|
||||
renderField (toField $ Binary (messageId msg)),
|
||||
renderField (toField $ systemSeconds (messageTs msg)),
|
||||
renderField (toField msgQuota),
|
||||
renderField (toField ntf),
|
||||
|
||||
@@ -12,6 +12,7 @@ module Simplex.Messaging.Server.Names
|
||||
closeNamesEnv,
|
||||
pingEndpoint,
|
||||
resolveName,
|
||||
ownedNames,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -21,7 +22,11 @@ import Data.Bifunctor (first)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Eth.Address (Address)
|
||||
import Simplex.Messaging.Names.Record (OwnedNames)
|
||||
import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameResponse)
|
||||
import Simplex.Messaging.Server.Names.HttpResolver
|
||||
( ResolverEnv,
|
||||
@@ -30,6 +35,7 @@ import Simplex.Messaging.Server.Names.HttpResolver
|
||||
closeResolverEnv,
|
||||
healthHttp,
|
||||
newResolverEnv,
|
||||
ownedByHttp,
|
||||
resolveHttp,
|
||||
)
|
||||
import System.Timeout (timeout)
|
||||
@@ -60,9 +66,12 @@ pingEndpoint NamesEnv {resolverEnv, config} =
|
||||
fromMaybe (Left ResolverTimeout) <$> timeout (resolverTimeoutMs config * 1000) (healthHttp resolverEnv)
|
||||
|
||||
resolveName :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse)
|
||||
resolveName env q = do
|
||||
r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetch env q))
|
||||
case r of
|
||||
resolveName env q = resolverCall env $ fetch env q
|
||||
|
||||
-- | Bound the call by the configured timeout, and report a raised exception as an error, leaving async exceptions alone.
|
||||
resolverCall :: NamesEnv -> IO (Either NameErrorType a) -> IO (Either NameErrorType a)
|
||||
resolverCall env a =
|
||||
E.try (timeout (resolverTimeoutMs (config env) * 1000) a) >>= \case
|
||||
Right result -> pure (fromMaybe (Left (RESOLVER "timeout")) result)
|
||||
Left e
|
||||
| Just (_ :: E.SomeAsyncException) <- E.fromException e -> E.throwIO e
|
||||
@@ -74,6 +83,14 @@ fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse)
|
||||
fetch NamesEnv {resolverEnv} q =
|
||||
first mapResolverError <$> resolveHttp resolverEnv (decodeLatin1 $ smpEncode q)
|
||||
|
||||
-- | The names an address owns. A resolver without the endpoint answers 404, which is a RESOLVER error, not an empty list.
|
||||
ownedNames :: NamesEnv -> Address -> Word32 -> IO (Either NameErrorType OwnedNames)
|
||||
ownedNames env addr offset = resolverCall env $ fetchOwned env addr offset
|
||||
|
||||
fetchOwned :: NamesEnv -> Address -> Word32 -> IO (Either NameErrorType OwnedNames)
|
||||
fetchOwned NamesEnv {resolverEnv} addr offset =
|
||||
first mapResolverError <$> ownedByHttp resolverEnv (decodeLatin1 $ strEncode addr) offset
|
||||
|
||||
mapResolverError :: ResolverError -> NameErrorType
|
||||
mapResolverError = \case
|
||||
HttpStatusErr code -> RESOLVER ("HTTP " <> T.pack (show code))
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
-- GET /v2/resolve/<query> -> 200 with a NameRegistration JSON document, for
|
||||
-- all three registration shapes; 400 for unknown
|
||||
-- TLDs, 502 for upstream RPC failures
|
||||
-- GET /v2/owned-by/<addr> -> 200 with an OwnedNames JSON document
|
||||
-- GET /v1/resolve/<name> -> 200 with a NameRecord, what relays before SMP
|
||||
-- v22 call as /resolve
|
||||
-- GET /health -> 200 when the resolver process is ready
|
||||
@@ -29,6 +30,7 @@ module Simplex.Messaging.Server.Names.HttpResolver
|
||||
newResolverEnv,
|
||||
closeResolverEnv,
|
||||
resolveHttp,
|
||||
ownedByHttp,
|
||||
healthHttp,
|
||||
)
|
||||
where
|
||||
@@ -42,6 +44,7 @@ import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word32)
|
||||
import Network.HTTP.Client
|
||||
( HttpException,
|
||||
Manager,
|
||||
@@ -59,7 +62,7 @@ import qualified Network.HTTP.Client as HC
|
||||
import Network.HTTP.Client.TLS (tlsManagerSettings)
|
||||
import qualified Network.HTTP.Types as HT
|
||||
import Network.HTTP.Types.URI (urlEncode)
|
||||
import Simplex.Messaging.Names.Record (NameResponse)
|
||||
import Simplex.Messaging.Names.Record (NameResponse, OwnedNames)
|
||||
|
||||
data RpcAuth = AuthBearer Text | AuthBasic Text Text
|
||||
|
||||
@@ -117,6 +120,12 @@ resolveHttp env q =
|
||||
(>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict)
|
||||
<$> httpGet env ("/v2/resolve/" <> B.unpack (urlEncode True (encodeUtf8 q)))
|
||||
|
||||
-- | The address is encoded for the same reason the name is: nothing from a client reaches the path raw.
|
||||
ownedByHttp :: ResolverEnv -> Text -> Word32 -> IO (Either ResolverError OwnedNames)
|
||||
ownedByHttp env addr offset =
|
||||
(>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict)
|
||||
<$> httpGet env ("/v2/owned-by/" <> B.unpack (urlEncode True (encodeUtf8 addr)) <> "?offset=" <> show offset)
|
||||
|
||||
-- | GET <baseUrl>/health; success = reachable with status < 400. The body is
|
||||
-- size-capped but NOT decoded — the probe only checks reachability.
|
||||
healthHttp :: ResolverEnv -> IO (Either ResolverError ())
|
||||
|
||||
@@ -465,11 +465,11 @@ prometheusMetrics sm rtm ts =
|
||||
in "# Names\n\
|
||||
\# -----\n\
|
||||
\\n\
|
||||
\# HELP simplex_smp_names_reqs Total RSLV requests forwarded to this server.\n\
|
||||
\# HELP simplex_smp_names_reqs Total resolver requests (RSLV and ROWN) forwarded to this server.\n\
|
||||
\# TYPE simplex_smp_names_reqs counter\n\
|
||||
\simplex_smp_names_reqs " <> mshow _rslvReqs <> "\n# rslvReqs\n\
|
||||
\\n\
|
||||
\# HELP simplex_smp_names_success NameRecord resolved, or availability answered.\n\
|
||||
\# HELP simplex_smp_names_success NameRecord resolved, availability answered, or names owned listed.\n\
|
||||
\# TYPE simplex_smp_names_success counter\n\
|
||||
\simplex_smp_names_success " <> mshow _rslvSucc <> "\n# rslvSucc\n\
|
||||
\\n\
|
||||
@@ -481,7 +481,7 @@ prometheusMetrics sm rtm ts =
|
||||
\# TYPE simplex_smp_names_resolver_errs counter\n\
|
||||
\simplex_smp_names_resolver_errs " <> mshow _rslvResolverErrs <> "\n# rslvResolverErrs\n\
|
||||
\\n\
|
||||
\# HELP simplex_smp_names_disabled RSLV requests rejected because no resolver is configured (names role off).\n\
|
||||
\# HELP simplex_smp_names_disabled Resolver requests rejected because no resolver is configured (names role off).\n\
|
||||
\# TYPE simplex_smp_names_disabled counter\n\
|
||||
\simplex_smp_names_disabled " <> mshow _rslvDisabled <> "\n# rslvDisabled\n\
|
||||
\\n"
|
||||
|
||||
@@ -13,14 +13,12 @@ module Simplex.Messaging.Server.QueueStore
|
||||
ServiceRec (..),
|
||||
CertFingerprint,
|
||||
ServerEntityStatus (..),
|
||||
isSecuredMsgQueue,
|
||||
) where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Maybe (isJust)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import Simplex.Messaging.Encoding
|
||||
@@ -50,11 +48,6 @@ data QueueRec = QueueRec
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
isSecuredMsgQueue :: QueueRec -> Bool
|
||||
isSecuredMsgQueue QueueRec {queueMode, senderKey} = case queueMode of
|
||||
Just QMContact -> False
|
||||
_ -> isJust senderKey
|
||||
|
||||
data NtfCreds = NtfCreds
|
||||
{ notifierId :: NotifierId,
|
||||
notifierKey :: NtfPublicAuthKey,
|
||||
|
||||
@@ -314,9 +314,9 @@ instance StoreQueueClass q => QueueStoreClass q (PostgresQueueStore q) where
|
||||
addQueueLinkData st sq lnkId d =
|
||||
withQueueRec sq "addQueueLinkData" $ \q -> case queueData q of
|
||||
Nothing ->
|
||||
addLink q $ \db -> DB.execute db qry (d :. (lnkId, rId, QMContact))
|
||||
addLink q $ \db -> DB.execute db qry (d :. (lnkId, rId))
|
||||
Just (lnkId', _) | lnkId' == lnkId ->
|
||||
addLink q $ \db -> DB.execute db (qry <> " AND (fixed_data IS NULL OR fixed_data = ?)") (d :. (lnkId, rId, QMContact, fst d))
|
||||
addLink q $ \db -> DB.execute db (qry <> " AND (fixed_data IS NULL OR fixed_data = ?)") (d :. (lnkId, rId, fst d))
|
||||
_ -> throwE AUTH
|
||||
where
|
||||
rId = recipientId sq
|
||||
@@ -324,8 +324,7 @@ instance StoreQueueClass q => QueueStoreClass q (PostgresQueueStore q) where
|
||||
assertUpdated $ withDB' "addQueueLinkData" st update
|
||||
atomically $ writeTVar (queueRec sq) $ Just q {queueData = Just (lnkId, d)}
|
||||
withLog "addQueueLinkData" st $ \s -> logCreateLink s rId lnkId d
|
||||
-- the sender key condition is checked in SQL because without cache each command reads its own copy of the queue record
|
||||
qry = "UPDATE msg_queues SET fixed_data = ?, user_data = ?, link_id = ? WHERE recipient_id = ? AND deleted_at IS NULL AND (sender_key IS NULL OR queue_mode = ?)"
|
||||
qry = "UPDATE msg_queues SET fixed_data = ?, user_data = ?, link_id = ? WHERE recipient_id = ? AND deleted_at IS NULL"
|
||||
|
||||
deleteQueueLinkData :: PostgresQueueStore q -> q -> IO (Either ErrorType ())
|
||||
deleteQueueLinkData st sq =
|
||||
@@ -825,10 +824,6 @@ 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)
|
||||
|
||||
@@ -22,8 +22,7 @@ serverSchemaMigrations =
|
||||
("20250903_store_messages", m20250903_store_messages, Just down_m20250903_store_messages),
|
||||
("20250915_queue_ids_hash", m20250915_queue_ids_hash, Just down_m20250915_queue_ids_hash),
|
||||
("20260916_prometheus_indexes", m20260916_prometheus_indexes, Just down_m20260916_prometheus_indexes),
|
||||
("20260917_msg_queues_hot", m20260917_msg_queues_hot, Just down_m20260917_msg_queues_hot),
|
||||
("20260918_expire_messages", m20260918_expire_messages, Just down_m20260918_expire_messages)
|
||||
("20260917_msg_queues_hot", m20260917_msg_queues_hot, Just down_m20260917_msg_queues_hot)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -735,504 +734,3 @@ ALTER TABLE msg_queues RESET (fillfactor, autovacuum_vacuum_scale_factor, autova
|
||||
ALTER TABLE messages RESET (autovacuum_vacuum_scale_factor, autovacuum_analyze_scale_factor, toast.autovacuum_vacuum_scale_factor);
|
||||
ALTER TABLE services RESET (fillfactor, autovacuum_vacuum_threshold, autovacuum_vacuum_scale_factor);
|
||||
|]
|
||||
|
||||
m20260918_expire_messages :: Text
|
||||
m20260918_expire_messages =
|
||||
[r|
|
||||
CREATE INDEX idx_messages_expire ON messages (msg_ts, recipient_id) WHERE NOT msg_quota;
|
||||
|
||||
DROP INDEX idx_messages_recipient_id_msg_ts;
|
||||
|
||||
DROP INDEX idx_msg_queues_expire;
|
||||
|
||||
DROP PROCEDURE expire_old_messages(bigint, integer);
|
||||
|
||||
CREATE PROCEDURE expire_old_messages(IN p_old_ts bigint, IN batch_size integer, OUT r_expired_msgs_count bigint, OUT r_stored_msgs_count bigint, OUT r_stored_queues bigint)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
rids BYTEA[];
|
||||
rid BYTEA;
|
||||
last_ts BIGINT := -1;
|
||||
last_rid BYTEA := '\x';
|
||||
next_ts BIGINT;
|
||||
next_rid BYTEA;
|
||||
del_count BIGINT;
|
||||
total_deleted BIGINT := 0;
|
||||
BEGIN
|
||||
LOOP
|
||||
-- The page is scanned in (msg_ts, recipient_id) order, so its last row is the next
|
||||
-- keyset cursor. Advancing it past every row read, including queues left unexpired
|
||||
-- because delete_expired_msgs skipped a locked row or raised, is what terminates the
|
||||
-- loop; re-reading from the start instead would repeat those queues forever.
|
||||
SELECT array_agg(DISTINCT recipient_id),
|
||||
(array_agg(msg_ts ORDER BY msg_ts DESC, recipient_id DESC))[1],
|
||||
(array_agg(recipient_id ORDER BY msg_ts DESC, recipient_id DESC))[1]
|
||||
INTO rids, next_ts, next_rid
|
||||
FROM (
|
||||
SELECT msg_ts, recipient_id
|
||||
FROM messages
|
||||
WHERE NOT msg_quota
|
||||
AND msg_ts < p_old_ts
|
||||
AND (msg_ts, recipient_id) > (last_ts, last_rid)
|
||||
ORDER BY msg_ts ASC, recipient_id ASC
|
||||
LIMIT batch_size
|
||||
) m;
|
||||
|
||||
EXIT WHEN rids IS NULL;
|
||||
|
||||
FOREACH rid IN ARRAY rids
|
||||
LOOP
|
||||
BEGIN
|
||||
del_count := delete_expired_msgs(rid, p_old_ts);
|
||||
total_deleted := total_deleted + del_count;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE WARNING 'STORE, expire_old_messages, error expiring queue %: %', encode(rid, 'base64'), SQLERRM;
|
||||
CONTINUE;
|
||||
END;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
|
||||
last_ts := next_ts;
|
||||
last_rid := next_rid;
|
||||
END LOOP;
|
||||
|
||||
r_expired_msgs_count := total_deleted;
|
||||
r_stored_msgs_count := (SELECT COUNT(1) FROM messages);
|
||||
r_stored_queues := (SELECT COUNT(1) FROM msg_queues WHERE deleted_at IS NULL);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION delete_expired_msgs(p_recipient_id bytea, p_old_ts bigint) RETURNS bigint
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_size BIGINT;
|
||||
keep_min_id BIGINT;
|
||||
del_count BIGINT;
|
||||
BEGIN
|
||||
SELECT msg_queue_size INTO q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE SKIP LOCKED;
|
||||
|
||||
IF NOT FOUND OR q_size = 0 THEN
|
||||
RETURN 0;
|
||||
END IF;
|
||||
|
||||
SELECT MIN(message_id) INTO keep_min_id
|
||||
FROM messages WHERE recipient_id = p_recipient_id AND msg_ts >= p_old_ts AND msg_quota = FALSE;
|
||||
|
||||
IF keep_min_id IS NULL THEN
|
||||
DELETE FROM messages WHERE recipient_id = p_recipient_id AND msg_quota = FALSE;
|
||||
ELSE
|
||||
DELETE FROM messages WHERE recipient_id = p_recipient_id AND message_id < keep_min_id AND msg_quota = FALSE;
|
||||
END IF;
|
||||
|
||||
GET DIAGNOSTICS del_count = ROW_COUNT;
|
||||
IF del_count > 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= del_count,
|
||||
msg_queue_size = GREATEST(msg_queue_size - del_count, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
RETURN del_count;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION write_message(p_recipient_id bytea, p_msg_id bytea, p_msg_ts bigint, p_msg_quota boolean, p_msg_ntf_flag boolean, p_msg_body bytea, p_quota integer) RETURNS TABLE(quota_written boolean, was_empty boolean)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_can_write BOOLEAN;
|
||||
q_size BIGINT;
|
||||
BEGIN
|
||||
SELECT msg_can_write, msg_queue_size INTO q_can_write, q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
IF q_can_write OR q_size = 0 THEN
|
||||
quota_written := p_msg_quota OR q_size >= p_quota;
|
||||
was_empty := q_size = 0;
|
||||
|
||||
INSERT INTO messages(recipient_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body)
|
||||
VALUES (p_recipient_id, p_msg_id, p_msg_ts, quota_written, p_msg_ntf_flag AND NOT quota_written, CASE WHEN quota_written THEN '' :: BYTEA ELSE p_msg_body END);
|
||||
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = NOT quota_written,
|
||||
msg_queue_size = msg_queue_size + 1
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
|
||||
RETURN QUERY VALUES (quota_written, was_empty);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION try_del_msg(p_recipient_id bytea, p_msg_id bytea) RETURNS TABLE(r_msg_id bytea, r_msg_ts bigint, r_msg_quota boolean, r_msg_ntf_flag boolean, r_msg_body bytea)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_size BIGINT;
|
||||
msg RECORD;
|
||||
BEGIN
|
||||
SELECT msg_queue_size INTO q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT message_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
INTO msg
|
||||
FROM messages
|
||||
WHERE recipient_id = p_recipient_id
|
||||
ORDER BY message_id ASC LIMIT 1;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
IF q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF msg.msg_id = p_msg_id THEN
|
||||
DELETE FROM messages WHERE message_id = msg.message_id;
|
||||
IF FOUND THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
||||
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION try_del_peek_msg(p_recipient_id bytea, p_msg_id bytea) RETURNS TABLE(r_msg_id bytea, r_msg_ts bigint, r_msg_quota boolean, r_msg_ntf_flag boolean, r_msg_body bytea)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_size BIGINT;
|
||||
msg RECORD;
|
||||
msg_deleted BOOLEAN;
|
||||
BEGIN
|
||||
SELECT msg_queue_size INTO q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT message_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
INTO msg
|
||||
FROM messages
|
||||
WHERE recipient_id = p_recipient_id
|
||||
ORDER BY message_id ASC LIMIT 1;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
IF q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF msg.msg_id = p_msg_id THEN
|
||||
DELETE FROM messages WHERE message_id = msg.message_id;
|
||||
|
||||
msg_deleted := FOUND;
|
||||
IF msg_deleted THEN
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
END IF;
|
||||
|
||||
SELECT msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
INTO msg
|
||||
FROM messages
|
||||
WHERE recipient_id = p_recipient_id
|
||||
ORDER BY message_id ASC LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
IF msg_deleted THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
||||
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
ELSIF msg_deleted OR q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
ELSE
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER TABLE msg_queues DROP COLUMN msg_queue_expire;
|
||||
|]
|
||||
|
||||
down_m20260918_expire_messages :: Text
|
||||
down_m20260918_expire_messages =
|
||||
[r|
|
||||
ALTER TABLE msg_queues ADD COLUMN msg_queue_expire boolean NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- ADD COLUMN already defaulted every row to FALSE, so the backfill only needs the rows
|
||||
-- that become TRUE, which avoids rewriting the whole table. The flag is only read to pick
|
||||
-- queues for delete_expired_msgs, and that returns 0 without deleting when msg_queue_size
|
||||
-- is 0, so excluding empty queues here cannot change what expires.
|
||||
UPDATE msg_queues q
|
||||
SET msg_queue_expire = TRUE
|
||||
WHERE msg_queue_size > 0
|
||||
AND EXISTS (SELECT 1 FROM messages m WHERE m.recipient_id = q.recipient_id AND NOT m.msg_quota);
|
||||
|
||||
CREATE INDEX idx_msg_queues_expire ON msg_queues (recipient_id) WHERE deleted_at IS NULL AND msg_queue_expire;
|
||||
|
||||
CREATE INDEX idx_messages_recipient_id_msg_ts ON messages (recipient_id, msg_ts);
|
||||
|
||||
DROP INDEX idx_messages_expire;
|
||||
|
||||
CREATE OR REPLACE FUNCTION delete_expired_msgs(p_recipient_id bytea, p_old_ts bigint) RETURNS bigint
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_size BIGINT;
|
||||
keep_min_id BIGINT;
|
||||
del_count BIGINT;
|
||||
BEGIN
|
||||
SELECT msg_queue_size INTO q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE SKIP LOCKED;
|
||||
|
||||
IF NOT FOUND OR q_size = 0 THEN
|
||||
RETURN 0;
|
||||
END IF;
|
||||
|
||||
SELECT MIN(message_id) INTO keep_min_id
|
||||
FROM messages WHERE recipient_id = p_recipient_id AND msg_ts >= p_old_ts AND msg_quota = FALSE;
|
||||
|
||||
IF keep_min_id IS NULL THEN
|
||||
DELETE FROM messages WHERE recipient_id = p_recipient_id AND msg_quota = FALSE;
|
||||
ELSE
|
||||
DELETE FROM messages WHERE recipient_id = p_recipient_id AND message_id < keep_min_id AND msg_quota = FALSE;
|
||||
END IF;
|
||||
|
||||
GET DIAGNOSTICS del_count = ROW_COUNT;
|
||||
IF del_count > 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= del_count,
|
||||
msg_queue_expire = msg_queue_size > del_count AND keep_min_id IS NOT NULL,
|
||||
msg_queue_size = GREATEST(msg_queue_size - del_count, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
RETURN del_count;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION write_message(p_recipient_id bytea, p_msg_id bytea, p_msg_ts bigint, p_msg_quota boolean, p_msg_ntf_flag boolean, p_msg_body bytea, p_quota integer) RETURNS TABLE(quota_written boolean, was_empty boolean)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_can_write BOOLEAN;
|
||||
q_size BIGINT;
|
||||
BEGIN
|
||||
SELECT msg_can_write, msg_queue_size INTO q_can_write, q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
IF q_can_write OR q_size = 0 THEN
|
||||
quota_written := p_msg_quota OR q_size >= p_quota;
|
||||
was_empty := q_size = 0;
|
||||
|
||||
INSERT INTO messages(recipient_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body)
|
||||
VALUES (p_recipient_id, p_msg_id, p_msg_ts, quota_written, p_msg_ntf_flag AND NOT quota_written, CASE WHEN quota_written THEN '' :: BYTEA ELSE p_msg_body END);
|
||||
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = NOT quota_written,
|
||||
msg_queue_expire = TRUE,
|
||||
msg_queue_size = msg_queue_size + 1
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
|
||||
RETURN QUERY VALUES (quota_written, was_empty);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION try_del_msg(p_recipient_id bytea, p_msg_id bytea) RETURNS TABLE(r_msg_id bytea, r_msg_ts bigint, r_msg_quota boolean, r_msg_ntf_flag boolean, r_msg_body bytea)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_size BIGINT;
|
||||
msg RECORD;
|
||||
BEGIN
|
||||
SELECT msg_queue_size INTO q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT message_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
INTO msg
|
||||
FROM messages
|
||||
WHERE recipient_id = p_recipient_id
|
||||
ORDER BY message_id ASC LIMIT 1;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
IF q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_expire = FALSE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF msg.msg_id = p_msg_id THEN
|
||||
DELETE FROM messages WHERE message_id = msg.message_id;
|
||||
IF FOUND THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
||||
msg_queue_expire = msg_queue_size > 1,
|
||||
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION try_del_peek_msg(p_recipient_id bytea, p_msg_id bytea) RETURNS TABLE(r_msg_id bytea, r_msg_ts bigint, r_msg_quota boolean, r_msg_ntf_flag boolean, r_msg_body bytea)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
q_size BIGINT;
|
||||
msg RECORD;
|
||||
msg_deleted BOOLEAN;
|
||||
BEGIN
|
||||
SELECT msg_queue_size INTO q_size
|
||||
FROM msg_queues
|
||||
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT message_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
INTO msg
|
||||
FROM messages
|
||||
WHERE recipient_id = p_recipient_id
|
||||
ORDER BY message_id ASC LIMIT 1;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
IF q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_expire = FALSE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF msg.msg_id = p_msg_id THEN
|
||||
DELETE FROM messages WHERE message_id = msg.message_id;
|
||||
|
||||
msg_deleted := FOUND;
|
||||
IF msg_deleted THEN
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
END IF;
|
||||
|
||||
SELECT msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
INTO msg
|
||||
FROM messages
|
||||
WHERE recipient_id = p_recipient_id
|
||||
ORDER BY message_id ASC LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
IF msg_deleted THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
||||
msg_queue_expire = msg_queue_size > 1,
|
||||
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
ELSIF msg_deleted OR q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_expire = FALSE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
ELSE
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP PROCEDURE expire_old_messages(bigint, integer);
|
||||
|
||||
CREATE PROCEDURE expire_old_messages(IN p_old_ts bigint, IN batch_size integer, OUT r_expired_msgs_count bigint, OUT r_stored_msgs_count bigint, OUT r_stored_queues bigint)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
rids BYTEA[];
|
||||
rid BYTEA;
|
||||
last_rid BYTEA := '\x';
|
||||
del_count BIGINT;
|
||||
total_deleted BIGINT := 0;
|
||||
BEGIN
|
||||
LOOP
|
||||
SELECT array_agg(recipient_id)
|
||||
INTO rids
|
||||
FROM (
|
||||
SELECT recipient_id
|
||||
FROM msg_queues
|
||||
WHERE deleted_at IS NULL
|
||||
AND msg_queue_expire = TRUE
|
||||
AND recipient_id > last_rid
|
||||
ORDER BY recipient_id ASC
|
||||
LIMIT batch_size
|
||||
) qs;
|
||||
|
||||
EXIT WHEN rids IS NULL OR cardinality(rids) = 0;
|
||||
|
||||
FOREACH rid IN ARRAY rids
|
||||
LOOP
|
||||
BEGIN
|
||||
del_count := delete_expired_msgs(rid, p_old_ts);
|
||||
total_deleted := total_deleted + del_count;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE WARNING 'STORE, expire_old_messages, error expiring queue %: %', encode(rid, 'base64'), SQLERRM;
|
||||
CONTINUE;
|
||||
END;
|
||||
COMMIT;
|
||||
END LOOP;
|
||||
last_rid := rids[cardinality(rids)];
|
||||
END LOOP;
|
||||
|
||||
r_expired_msgs_count := total_deleted;
|
||||
r_stored_msgs_count := (SELECT COUNT(1) FROM messages);
|
||||
r_stored_queues := (SELECT COUNT(1) FROM msg_queues WHERE deleted_at IS NULL);
|
||||
END;
|
||||
$$;
|
||||
|]
|
||||
|
||||
@@ -47,6 +47,7 @@ BEGIN
|
||||
IF del_count > 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= del_count,
|
||||
msg_queue_expire = msg_queue_size > del_count AND keep_min_id IS NOT NULL,
|
||||
msg_queue_size = GREATEST(msg_queue_size - del_count, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
@@ -62,33 +63,24 @@ CREATE PROCEDURE smp_server.expire_old_messages(IN p_old_ts bigint, IN batch_siz
|
||||
DECLARE
|
||||
rids BYTEA[];
|
||||
rid BYTEA;
|
||||
last_ts BIGINT := -1;
|
||||
last_rid BYTEA := '\x';
|
||||
next_ts BIGINT;
|
||||
next_rid BYTEA;
|
||||
del_count BIGINT;
|
||||
total_deleted BIGINT := 0;
|
||||
BEGIN
|
||||
LOOP
|
||||
-- The page is scanned in (msg_ts, recipient_id) order, so its last row is the next
|
||||
-- keyset cursor. Advancing it past every row read, including queues left unexpired
|
||||
-- because delete_expired_msgs skipped a locked row or raised, is what terminates the
|
||||
-- loop; re-reading from the start instead would repeat those queues forever.
|
||||
SELECT array_agg(DISTINCT recipient_id),
|
||||
(array_agg(msg_ts ORDER BY msg_ts DESC, recipient_id DESC))[1],
|
||||
(array_agg(recipient_id ORDER BY msg_ts DESC, recipient_id DESC))[1]
|
||||
INTO rids, next_ts, next_rid
|
||||
SELECT array_agg(recipient_id)
|
||||
INTO rids
|
||||
FROM (
|
||||
SELECT msg_ts, recipient_id
|
||||
FROM messages
|
||||
WHERE NOT msg_quota
|
||||
AND msg_ts < p_old_ts
|
||||
AND (msg_ts, recipient_id) > (last_ts, last_rid)
|
||||
ORDER BY msg_ts ASC, recipient_id ASC
|
||||
SELECT recipient_id
|
||||
FROM msg_queues
|
||||
WHERE deleted_at IS NULL
|
||||
AND msg_queue_expire = TRUE
|
||||
AND recipient_id > last_rid
|
||||
ORDER BY recipient_id ASC
|
||||
LIMIT batch_size
|
||||
) m;
|
||||
) qs;
|
||||
|
||||
EXIT WHEN rids IS NULL;
|
||||
EXIT WHEN rids IS NULL OR cardinality(rids) = 0;
|
||||
|
||||
FOREACH rid IN ARRAY rids
|
||||
LOOP
|
||||
@@ -99,11 +91,9 @@ BEGIN
|
||||
RAISE WARNING 'STORE, expire_old_messages, error expiring queue %: %', encode(rid, 'base64'), SQLERRM;
|
||||
CONTINUE;
|
||||
END;
|
||||
COMMIT;
|
||||
END LOOP;
|
||||
COMMIT;
|
||||
|
||||
last_ts := next_ts;
|
||||
last_rid := next_rid;
|
||||
last_rid := rids[cardinality(rids)];
|
||||
END LOOP;
|
||||
|
||||
r_expired_msgs_count := total_deleted;
|
||||
@@ -205,6 +195,7 @@ BEGIN
|
||||
IF q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_expire = FALSE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
@@ -216,6 +207,7 @@ BEGIN
|
||||
IF FOUND THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
||||
msg_queue_expire = msg_queue_size > 1,
|
||||
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
||||
@@ -253,6 +245,7 @@ BEGIN
|
||||
IF q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_expire = FALSE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
@@ -278,12 +271,14 @@ BEGIN
|
||||
IF msg_deleted THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
||||
msg_queue_expire = msg_queue_size > 1,
|
||||
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
ELSIF msg_deleted OR q_size != 0 THEN
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = TRUE,
|
||||
msg_queue_expire = FALSE,
|
||||
msg_queue_size = 0
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
END IF;
|
||||
@@ -353,6 +348,7 @@ BEGIN
|
||||
|
||||
UPDATE msg_queues
|
||||
SET msg_can_write = NOT quota_written,
|
||||
msg_queue_expire = TRUE,
|
||||
msg_queue_size = msg_queue_size + 1
|
||||
WHERE recipient_id = p_recipient_id;
|
||||
|
||||
@@ -444,6 +440,7 @@ CREATE TABLE smp_server.msg_queues (
|
||||
rcv_service_id bytea,
|
||||
ntf_service_id bytea,
|
||||
msg_can_write boolean DEFAULT true NOT NULL,
|
||||
msg_queue_expire boolean DEFAULT false NOT NULL,
|
||||
msg_queue_size bigint DEFAULT 0 NOT NULL
|
||||
)
|
||||
WITH (fillfactor='80', autovacuum_vacuum_scale_factor='0.02', autovacuum_analyze_scale_factor='0.01', autovacuum_vacuum_cost_limit='1000');
|
||||
@@ -488,10 +485,6 @@ ALTER TABLE ONLY smp_server.services
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_messages_expire ON smp_server.messages USING btree (msg_ts, recipient_id) WHERE (NOT msg_quota);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_messages_recipient_id_message_id ON smp_server.messages USING btree (recipient_id, message_id);
|
||||
|
||||
|
||||
@@ -500,6 +493,14 @@ CREATE INDEX idx_messages_recipient_id_msg_quota ON smp_server.messages USING bt
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_messages_recipient_id_msg_ts ON smp_server.messages USING btree (recipient_id, msg_ts);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_msg_queues_expire ON smp_server.msg_queues USING btree (recipient_id) WHERE ((deleted_at IS NULL) AND msg_queue_expire);
|
||||
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX idx_msg_queues_link_id ON smp_server.msg_queues USING btree (link_id);
|
||||
|
||||
|
||||
|
||||
@@ -173,7 +173,6 @@ instance StoreQueueClass q => QueueStoreClass q (STMQueueStore q) where
|
||||
rId = recipientId sq
|
||||
qr = queueRec sq
|
||||
add q = case queueData q of
|
||||
_ | isSecuredMsgQueue q -> pure $ Left AUTH
|
||||
Nothing -> addLink
|
||||
Just (lnkId', d') | lnkId' == lnkId && fst d' == fst d -> addLink
|
||||
_ -> pure $ Left AUTH
|
||||
|
||||
@@ -46,6 +46,7 @@ module Simplex.Messaging.Transport
|
||||
minServerSMPRelayVersion,
|
||||
currentClientSMPRelayVersion,
|
||||
currentServerSMPRelayVersion,
|
||||
shortLinksSMPVersion,
|
||||
serviceCertsSMPVersion,
|
||||
newNtfCredsSMPVersion,
|
||||
clientNoticesSMPVersion,
|
||||
@@ -53,7 +54,7 @@ module Simplex.Messaging.Transport
|
||||
namesSMPVersion,
|
||||
serverInfoSMPVersion,
|
||||
nameAvailSMPVersion,
|
||||
fwdNoncesSMPVersion,
|
||||
nameOwnedSMPVersion,
|
||||
simplexMQVersion,
|
||||
smpBlockSize,
|
||||
TransportConfig (..),
|
||||
@@ -177,7 +178,7 @@ smpBlockSize = 16384
|
||||
-- 20 - public namespaces resolver, RSLV command (6/20/2026)
|
||||
-- 21 - server public information in handshake (7/5/2026)
|
||||
-- 22 - RNAME answers name availability as well as the record (7/25/2026)
|
||||
-- 23 - version in forwarded command nonce, random nonce in forwarded responses (10/2/2026)
|
||||
-- 23 - ROWN command, the names an address owns (9/22/2026)
|
||||
|
||||
data SMPVersion
|
||||
|
||||
@@ -190,8 +191,11 @@ type VersionRangeSMP = VersionRange SMPVersion
|
||||
pattern VersionSMP :: Word16 -> VersionSMP
|
||||
pattern VersionSMP v = Version v
|
||||
|
||||
_shortLinksSMPVersion :: VersionSMP
|
||||
_shortLinksSMPVersion = VersionSMP 15
|
||||
_proxyServerHandshakeSMPVersion :: VersionSMP
|
||||
_proxyServerHandshakeSMPVersion = VersionSMP 14
|
||||
|
||||
shortLinksSMPVersion :: VersionSMP
|
||||
shortLinksSMPVersion = VersionSMP 15
|
||||
|
||||
serviceCertsSMPVersion :: VersionSMP
|
||||
serviceCertsSMPVersion = VersionSMP 16
|
||||
@@ -216,14 +220,15 @@ serverInfoSMPVersion = VersionSMP 21
|
||||
nameAvailSMPVersion :: VersionSMP
|
||||
nameAvailSMPVersion = VersionSMP 22
|
||||
|
||||
fwdNoncesSMPVersion :: VersionSMP
|
||||
fwdNoncesSMPVersion = VersionSMP 23
|
||||
-- | ROWN lists the names an address owns; below this a server does not answer it, which is not the same as owning nothing.
|
||||
nameOwnedSMPVersion :: VersionSMP
|
||||
nameOwnedSMPVersion = VersionSMP 23
|
||||
|
||||
minClientSMPRelayVersion :: VersionSMP
|
||||
minClientSMPRelayVersion = VersionSMP 15
|
||||
minClientSMPRelayVersion = VersionSMP 14
|
||||
|
||||
minServerSMPRelayVersion :: VersionSMP
|
||||
minServerSMPRelayVersion = VersionSMP 15
|
||||
minServerSMPRelayVersion = VersionSMP 14
|
||||
|
||||
currentClientSMPRelayVersion :: VersionSMP
|
||||
currentClientSMPRelayVersion = VersionSMP 23
|
||||
@@ -234,14 +239,11 @@ currentServerSMPRelayVersion = VersionSMP 23
|
||||
-- Max SMP protocol version to be used in e2e encrypted connection between
|
||||
-- client and server, as defined by SMP proxy. Normally set below the current
|
||||
-- version to prevent client version fingerprinting by the destination relays
|
||||
-- when clients upgrade at different times. Pinned to the current version (23)
|
||||
-- for this release because forwarded commands use the nonces from
|
||||
-- fwdNoncesSMPVersion (23), so the one-version anti-fingerprinting buffer does
|
||||
-- not apply yet; it reappears once the current version advances past 23.
|
||||
-- when clients upgrade at different times. Pinned to the current version (23) because a proxied ROWN needs it, and the buffer returns once current passes 23.
|
||||
proxiedSMPRelayVersion :: VersionSMP
|
||||
proxiedSMPRelayVersion = VersionSMP 23
|
||||
|
||||
-- minimal supported protocol version is 15
|
||||
-- minimal supported protocol version is 14
|
||||
supportedClientSMPRelayVRange :: VersionRangeSMP
|
||||
supportedClientSMPRelayVRange = mkVersionRange minClientSMPRelayVersion currentClientSMPRelayVersion
|
||||
|
||||
@@ -866,6 +868,7 @@ forceCertChain :: CertChainPubKey -> CertChainPubKey
|
||||
forceCertChain cert@(CertChainPubKey (X.CertificateChain cc) signedKey) = length (show cc) `seq` show signedKey `seq` cert
|
||||
{-# INLINE forceCertChain #-}
|
||||
|
||||
-- This function is only used with v >= 8, so currently it's a simple record update.
|
||||
-- * Note: it requires updating version-based parameters, to be consistent with smpTHandle_.
|
||||
smpTHParamsSetVersion :: VersionSMP -> THandleParams SMPVersion p -> THandleParams SMPVersion p
|
||||
smpTHParamsSetVersion v params =
|
||||
|
||||
@@ -11,7 +11,6 @@ module Simplex.Messaging.Transport.KeepAlive
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Control.Monad (when)
|
||||
import Foreign.C (CInt (..))
|
||||
import Network.Socket
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
@@ -19,25 +18,16 @@ import Simplex.Messaging.Parsers (defaultJSON)
|
||||
data KeepAliveOpts = KeepAliveOpts
|
||||
{ keepIdle :: Int,
|
||||
keepIntvl :: Int,
|
||||
keepCnt :: Int,
|
||||
-- | A connection with unacknowledged data gets no keep-alive probes, so the OS fails it only when it
|
||||
-- gives up retransmitting - ~15 minutes on Linux and Android. When this is set such a connection is
|
||||
-- failed after the keep-alive detection time instead. Absent or False - the OS default.
|
||||
unackedDataTimeout :: Maybe Bool
|
||||
keepCnt :: Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- seconds after which a connection with no response to the probes is reported as failed
|
||||
keepAliveDetectionTime :: KeepAliveOpts -> Int
|
||||
keepAliveDetectionTime KeepAliveOpts {keepIdle, keepIntvl, keepCnt} = keepIdle + keepIntvl * keepCnt
|
||||
|
||||
defaultKeepAliveOpts :: KeepAliveOpts
|
||||
defaultKeepAliveOpts =
|
||||
KeepAliveOpts
|
||||
{ keepIdle = 30,
|
||||
keepIntvl = 15,
|
||||
keepCnt = 4,
|
||||
unackedDataTimeout = Nothing
|
||||
keepCnt = 4
|
||||
}
|
||||
|
||||
_SOL_TCP :: CInt
|
||||
@@ -63,12 +53,8 @@ _TCP_KEEPCNT = 16
|
||||
|
||||
#if defined(darwin_HOST_OS)
|
||||
foreign import capi "netinet/tcp.h value TCP_KEEPALIVE" _TCP_KEEPIDLE :: CInt
|
||||
|
||||
foreign import capi "netinet/tcp.h value TCP_RXT_CONNDROPTIME" _TCP_RXT_CONNDROPTIME :: CInt
|
||||
#else
|
||||
foreign import capi "netinet/tcp.h value TCP_KEEPIDLE" _TCP_KEEPIDLE :: CInt
|
||||
|
||||
foreign import capi "netinet/tcp.h value TCP_USER_TIMEOUT" _TCP_USER_TIMEOUT :: CInt
|
||||
#endif
|
||||
|
||||
foreign import capi "netinet/tcp.h value TCP_KEEPINTVL" _TCP_KEEPINTVL :: CInt
|
||||
@@ -77,22 +63,11 @@ foreign import capi "netinet/tcp.h value TCP_KEEPCNT" _TCP_KEEPCNT :: CInt
|
||||
|
||||
#endif
|
||||
|
||||
-- | seconds
|
||||
setSocketUnackedDataTimeout :: Socket -> Int -> IO ()
|
||||
#if defined(mingw32_HOST_OS)
|
||||
setSocketUnackedDataTimeout _ _ = pure ()
|
||||
#elif defined(darwin_HOST_OS)
|
||||
setSocketUnackedDataTimeout sock = setSocketOption sock (SockOpt _SOL_TCP _TCP_RXT_CONNDROPTIME)
|
||||
#else
|
||||
setSocketUnackedDataTimeout sock t = setSocketOption sock (SockOpt _SOL_TCP _TCP_USER_TIMEOUT) (t * 1000) -- TCP_USER_TIMEOUT is in milliseconds
|
||||
#endif
|
||||
|
||||
setSocketKeepAlive :: Socket -> KeepAliveOpts -> IO ()
|
||||
setSocketKeepAlive sock ka@KeepAliveOpts {keepCnt, keepIdle, keepIntvl, unackedDataTimeout} = do
|
||||
setSocketKeepAlive sock KeepAliveOpts {keepCnt, keepIdle, keepIntvl} = do
|
||||
setSocketOption sock KeepAlive 1
|
||||
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPIDLE) keepIdle
|
||||
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPINTVL) keepIntvl
|
||||
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPCNT) keepCnt
|
||||
when (unackedDataTimeout == Just True) $ setSocketUnackedDataTimeout sock $ keepAliveDetectionTime ka
|
||||
|
||||
$(J.deriveJSON defaultJSON ''KeepAliveOpts)
|
||||
|
||||
@@ -26,7 +26,6 @@ module Simplex.RemoteControl.Client
|
||||
-- for tests only
|
||||
sendRCPacket,
|
||||
receiveRCPacket,
|
||||
findRCCtrlPairing,
|
||||
) where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
@@ -47,7 +46,7 @@ import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (isNothing)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import Data.Time.Clock.System (getSystemTime)
|
||||
import Data.Tuple (swap)
|
||||
import Data.Word (Word16)
|
||||
import qualified Data.X509 as X
|
||||
@@ -396,10 +395,8 @@ findRCCtrlPairing :: NonEmpty RCCtrlPairing -> RCEncInvitation -> ExceptT RCErro
|
||||
findRCCtrlPairing pairings RCEncInvitation {dhPubKey, nonce, encInvitation} = do
|
||||
(pairing, signedInvStr) <- liftEither $ decrypt (L.toList pairings)
|
||||
signedInv <- liftEitherWith RCESyntax $ strDecode signedInvStr
|
||||
inv@(RCVerifiedInvitation RCInvitation {dh = invDh, ts}) <- maybe (throwE RCEInvitation) pure $ verifySignedInvitation signedInv
|
||||
inv@(RCVerifiedInvitation RCInvitation {dh = invDh}) <- maybe (throwE RCEInvitation) pure $ verifySignedInvitation signedInv
|
||||
unless (invDh == dhPubKey) $ throwE RCEInvitation
|
||||
now <- systemSeconds <$> liftIO getSystemTime
|
||||
unless (now - 3660 <= systemSeconds ts && systemSeconds ts <= now + 3600) $ throwE RCEInvitation
|
||||
pure (pairing, inv)
|
||||
where
|
||||
decrypt :: [RCCtrlPairing] -> Either RCErrorType (RCCtrlPairing, ByteString)
|
||||
|
||||
@@ -122,22 +122,18 @@ closeListener subscribers sock =
|
||||
joinMulticast :: TMVar Int -> N.Socket -> N.HostAddress -> IO ()
|
||||
joinMulticast subscribers sock group = do
|
||||
now <- atomically $ takeTMVar subscribers
|
||||
if now == 0
|
||||
then
|
||||
setMembership sock group True >>= \case
|
||||
Left e -> atomically (putTMVar subscribers now) >> logError ("setMembership failed " <> tshow e)
|
||||
Right () -> atomically $ putTMVar subscribers (now + 1)
|
||||
else atomically $ putTMVar subscribers (now + 1)
|
||||
when (now == 0) $ do
|
||||
setMembership sock group True >>= \case
|
||||
Left e -> atomically (putTMVar subscribers now) >> logError ("setMembership failed " <> tshow e)
|
||||
Right () -> atomically $ putTMVar subscribers (now + 1)
|
||||
|
||||
partMulticast :: TMVar Int -> N.Socket -> N.HostAddress -> IO ()
|
||||
partMulticast subscribers sock group = do
|
||||
now <- atomically $ takeTMVar subscribers
|
||||
if now == 1
|
||||
then
|
||||
setMembership sock group False >>= \case
|
||||
Left e -> atomically (putTMVar subscribers (now - 1)) >> logError ("setMembership failed " <> tshow e)
|
||||
Right () -> atomically $ putTMVar subscribers (now - 1)
|
||||
else atomically $ putTMVar subscribers (max 0 (now - 1))
|
||||
when (now == 1) $
|
||||
setMembership sock group False >>= \case
|
||||
Left e -> atomically (putTMVar subscribers now) >> logError ("setMembership failed " <> tshow e)
|
||||
Right () -> atomically $ putTMVar subscribers (now - 1)
|
||||
|
||||
listenerHostAddr4 :: UDP.ListenSocket -> N.HostAddress
|
||||
listenerHostAddr4 sock = case UDP.mySockAddr sock of
|
||||
|
||||
@@ -20,8 +20,6 @@ module AgentTests.ConnectionRequestTests
|
||||
|
||||
import AgentTests.EqInstances ()
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (isLeft)
|
||||
import Network.HTTP.Types (urlEncode)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -287,9 +285,6 @@ connectionRequestTests =
|
||||
contactAddressV6 #== ("https://simplex.chat/contact#/?v=1-2&smp=" <> url queueStr) -- adjusted to v6
|
||||
contactAddressV6 #== ("https://simplex.chat/contact#/?v=2-2&smp=" <> url queueStr)
|
||||
contactAddressClientData #==# ("simplex:/contact#/?v=6-8&smp=" <> url queueStr <> "&data=" <> url "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}")
|
||||
it "should reject KEM ciphertext without KEM key in e2e params" $
|
||||
strDecode @(RcvE2ERatchetParamsUri 'C.X448) (strEncode testE2ERatchetParams <> "&kem_ct=" <> strEncode (B.replicate 1039 '\0'))
|
||||
`shouldSatisfy` isLeft
|
||||
it "should serialize / parse queue address, connection invitations and contact addresses as binary" $ do
|
||||
smpEncodingTest queue
|
||||
smpEncodingTest queueNoQM -- this passes, no queue mode patch in SMPQueueUri encoding
|
||||
@@ -362,10 +357,6 @@ 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
|
||||
|
||||
@@ -53,7 +53,6 @@ doubleRatchetTests = do
|
||||
it "should propose KEM during agreement, but no shared secret" $ testAlgs testPqX3dhProposeInReply
|
||||
it "should agree shared secret using KEM" $ testAlgs testPqX3dhProposeAccept
|
||||
it "should reject proposed KEM in reply" $ testAlgs testPqX3dhProposeReject
|
||||
it "should agree different PQ associated data with substituted KEM key" $ testAlgs testPqX3dhSubstitutedKem
|
||||
it "should allow second proposal in reply" $ testAlgs testPqX3dhProposeAgain
|
||||
describe "hybrid KEM key agreement errors" $ do
|
||||
it "should fail if reply contains acceptance without proposal" $ testAlgs testPqX3dhAcceptWithoutProposalError
|
||||
@@ -363,7 +362,6 @@ testDecodeV2RatchetJSON :: IO ()
|
||||
testDecodeV2RatchetJSON = do
|
||||
let v2RatchetJSON = "{\"rcVersion\":[2,2],\"rcAD\":\"2GEJrq48TmQse6NR16I-hrI0tSySZQ57E_g46nDceAPRAiF6j0drq26RTE7be6X7uiB4RaGJGf4QRXzcYuVtWw==\",\"rcDHRs\":\"TUM0Q0FRQXdCUVlESzJWdUJDSUVJRkNYbUxtSHQ3SUNfeHpGTi1Qb3ZqTVQ3S2p6XzZlZlBjOG9fRFY2RWxKOQ==\",\"rcRK\":\"BOX2X7YW5qDSp2XknY_lqacSrtDqQNPvS6iJlZIs3G0=\",\"rcNs\":0,\"rcNr\":0,\"rcPN\":0,\"rcNHKs\":\"IMouSkXUvzT_mo0WM-pqEUK09-HTLk9WOTCFQglyQxU=\",\"rcNHKr\":\"g-tus1clYPV0rGlzkf5a959tUqDYQVZ1FpcPeXdKwxI=\"}"
|
||||
Right (r :: Ratchet X25519) <- pure $ J.eitherDecodeStrict' v2RatchetJSON
|
||||
rcVCPQ r `shouldBe` Nothing
|
||||
rcSupportKEM r `shouldBe` PQSupportOff
|
||||
rcEnableKEM r `shouldBe` PQEncOff
|
||||
rcSndKEM r `shouldBe` PQEncOff
|
||||
@@ -419,18 +417,6 @@ testPqX3dhProposeAccept _ = do
|
||||
Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob
|
||||
paramsAlice `compatibleRatchets` paramsBob
|
||||
|
||||
testPqX3dhSubstitutedKem :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO ()
|
||||
testPqX3dhSubstitutedKem _ = do
|
||||
g <- C.newRandom
|
||||
let v = currentE2EEncryptVersion
|
||||
(pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn
|
||||
(_, E2ERatchetParams _ _ _ (Just (RKParamsProposed mallorysKem))) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn
|
||||
(pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSAccepted $ AcceptKEM mallorysKem)
|
||||
Right (paramsBob, _) <- pure $ pqX3dhSnd pksBob e2eAlice
|
||||
Right (paramsAlice, _) <- runExceptT $ pqX3dhRcv pksAlice e2eBob
|
||||
assocData paramsAlice `shouldBe` assocData paramsBob
|
||||
rcVerifyCodePQ paramsAlice `shouldNotBe` rcVerifyCodePQ paramsBob
|
||||
|
||||
testPqX3dhProposeReject :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO ()
|
||||
testPqX3dhProposeReject _ = do
|
||||
g <- C.newRandom
|
||||
@@ -473,9 +459,9 @@ testPqX3dhProposeAgain _ = do
|
||||
|
||||
compatibleRatchets :: (RatchetInitParams, x) -> (RatchetInitParams, x) -> Expectation
|
||||
compatibleRatchets
|
||||
(RatchetInitParams {assocData, rcVerifyCodePQ, ratchetKey, sndHK, rcvNextHK, kemAccepted}, _)
|
||||
(RatchetInitParams {assocData = ad, rcVerifyCodePQ = vcPQ, ratchetKey = rk, sndHK = shk, rcvNextHK = rnhk, kemAccepted = ka}, _) = do
|
||||
assocData == ad && rcVerifyCodePQ == vcPQ && ratchetKey == rk && sndHK == shk && rcvNextHK == rnhk `shouldBe` True
|
||||
(RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, _)
|
||||
(RatchetInitParams {assocData = ad, ratchetKey = rk, sndHK = shk, rcvNextHK = rnhk, kemAccepted = ka}, _) = do
|
||||
assocData == ad && ratchetKey == rk && sndHK == shk && rcvNextHK == rnhk `shouldBe` True
|
||||
case (kemAccepted, ka) of
|
||||
(Just RatchetKEMAccepted {rcPQRr, rcPQRss, rcPQRct}, Just RatchetKEMAccepted {rcPQRr = pqk, rcPQRss = pqss, rcPQRct = pqct}) ->
|
||||
pqk /= rcPQRr && pqss == rcPQRss && pqct == rcPQRct `shouldBe` True
|
||||
|
||||
@@ -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, isInfixOf, isPrefixOf, isSuffixOf)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.List (find, 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 (..), sendAgentMessage, waitForUserNetwork)
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), ServerQueueInfo (..), UserNetworkInfo (..), UserNetworkType (..), 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 (deleteRatchetKeyHashesExpired, getConn, getRatchetX3dhKeys)
|
||||
import Simplex.Messaging.Agent.Store.AgentStore (getConn)
|
||||
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)
|
||||
import Simplex.Messaging.Transport (ASrvTransport, SMPVersion, VersionSMP, currentServerSMPRelayVersion, minClientSMPRelayVersion, minServerSMPRelayVersion, alpnSupportedSMPHandshakes, supportedServerSMPRelayVRange)
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..))
|
||||
import Simplex.Messaging.Util (bshow, diffToMicroseconds)
|
||||
import Simplex.Messaging.Version (VersionRange (..))
|
||||
@@ -204,7 +204,7 @@ pattern INFO :: ConnInfo -> AEvent 'AEConn
|
||||
pattern INFO connInfo = A.INFO PQSupportOn connInfo
|
||||
|
||||
pattern REQ :: InvitationId -> NonEmpty SMPServer -> ConnInfo -> AEvent e
|
||||
pattern REQ invId srvs connInfo <- A.REQ invId PQSupportOn srvs connInfo _ _
|
||||
pattern REQ invId srvs connInfo <- A.REQ invId PQSupportOn srvs connInfo _
|
||||
|
||||
pattern CON :: AEvent 'AEConn
|
||||
pattern CON = A.CON PQEncOn
|
||||
@@ -231,11 +231,7 @@ pattern Rcvd' :: AgentMsgId -> AgentMsgId -> AEvent 'AEConn
|
||||
pattern Rcvd' aMsgId rcvdMsgId <- RCVD MsgMeta {integrity = MsgOk, recipient = (aMsgId, _)} [MsgReceipt {agentMsgId = rcvdMsgId, msgRcptStatus = MROk}]
|
||||
|
||||
smpCfgVPrev :: ProtocolClientConfig SMPVersion
|
||||
smpCfgVPrev =
|
||||
(smpCfg agentCfg)
|
||||
{ serverVRange = prevRange $ serverVRange $ smpCfg agentCfg,
|
||||
proxiedRelayVRange = prevRange $ proxiedRelayVRange $ smpCfg agentCfg
|
||||
}
|
||||
smpCfgVPrev = (smpCfg agentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg agentCfg}
|
||||
|
||||
-- ntfCfgVPrev :: ProtocolClientConfig NTFVersion
|
||||
-- ntfCfgVPrev = (ntfCfg agentCfg) {clientALPN = Nothing, serverVRange = V.mkVersionRange (VersionNTF 1) (VersionNTF 1)}
|
||||
@@ -297,7 +293,7 @@ createConnection c userId enableNtfs cMode clientData subMode = do
|
||||
|
||||
joinConnection :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> AE (ConnId, SndQueueSecured)
|
||||
joinConnection c userId enableNtfs cReq connInfo subMode = do
|
||||
(connId, _) <- A.prepareConnectionToJoin c userId enableNtfs cReq PQSupportOn
|
||||
connId <- A.prepareConnectionToJoin c userId enableNtfs cReq PQSupportOn
|
||||
sndSecure <- A.joinConnection c NRMInteractive userId connId enableNtfs cReq connInfo PQSupportOn subMode
|
||||
pure (connId, sndSecure)
|
||||
|
||||
@@ -415,6 +411,7 @@ 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
|
||||
@@ -445,9 +442,6 @@ functionalAPITests ps = do
|
||||
it "should expire multiple messages" $ testExpireManyMessages ps
|
||||
it "should expire one message if quota is exceeded" $ testExpireMessageQuota ps
|
||||
it "should expire multiple messages if quota is exceeded" $ testExpireManyMessagesQuota ps
|
||||
describe "joining a full contact address" $ do
|
||||
it "should retry joining until the address has space" $ testJoinFullContactAsync ps
|
||||
it "should fail joining after quota exceeded timeout" $ testJoinFullContactAsyncExpire ps
|
||||
it "should drop message after too many receive attempts" $ testDropMsgAfterRcvAttempts ps
|
||||
#if !defined(dbPostgres)
|
||||
-- TODO [postgres] restore from outdated db backup (we use copyFile/renameFile for sqlite)
|
||||
@@ -462,16 +456,6 @@ 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" $
|
||||
@@ -550,10 +534,6 @@ 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" $
|
||||
@@ -601,9 +581,9 @@ functionalAPITests ps = do
|
||||
it "should pass with correct password" $ testSMPServerConnectionTest ps auth (srv auth) `shouldReturn` Right (Just (Right testServerInformation))
|
||||
it "should fail without password" $ testSMPServerConnectionTest ps auth (srv Nothing) `shouldReturn` Left authErr
|
||||
it "should fail with incorrect password" $ testSMPServerConnectionTest ps auth (srv $ Just "wrong") `shouldReturn` Left authErr
|
||||
describe "getConnectionVerifyCodes" $
|
||||
it "should return the same codes for both peers" $
|
||||
withSmpServer ps testConnectionVerifyCodes
|
||||
describe "getRatchetAdHash" $
|
||||
it "should return the same data for both peers" $
|
||||
withSmpServer ps testRatchetAdHash
|
||||
describe "Delivery receipts" $ do
|
||||
it "should send and receive delivery receipt" $ withSmpServer ps testDeliveryReceipts
|
||||
it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent ps
|
||||
@@ -680,6 +660,19 @@ 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
|
||||
|
||||
@@ -762,7 +755,7 @@ runAgentClientTestPQ :: HasCallStack => Bool -> (AgentClient, InitialKeys) -> (A
|
||||
runAgentClientTestPQ viaProxy (alice, aPQ) (bob, bPQ) baseId =
|
||||
runRight_ $ do
|
||||
(bobId, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMInvitation Nothing Nothing aPQ False SMSubscribe
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True qInfo bPQ
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True qInfo bPQ
|
||||
sqSecured' <- A.joinConnection bob NRMInteractive 1 aliceId True qInfo "bob's connInfo" bPQ SMSubscribe
|
||||
liftIO $ sqSecured' `shouldBe` True
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice
|
||||
@@ -991,12 +984,12 @@ runAgentClientContactTestPQ :: HasCallStack => SndQueueSecured -> Bool -> (Agent
|
||||
runAgentClientContactTestPQ sqSecured viaProxy (alice, aPQ) (bob, bPQ) baseId =
|
||||
runRight_ $ do
|
||||
(_, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMContact Nothing Nothing aPQ False SMSubscribe
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True qInfo bPQ
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True qInfo bPQ
|
||||
sqSecuredJoin <- A.joinConnection bob NRMInteractive 1 aliceId True qInfo "bob's connInfo" bPQ SMSubscribe
|
||||
liftIO $ sqSecuredJoin `shouldBe` False -- joining via contact address connection
|
||||
("", _, A.REQ invId pqSup' _ "bob's connInfo" _ _) <- get alice
|
||||
("", _, A.REQ invId pqSup' _ "bob's connInfo" _) <- get alice
|
||||
liftIO $ pqSup' `shouldBe` PQSupportOn
|
||||
(bobId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption aPQ)
|
||||
bobId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption aPQ)
|
||||
sqSecured' <- acceptContact alice 1 bobId True invId "alice's connInfo" (CR.connPQEncryption aPQ) SMSubscribe
|
||||
liftIO $ sqSecured' `shouldBe` sqSecured
|
||||
("", _, A.CONF confId pqSup'' _ "alice's connInfo") <- get bob
|
||||
@@ -1060,7 +1053,7 @@ runAgentClientContactDRTest_ asyncAccept asyncJoin addrIK useDR bPQ ps = withSmp
|
||||
Nothing -> expectationFailure "address must advertise DR ratchet keys"
|
||||
-- classic (non-DR) join drops the address keys from the request
|
||||
let connReqJoin = if useDR then connReq' else case connReq' of CRContactUri d _ -> CRContactUri d Nothing
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True connReqJoin bPQ
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True connReqJoin bPQ
|
||||
if asyncJoin
|
||||
then do
|
||||
A.joinConnectionAsync bob "join" False aliceId True connReqJoin "bob's connInfo" bPQ SMSubscribe
|
||||
@@ -1068,9 +1061,9 @@ runAgentClientContactDRTest_ asyncAccept asyncJoin addrIK useDR bPQ ps = withSmp
|
||||
else do
|
||||
sqSecuredJoin <- A.joinConnection bob NRMInteractive 1 aliceId True connReqJoin "bob's connInfo" bPQ SMSubscribe
|
||||
liftIO $ sqSecuredJoin `shouldBe` False
|
||||
("", _, A.REQ invId reqPQ _ "bob's connInfo" _ _) <- get alice
|
||||
("", _, A.REQ invId reqPQ _ "bob's connInfo" _) <- get alice
|
||||
liftIO $ reqPQ `shouldBe` PQSupportOn
|
||||
(bobId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
bobId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
if asyncAccept
|
||||
then do
|
||||
acceptContactAsync alice "accept" bobId True invId "alice's connInfo" (CR.connPQEncryption addrIK) SMSubscribe
|
||||
@@ -1100,10 +1093,10 @@ runCreateConnectionDRTest_ asyncNew addrIK bPQ ps = withSmpServer ps $ withAgent
|
||||
liftIO $ case addrKeys_ of
|
||||
Just (_, CR.E2ERatchetParamsUri _ _ _ kem_) -> isJust kem_ `shouldBe` supportPQ (CR.initialPQEncryption False addrIK)
|
||||
Nothing -> expectationFailure "createConnection must advertise DR ratchet keys"
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True connReq bPQ
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True connReq bPQ
|
||||
void $ A.joinConnection bob NRMInteractive 1 aliceId True connReq "bob's connInfo" bPQ SMSubscribe
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _ _) <- get alice
|
||||
(bobId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _) <- get alice
|
||||
bobId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
void $ acceptContact alice 1 bobId True invId "alice's connInfo" (CR.connPQEncryption addrIK) SMSubscribe
|
||||
("", _, A.CONF confId _ _ "alice's connInfo") <- get bob
|
||||
allowConfirmGreet alice bobId bob aliceId confId addrIK pqEnc
|
||||
@@ -1125,10 +1118,10 @@ testContactDRMatrix ps = do
|
||||
|
||||
joinContactDR :: HasCallStack => AgentClient -> AgentClient -> ConnectionRequestUri 'CMContact -> InitialKeys -> PQEncryption -> ExceptT AgentErrorType IO ()
|
||||
joinContactDR alice requester connReq addrIK pqEnc = do
|
||||
(aliceId, _) <- A.prepareConnectionToJoin requester 1 True connReq PQSupportOn
|
||||
aliceId <- A.prepareConnectionToJoin requester 1 True connReq PQSupportOn
|
||||
void $ A.joinConnection requester NRMInteractive 1 aliceId True connReq "bob's connInfo" PQSupportOn SMSubscribe
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _ _) <- get alice
|
||||
(reqId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _) <- get alice
|
||||
reqId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
void $ acceptContact alice 1 reqId True invId "alice's connInfo" (CR.connPQEncryption addrIK) SMSubscribe
|
||||
("", _, A.CONF confId _ _ "alice's connInfo") <- get requester
|
||||
allowConfirmGreet alice reqId requester aliceId confId addrIK pqEnc
|
||||
@@ -1156,7 +1149,7 @@ testAddressKeyRotation ps = withSmpServer ps $ withAgentClients3 $ \alice bob ca
|
||||
joinContactDR alice carol connReq2 addrIK pqEnc
|
||||
void $ A.setConnShortLink alice NRMInteractive addrConnId SCMContact userLinkData Nothing True (Just addrIK)
|
||||
void $ A.setConnShortLink alice NRMInteractive addrConnId SCMContact userLinkData Nothing True (Just addrIK)
|
||||
(aId, _) <- A.prepareConnectionToJoin bob 1 True connReq1 PQSupportOn
|
||||
aId <- A.prepareConnectionToJoin bob 1 True connReq1 PQSupportOn
|
||||
void $ A.joinConnection bob NRMInteractive 1 aId True connReq1 "bob's connInfo" PQSupportOn SMSubscribe
|
||||
get alice =##> \case ("", _, A.ERR _) -> True; _ -> False
|
||||
|
||||
@@ -1199,11 +1192,11 @@ testAddressUpdatePreservesDRKeys ps = withSmpServer ps $ withAgentClients2 $ \al
|
||||
liftIO $ shortLink' `shouldBe` shortLink
|
||||
(_, ContactLinkData _ updated, connReq') <- getConnShortLink bob 1 shortLink
|
||||
liftIO $ ratchetKeys updated `shouldBe` ratchetKeys published
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True connReq' bPQ
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True connReq' bPQ
|
||||
sqSecuredJoin <- A.joinConnection bob NRMInteractive 1 aliceId True connReq' "bob's connInfo" bPQ SMSubscribe
|
||||
liftIO $ sqSecuredJoin `shouldBe` False
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _ _) <- get alice
|
||||
(bobId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _) <- get alice
|
||||
bobId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
_ <- acceptContact alice 1 bobId True invId "alice's connInfo" (CR.connPQEncryption addrIK) SMSubscribe
|
||||
("", _, A.CONF confId _ _ "alice's connInfo") <- get bob
|
||||
allowConfirmGreet alice bobId bob aliceId confId addrIK pqEnc
|
||||
@@ -1223,10 +1216,10 @@ testAcceptContactDRResumeAfterOffline ps = withAgentClients2 $ \alice bob -> do
|
||||
(ccLink@(CCLink _ (Just shortLink)), preparedParams) <- A.prepareConnectionLink alice 1 rootKey linkEntId True Nothing connIK True Nothing
|
||||
_ <- A.createConnectionForLink alice NRMInteractive 1 True ccLink preparedParams (UserContactLinkData userCtData) SMSubscribe
|
||||
(_, _, connReq') <- getConnShortLink bob 1 shortLink
|
||||
(aId, _) <- A.prepareConnectionToJoin bob 1 True connReq' PQSupportOn
|
||||
aId <- A.prepareConnectionToJoin bob 1 True connReq' PQSupportOn
|
||||
_ <- A.joinConnection bob NRMInteractive 1 aId True connReq' "bob's connInfo" PQSupportOn SMSubscribe
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _ _) <- get alice
|
||||
(bId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _) <- get alice
|
||||
bId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption addrIK)
|
||||
pure (bId, invId, aId)
|
||||
("", "", DOWN _ _) <- nGet alice
|
||||
("", "", DOWN _ _) <- nGet bob
|
||||
@@ -1251,12 +1244,12 @@ runAgentClientContactTestPQ3 viaProxy (alice, aPQ) (bob, bPQ) (tom, tPQ) baseId
|
||||
where
|
||||
msgId = subtract baseId . fst
|
||||
connectViaContact b pq qInfo = do
|
||||
(aId, _) <- A.prepareConnectionToJoin b 1 True qInfo pq
|
||||
aId <- A.prepareConnectionToJoin b 1 True qInfo pq
|
||||
sqSecuredJoin <- A.joinConnection b NRMInteractive 1 aId True qInfo "bob's connInfo" pq SMSubscribe
|
||||
liftIO $ sqSecuredJoin `shouldBe` False -- joining via contact address connection
|
||||
("", _, A.REQ invId pqSup' _ "bob's connInfo" _ _) <- get alice
|
||||
("", _, A.REQ invId pqSup' _ "bob's connInfo" _) <- get alice
|
||||
liftIO $ pqSup' `shouldBe` PQSupportOn
|
||||
(bId, _) <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption aPQ)
|
||||
bId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption aPQ)
|
||||
sqSecuredAccept <- acceptContact alice 1 bId True invId "alice's connInfo" (CR.connPQEncryption aPQ) SMSubscribe
|
||||
liftIO $ sqSecuredAccept `shouldBe` True
|
||||
("", _, A.CONF confId pqSup'' _ "alice's connInfo") <- get b
|
||||
@@ -1297,10 +1290,10 @@ testRejectContactRequest :: HasCallStack => IO ()
|
||||
testRejectContactRequest =
|
||||
withAgentClients2 $ \alice bob -> runRight_ $ do
|
||||
(_addrConnId, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMContact Nothing Nothing IKPQOn False SMSubscribe
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
sqSecured <- A.joinConnection bob NRMInteractive 1 aliceId True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sqSecured `shouldBe` False -- joining via contact address connection
|
||||
("", _, A.REQ invId PQSupportOn _ "bob's connInfo" _ _) <- get alice
|
||||
("", _, A.REQ invId PQSupportOn _ "bob's connInfo" _) <- get alice
|
||||
Left (A.CMD PROHIBITED _) <- tryError $ rejectContact alice NRMInteractive 1 invId (Just "no rejection without double ratchet")
|
||||
rejectContact alice NRMInteractive 1 invId Nothing
|
||||
liftIO $ noMessages bob "nothing delivered to bob"
|
||||
@@ -1310,9 +1303,9 @@ testRejectContactRequestDR =
|
||||
withAgentClients2 $ \alice bob -> runRight_ $ do
|
||||
let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData "test user data", ratchetKeys = Nothing}
|
||||
(_addrConnId, CCLink connReq _) <- A.createConnection alice NRMInteractive 1 True True SCMContact (Just userLinkData) Nothing IKPQOn True SMSubscribe
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True connReq PQSupportOn
|
||||
aliceId <- 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" _ _) <- get alice
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _) <- get alice
|
||||
rejectContact alice NRMInteractive 1 invId (Just "not now")
|
||||
("", _, A.RJCT "not now") <- get bob
|
||||
pure ()
|
||||
@@ -1322,9 +1315,9 @@ testRejectContactRequestDRAsync =
|
||||
withAgentClients2 $ \alice bob -> runRight_ $ do
|
||||
let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData "test user data", ratchetKeys = Nothing}
|
||||
(_addrConnId, CCLink connReq _) <- A.createConnection alice NRMInteractive 1 True True SCMContact (Just userLinkData) Nothing IKPQOn True SMSubscribe
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True connReq PQSupportOn
|
||||
aliceId <- 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" _ _) <- get alice
|
||||
("", _, A.REQ invId _ _ "bob's connInfo" _) <- get alice
|
||||
rejectContactAsync alice "1" 1 invId (Just "not now")
|
||||
("", _, A.RJCT "not now") <- get bob
|
||||
pure ()
|
||||
@@ -1437,7 +1430,7 @@ testUpdateConnectionUserId =
|
||||
(connId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
newUserId <- createUser alice False [noAuthSrvCfg testSMPServer] [noAuthSrvCfg testXFTPServer]
|
||||
_ <- changeConnectionUser alice 1 connId newUserId
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
aliceId <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
sqSecured' <- A.joinConnection bob NRMInteractive 1 aliceId True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sqSecured' `shouldBe` True
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice
|
||||
@@ -1596,7 +1589,7 @@ testInvitationErrors ps restart = do
|
||||
b <- getAgentB
|
||||
(bId, cReq) <- withServer1 ps $ runRight $ createConnection a 1 True SCMInvitation Nothing SMSubscribe
|
||||
("", "", DOWN _ [_]) <- nGet a
|
||||
(aId, _) <- runRight $ A.prepareConnectionToJoin b 1 True cReq PQSupportOn
|
||||
aId <- runRight $ A.prepareConnectionToJoin b 1 True cReq PQSupportOn
|
||||
-- fails to secure the queue on testPort
|
||||
BROKER srv (NETWORK _) <- runLeft $ A.joinConnection b NRMInteractive 1 aId True cReq "bob's connInfo" PQSupportOn SMSubscribe
|
||||
(testPort `isSuffixOf` srv) `shouldBe` True
|
||||
@@ -1666,7 +1659,7 @@ testContactErrors ps restart = do
|
||||
b <- getAgentB
|
||||
(contactId, cReq) <- withServer1 ps $ runRight $ createConnection a 1 True SCMContact Nothing SMSubscribe
|
||||
("", "", DOWN _ [_]) <- nGet a
|
||||
(aId, _) <- runRight $ A.prepareConnectionToJoin b 1 True cReq PQSupportOn
|
||||
aId <- runRight $ A.prepareConnectionToJoin b 1 True cReq PQSupportOn
|
||||
-- fails to create queue on testPort2
|
||||
BROKER srv2 (NETWORK _) <- runLeft $ A.joinConnection b NRMInteractive 1 aId True cReq "bob's connInfo" PQSupportOn SMSubscribe
|
||||
(testPort2 `isSuffixOf` srv2) `shouldBe` True
|
||||
@@ -1688,10 +1681,10 @@ testContactErrors ps restart = do
|
||||
Right r -> error $ "unexpected result " <> show r
|
||||
Left _ -> putStrLn "retrying send" >> threadDelay 200000 >> loopSend
|
||||
loopSend
|
||||
("", _, A.REQ invId PQSupportOn _ "bob's connInfo" _ _) <- get a
|
||||
("", _, A.REQ invId PQSupportOn _ "bob's connInfo" _) <- get a
|
||||
pure invId
|
||||
("", "", DOWN _ [_]) <- nGet a
|
||||
(bId, _) <- runRight $ A.prepareConnectionToAccept a 1 True invId PQSupportOn
|
||||
bId <- runRight $ A.prepareConnectionToAccept a 1 True invId PQSupportOn
|
||||
withServer2 ps $ do
|
||||
("", "", UP _ [_]) <- nGet b''
|
||||
let loopSecure = do
|
||||
@@ -1771,7 +1764,7 @@ testInvitationShortLink viaProxy a b =
|
||||
|
||||
testJoinConn_ :: Bool -> Bool -> AgentClient -> ConnId -> AgentClient -> ConnectionRequestUri c -> ExceptT AgentErrorType IO ()
|
||||
testJoinConn_ viaProxy sndSecure a bId b connReq = do
|
||||
(aId, _) <- A.prepareConnectionToJoin b 1 True connReq PQSupportOn
|
||||
aId <- A.prepareConnectionToJoin b 1 True connReq PQSupportOn
|
||||
sndSecure' <- A.joinConnection b NRMInteractive 1 aId True connReq "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sndSecure' `shouldBe` sndSecure
|
||||
("", _, CONF confId _ "bob's connInfo") <- get a
|
||||
@@ -1781,6 +1774,14 @@ 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"
|
||||
@@ -1791,7 +1792,7 @@ testInvitationShortLinkAsync viaProxy a b = do
|
||||
connReq' `shouldBe` connReq
|
||||
linkUserData connData' `shouldBe` userData
|
||||
runRight $ do
|
||||
(aId, _) <- A.prepareConnectionToJoin b 1 True connReq PQSupportOn
|
||||
aId <- A.prepareConnectionToJoin b 1 True connReq PQSupportOn
|
||||
A.joinConnectionAsync b "123" False aId True connReq "bob's connInfo" PQSupportOn SMSubscribe
|
||||
get b =##> \case ("123", c, JOINED sndSecure) -> c == aId && sndSecure; _ -> False
|
||||
("", _, CONF confId _ "bob's connInfo") <- get a
|
||||
@@ -1833,7 +1834,7 @@ testContactShortLink viaProxy a b =
|
||||
(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
|
||||
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
|
||||
@@ -1886,7 +1887,7 @@ testAddContactShortLink viaProxy a b =
|
||||
(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
|
||||
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
|
||||
@@ -2045,7 +2046,7 @@ testPrepareCreateConnectionLink ps = withSmpServer ps $ withAgentClients2 $ \a b
|
||||
(bId, sndSecure) <- joinConnection b 1 True connReq' "bob's connInfo" SMSubscribe
|
||||
liftIO $ sndSecure `shouldBe` False
|
||||
("", _, REQ invId _ "bob's connInfo") <- get a
|
||||
(aId, _) <- A.prepareConnectionToAccept a 1 True invId PQSupportOn
|
||||
aId <- A.prepareConnectionToAccept a 1 True invId PQSupportOn
|
||||
sndSecure' <- acceptContact a 1 aId True invId "alice's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sndSecure' `shouldBe` True
|
||||
("", _, CONF confId _ "alice's connInfo") <- get b
|
||||
@@ -2483,49 +2484,6 @@ testExpireManyMessagesQuota (t, msType) = withSmpServerConfigOn t cfg' testPort
|
||||
where
|
||||
cfg' = updateCfg (cfgMS msType) $ \cfg_ -> cfg_ {msgQueueQuota = 1, maxJournalMsgCount = 2}
|
||||
|
||||
testJoinFullContactAsync :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
|
||||
testJoinFullContactAsync (t, msType) = withSmpServerConfigOn t cfg' testPort $ \_ -> do
|
||||
(contactId, qInfo) <- fillContactAddress
|
||||
withAgent 2 agentCfg {commandQuotaRetryInterval = fastRetryInterval} initAgentServers testDB2 $ \bob -> do
|
||||
aliceId <- runRight $ do
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
A.joinConnectionAsync bob "2" False aliceId True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
pure aliceId
|
||||
threadDelay 500000
|
||||
noMessages bob "joining should be retried while the address is full"
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \alice -> runRight_ $ do
|
||||
subscribeConnection alice contactId
|
||||
("", _, A.REQ _ _ _ "carol's connInfo" _ _) <- get alice
|
||||
get bob =##> \case ("2", c, JOINED False) -> c == aliceId; _ -> False
|
||||
("", _, A.REQ _ _ _ "bob's connInfo" _ _) <- get alice
|
||||
pure ()
|
||||
where
|
||||
cfg' = updateCfg (cfgMS msType) $ \cfg_ -> cfg_ {msgQueueQuota = 1, maxJournalMsgCount = 2}
|
||||
|
||||
testJoinFullContactAsyncExpire :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
|
||||
testJoinFullContactAsyncExpire (t, msType) = withSmpServerConfigOn t cfg' testPort $ \_ -> do
|
||||
(_, qInfo) <- fillContactAddress
|
||||
withAgent 2 agentCfg {quotaExceededTimeout = 2, commandQuotaRetryInterval = fastRetryInterval} initAgentServers testDB2 $ \bob -> do
|
||||
aliceId <- runRight $ do
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
A.joinConnectionAsync bob "2" False aliceId True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
pure aliceId
|
||||
threadDelay 500000
|
||||
noMessages bob "joining should be retried until quota exceeded timeout"
|
||||
get bob =##> \case ("2", c, ERR (SMP _ QUOTA)) -> c == aliceId; _ -> False
|
||||
where
|
||||
cfg' = updateCfg (cfgMS msType) $ \cfg_ -> cfg_ {msgQueueQuota = 1, maxJournalMsgCount = 2}
|
||||
|
||||
fillContactAddress :: HasCallStack => IO (ConnId, ConnectionRequestUri 'CMContact)
|
||||
fillContactAddress = do
|
||||
(contactId, qInfo) <- withAgent 1 agentCfg initAgentServers testDB $ \alice -> runRight $ do
|
||||
(contactId, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMContact Nothing Nothing IKPQOn False SMSubscribe
|
||||
pure (contactId, qInfo)
|
||||
withAgent 3 agentCfg initAgentServers testDB3 $ \carol -> runRight_ $ do
|
||||
(aliceId, _) <- A.prepareConnectionToJoin carol 1 True qInfo PQSupportOn
|
||||
void $ A.joinConnection carol NRMInteractive 1 aliceId True qInfo "carol's connInfo" PQSupportOn SMSubscribe
|
||||
pure (contactId, qInfo)
|
||||
|
||||
testDropMsgAfterRcvAttempts :: HasCallStack => (ASrvTransport, AStoreType) -> IO ()
|
||||
testDropMsgAfterRcvAttempts ps =
|
||||
withSmpServerStoreLogOn ps testPort $ \_ -> do
|
||||
@@ -2740,117 +2698,6 @@ 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"
|
||||
@@ -2902,7 +2749,7 @@ makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn
|
||||
makeConnectionForUsers_ :: HasCallStack => PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnectionForUsers_ pqSupport alice aliceUserId bob bobUserId = do
|
||||
(bobId, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive aliceUserId True True SCMInvitation Nothing Nothing (IKLinkPQ pqSupport) False SMSubscribe
|
||||
(aliceId, _) <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport
|
||||
aliceId <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport
|
||||
sqSecured' <- A.joinConnection bob NRMInteractive bobUserId aliceId True qInfo "bob's connInfo" pqSupport SMSubscribe
|
||||
liftIO $ sqSecured' `shouldBe` True
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice
|
||||
@@ -3184,7 +3031,7 @@ testAsyncCommands alice bob baseId =
|
||||
createConnectionAsync alice "1" bobId True SCMInvitation IKPQOn False SMSubscribe
|
||||
("1", bobId', INV (ACR _ qInfo)) <- get alice
|
||||
liftIO $ bobId' `shouldBe` bobId
|
||||
(aliceId, _) <- prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
aliceId <- prepareConnectionToJoin bob 1 True qInfo PQSupportOn
|
||||
joinConnectionAsync bob "2" False aliceId True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
("2", aliceId', JOINED sqSecured') <- get bob
|
||||
liftIO $ do
|
||||
@@ -3254,7 +3101,7 @@ testSetConnShortLinkAsync ps = withAgentClients2 $ \alice bob ->
|
||||
-- complete connection via contact address
|
||||
(aliceId, _) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
("", _, REQ invId _ "bob's connInfo") <- get alice
|
||||
(bobId, _) <- A.prepareConnectionToAccept alice 1 True invId PQSupportOn
|
||||
bobId <- A.prepareConnectionToAccept alice 1 True invId PQSupportOn
|
||||
_ <- acceptContact alice 1 bobId True invId "alice's connInfo" PQSupportOn SMSubscribe
|
||||
("", _, CONF confId _ "alice's connInfo") <- get bob
|
||||
allowConnection bob aliceId confId "bob's connInfo"
|
||||
@@ -3282,7 +3129,7 @@ testGetConnShortLinkAsync ps = withAgentClients2 $ \alice bob ->
|
||||
liftIO $ aliceId' `shouldBe` aliceId
|
||||
-- complete connection
|
||||
("", _, REQ invId _ "bob's connInfo") <- get alice
|
||||
(bobId, _) <- A.prepareConnectionToAccept alice 1 True invId PQSupportOn
|
||||
bobId <- A.prepareConnectionToAccept alice 1 True invId PQSupportOn
|
||||
_ <- acceptContact alice 1 bobId True invId "alice's connInfo" PQSupportOn SMSubscribe
|
||||
("", _, CONF confId _ "alice's connInfo") <- get bob
|
||||
allowConnection bob aliceId confId "bob's connInfo"
|
||||
@@ -3312,7 +3159,7 @@ testAcceptContactAsync alice bob baseId =
|
||||
(aliceId, sqSecuredJoin) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
liftIO $ sqSecuredJoin `shouldBe` False -- joining via contact address connection
|
||||
("", _, REQ invId _ "bob's connInfo") <- get alice
|
||||
(bobId, _) <- prepareConnectionToAccept alice 1 True invId PQSupportOn
|
||||
bobId <- prepareConnectionToAccept alice 1 True invId PQSupportOn
|
||||
acceptContactAsync alice "1" bobId True invId "alice's connInfo" PQSupportOn SMSubscribe
|
||||
get alice =##> \case ("1", c, JOINED sqSecured') -> c == bobId && sqSecured' == True; _ -> False
|
||||
("", _, CONF confId _ "alice's connInfo") <- get bob
|
||||
@@ -3593,7 +3440,7 @@ testJoinConnectionAsyncReplyError ps@(t, ASType qsType _) = do
|
||||
createConnectionAsync a "1" bId True SCMInvitation IKPQOn False SMSubscribe
|
||||
("1", bId', INV (ACR _ qInfo)) <- get a
|
||||
liftIO $ bId' `shouldBe` bId
|
||||
(aId, _) <- prepareConnectionToJoin b 1 True qInfo PQSupportOn
|
||||
aId <- prepareConnectionToJoin b 1 True qInfo PQSupportOn
|
||||
joinConnectionAsync b "2" False aId True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ threadDelay 500000
|
||||
ConnectionStats {rcvQueuesInfo = [], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId
|
||||
@@ -3733,7 +3580,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 aCfg initAgentServers testDB $ \a ->
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a ->
|
||||
withAgent 2 agentCfg bServers testDB2 $ \b -> do
|
||||
(aId, bId) <- withSmpServerStoreLogOn ps testPort $ \_ -> runRight $ do
|
||||
(aId, bId) <- makeConnection a b
|
||||
@@ -3749,10 +3596,7 @@ 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)
|
||||
@@ -3772,69 +3616,6 @@ 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)
|
||||
|
||||
@@ -4242,23 +4023,13 @@ testServerInformation =
|
||||
serverCountry = Nothing
|
||||
}
|
||||
|
||||
testConnectionVerifyCodes :: HasCallStack => IO ()
|
||||
testConnectionVerifyCodes =
|
||||
testRatchetAdHash :: HasCallStack => IO ()
|
||||
testRatchetAdHash =
|
||||
withAgentClients2 $ \a b -> runRight_ $ do
|
||||
(aId, bId) <- makeConnection a b
|
||||
codes1 <- getConnectionVerifyCodes a bId
|
||||
codes2 <- getConnectionVerifyCodes b aId
|
||||
liftIO $ do
|
||||
codes1 `shouldBe` codes2
|
||||
codePQ codes1 `shouldNotBe` Nothing
|
||||
liftIO $ withTransaction (store $ agentEnv a) $ \db ->
|
||||
DB.execute_ db "UPDATE ratchets SET rc_verify_code_ad = NULL, rc_verify_code_pq = NULL"
|
||||
codes1' <- getConnectionVerifyCodes a bId
|
||||
liftIO $ codes1' `shouldBe` codes1
|
||||
liftIO $ withTransaction (store $ agentEnv a) $ \db ->
|
||||
DB.execute_ db "UPDATE ratchets SET ratchet_state = NULL"
|
||||
codes1'' <- getConnectionVerifyCodes a bId
|
||||
liftIO $ codes1'' `shouldBe` codes1
|
||||
ad1 <- getConnectionRatchetAdHash a bId
|
||||
ad2 <- getConnectionRatchetAdHash b aId
|
||||
liftIO $ ad1 `shouldBe` ad2
|
||||
|
||||
testDeliveryReceipts :: HasCallStack => IO ()
|
||||
testDeliveryReceipts =
|
||||
@@ -4847,7 +4618,8 @@ testServerMultipleIdentities =
|
||||
testE2ERatchetParams12
|
||||
|
||||
testWaitForUserNetwork :: IO ()
|
||||
testWaitForUserNetwork = withAgent 1 aCfg initAgentServers testDB $ \a -> do
|
||||
testWaitForUserNetwork = do
|
||||
a <- getSMPAgentClient' 1 aCfg initAgentServers testDB
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False
|
||||
networkDelay a 100000
|
||||
@@ -4864,7 +4636,8 @@ testWaitForUserNetwork = withAgent 1 aCfg initAgentServers testDB $ \a -> do
|
||||
aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0}
|
||||
|
||||
testDoNotResetOnlineToOffline :: IO ()
|
||||
testDoNotResetOnlineToOffline = withAgent 1 aCfg initAgentServers testDB $ \a -> do
|
||||
testDoNotResetOnlineToOffline = do
|
||||
a <- getSMPAgentClient' 1 aCfg initAgentServers testDB
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNWifi False
|
||||
networkDelay a 100000
|
||||
@@ -4885,7 +4658,8 @@ testDoNotResetOnlineToOffline = withAgent 1 aCfg initAgentServers testDB $ \a ->
|
||||
aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0.1}
|
||||
|
||||
testResumeMultipleThreads :: IO ()
|
||||
testResumeMultipleThreads = withAgent 1 aCfg initAgentServers testDB $ \a -> do
|
||||
testResumeMultipleThreads = do
|
||||
a <- getSMPAgentClient' 1 aCfg initAgentServers testDB
|
||||
noNetworkDelay a
|
||||
setUserNetworkInfo a $ UserNetworkInfo UNNone False
|
||||
vs <-
|
||||
@@ -4931,7 +4705,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 $ SMP.unMsgId smId) == srvMsgId && mId == msgId
|
||||
c == aliceId && decodeLatin1 (B64.encode smId) == srvMsgId && mId == msgId
|
||||
_ -> False
|
||||
ackMessage bob aliceId msgId Nothing
|
||||
liftIO $ threadDelay 200000
|
||||
|
||||
@@ -230,7 +230,7 @@ testNotificationToken apns = do
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}} <-
|
||||
getMockNotification apns tkn
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData' .-> "nonce"
|
||||
nonce' <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
vNonce <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData' .-> "nonce"
|
||||
nonce' <- C.cbNonce <$> 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.unsafeCbNonce <$> ntfData .-> "nonce"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
message <- ntfData .-> "message"
|
||||
pure (nonce, message)
|
||||
_ -> error "bad notification"
|
||||
|
||||
@@ -15,6 +15,7 @@ module AgentTests.ResolveNameTests (resolveNameTests) where
|
||||
import AgentTests.FunctionalAPITests (withAgent)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.IORef (IORef, readIORef)
|
||||
import Data.List (isInfixOf)
|
||||
import Network.HTTP.Types (Status, status200, status404, status502)
|
||||
import NamesResolverServer (memCfg, memCfg2, memProxyCfg, withNames)
|
||||
@@ -22,7 +23,10 @@ import qualified NamesResolverServer as NRS
|
||||
import SMPAgentClient
|
||||
import SMPClient
|
||||
import SMPNamesTests (availableBody, registeredBody, testNameRecord)
|
||||
import Simplex.Messaging.Agent (resolveSimplexName)
|
||||
import Data.Text (Text)
|
||||
import Simplex.Messaging.Agent (ownedSimplexNames, resolveSimplexName)
|
||||
import Simplex.Messaging.Encoding.String (strDecode)
|
||||
import Simplex.Messaging.Eth.Address (Address)
|
||||
import Simplex.Messaging.Agent.Client (AgentClient)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (InitialAgentServers (..), ServerCfg, ServerRoles (..), presetServerCfg)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..))
|
||||
@@ -44,10 +48,14 @@ oneSrv :: ServerCfg 'SMP.PSMP -> InitialAgentServers
|
||||
oneSrv cfg_ = (initAgentServersProxy_ SPMNever SPFProhibit) {smp = [(1, [cfg_])]}
|
||||
|
||||
withDirectResolver :: (Status, LB.ByteString) -> (AgentClient -> IO a) -> IO a
|
||||
withDirectResolver (st, body) k =
|
||||
NRS.withResolverServer (NRS.resolveResp st body) $ \port _ ->
|
||||
withDirectResolver resp k = withDirectResolverReqs resp $ \c _ -> k c
|
||||
|
||||
-- | As 'withDirectResolver', with the requests the resolver was asked for.
|
||||
withDirectResolverReqs :: (Status, LB.ByteString) -> (AgentClient -> IORef [[Text]] -> IO a) -> IO a
|
||||
withDirectResolverReqs (st, body) k =
|
||||
NRS.withResolverServer (NRS.resolveResp st body) $ \port reqs ->
|
||||
withSmpServerConfigOn (transport @TLS) (withNames port memCfg) testPort $ \_ ->
|
||||
withAgent 1 agentCfg (oneSrv (nameSrvCfg testSMPServer)) testDB k
|
||||
withAgent 1 agentCfg (oneSrv (nameSrvCfg testSMPServer)) testDB $ \c -> k c reqs
|
||||
|
||||
withProxyAndResolver :: (Status, LB.ByteString) -> (AgentClient -> IO a) -> IO a
|
||||
withProxyAndResolver (st, body) k =
|
||||
@@ -87,6 +95,22 @@ resolveNameTests = do
|
||||
it "returns NameRecord" testDirectSuccess
|
||||
describe "name availability" $
|
||||
it "an unregistered name answers as available" testAvailSuccess
|
||||
describe "owned names" $
|
||||
it "a relay that cannot answer is not the end of the lookup" testOwnedRetriesRelays
|
||||
|
||||
-- | A relay that cannot answer must not end a scan at the account it was asked about.
|
||||
testOwnedRetriesRelays :: HasCallStack => IO ()
|
||||
testOwnedRetriesRelays =
|
||||
withDirectResolverReqs (status502, "{}") $ \c reqs -> do
|
||||
r <- runExceptT $ ownedSimplexNames c NRMInteractive 1 [] testAddr 0
|
||||
case r of
|
||||
Left (SMP _ (SMP.NAME _)) -> pure ()
|
||||
_ -> expectationFailure $ "expected Left (SMP _ (NAME ..)), got: " <> show r
|
||||
asked <- readIORef reqs
|
||||
length [q | q@("v2" : "owned-by" : _) <- asked] `shouldBe` 3
|
||||
|
||||
testAddr :: Address
|
||||
testAddr = either error id $ strDecode "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
|
||||
|
||||
testAvailSuccess :: HasCallStack => IO ()
|
||||
testAvailSuccess =
|
||||
|
||||
@@ -20,13 +20,12 @@ import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.MVar
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (SomeException)
|
||||
import Control.Monad (forM_, replicateM_)
|
||||
import Control.Monad (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
|
||||
@@ -52,7 +51,6 @@ 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
|
||||
@@ -137,8 +135,6 @@ 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
|
||||
@@ -513,7 +509,7 @@ hw = encodeUtf8 "Hello world!"
|
||||
ts :: UTCTime
|
||||
ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
|
||||
|
||||
mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> ByteString -> MsgHash -> RcvMsgData
|
||||
mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> BrokerId -> MsgHash -> RcvMsgData
|
||||
mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
|
||||
RcvMsgData
|
||||
{ internalRcvId,
|
||||
@@ -522,7 +518,7 @@ mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
|
||||
{ integrity = MsgOk,
|
||||
recipient = (unId internalId, ts),
|
||||
sndMsgId = externalSndId,
|
||||
broker = (SMP.unsafeMsgId $ mconcat (replicate 24 brokerId), ts),
|
||||
broker = (brokerId, ts),
|
||||
pqEncryption = CR.PQEncOn
|
||||
},
|
||||
msgType = AM_A_MSG_,
|
||||
@@ -603,42 +599,6 @@ 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'
|
||||
@@ -688,7 +648,7 @@ hasMigrations :: DBStore -> Expectation
|
||||
hasMigrations st = getMigrations st `shouldReturn` True
|
||||
|
||||
errorGettingMigrations :: DBStore -> Expectation
|
||||
errorGettingMigrations st = getMigrations st `shouldThrow` \(e :: SomeException) -> "withTransaction: database closed" `isInfixOf` show e
|
||||
errorGettingMigrations st = getMigrations st `shouldThrow` \(e :: SomeException) -> "ErrorMisuse" `isInfixOf` show e
|
||||
|
||||
testGetPendingQueueMsg :: DBStore -> Expectation
|
||||
testGetPendingQueueMsg st = do
|
||||
@@ -783,11 +743,9 @@ testGetNextRcvChunkToDownload st = do
|
||||
withTransaction st $ \db -> do
|
||||
Right Nothing <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
|
||||
Right fId1 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
Right _ <- startPreparedRcvFile db fId1
|
||||
Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1"
|
||||
Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True
|
||||
Right _ <- startPreparedRcvFile db fId2
|
||||
|
||||
Left e <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
show e `shouldContain` "ConversionFailed"
|
||||
@@ -825,7 +783,6 @@ testGetNextSndFileToPrepare st = do
|
||||
-- Right _ <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing
|
||||
-- DB.execute_ db "UPDATE snd_files SET status = 'new', num_recipients = 'bad' WHERE snd_file_id = 1"
|
||||
Right fId2 <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing Nothing
|
||||
Right _ <- startPreparedSndFile db fId2
|
||||
DB.execute_ db "UPDATE snd_files SET status = 'new' WHERE snd_file_id = 2"
|
||||
|
||||
-- Left e <- getNextSndFileToPrepare db 86400
|
||||
|
||||
@@ -48,7 +48,6 @@ 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
|
||||
|
||||
testVerifySchemaDump :: IO ()
|
||||
testVerifySchemaDump = do
|
||||
@@ -115,30 +114,12 @@ 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'
|
||||
|
||||
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",
|
||||
-- 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"
|
||||
"m20250322_short_links"
|
||||
]
|
||||
|
||||
getSchema :: FilePath -> FilePath -> IO String
|
||||
|
||||
@@ -71,7 +71,9 @@ initServers =
|
||||
}
|
||||
|
||||
testChooseDifferentOperator :: IO ()
|
||||
testChooseDifferentOperator = withAgent 1 agentCfg initServers testDB $ \c -> runRight_ $ do
|
||||
testChooseDifferentOperator = do
|
||||
c <- getSMPAgentClient' 1 agentCfg initServers testDB
|
||||
runRight_ $ do
|
||||
-- chooses the only operator with storage role
|
||||
srv1 <- withAgentEnv c $ getNextServer c 1 storageSrvs []
|
||||
liftIO $ srv1 == testOp1Srv1 || srv1 == testOp1Srv2 `shouldBe` True
|
||||
|
||||
@@ -14,13 +14,11 @@ 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
|
||||
@@ -42,8 +40,6 @@ 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
|
||||
@@ -170,26 +166,6 @@ 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
|
||||
@@ -207,7 +183,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) (Just nonce) tForAuth
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ True (Just rpKey) nonce tForAuth
|
||||
|
||||
randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg)
|
||||
randomSUBCmd c = do
|
||||
@@ -226,7 +202,7 @@ randomNMSGCmd :: SystemTime -> IO (Transmission BrokerMsg)
|
||||
randomNMSGCmd ts = do
|
||||
g <- C.newRandom
|
||||
nId <- atomically $ C.randomBytes 24 g
|
||||
msgId <- atomically $ randomMsgId 24 g
|
||||
msgId <- atomically $ C.randomBytes 24 g
|
||||
(k, pk) <- atomically $ C.generateKeyPair g
|
||||
nonce <- atomically $ C.randomCbNonce g
|
||||
let msgMeta = NMsgMeta {msgId, msgTs = ts}
|
||||
@@ -245,7 +221,7 @@ randomMSG = do
|
||||
g <- C.newRandom
|
||||
corrId <- atomically $ C.randomBytes 24 g
|
||||
rId <- atomically $ C.randomBytes 24 g
|
||||
msgId <- atomically $ randomMsgId 24 g
|
||||
msgId <- atomically $ C.randomBytes 24 g
|
||||
msg <- atomically $ C.randomBytes maxMessageLength g
|
||||
pure (CorrId corrId, EntityId rId, MSG RcvMessage {msgId, msgBody = EncRcvMsgBody msg})
|
||||
|
||||
@@ -259,9 +235,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) (Just nonce) tForAuth
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ False (Just spKey) nonce tForAuth
|
||||
|
||||
testTHandleParams :: ByteString -> THandleParams SMPVersion p
|
||||
testTHandleParams :: ByteString -> THandleParams SMPVersion 'TClient
|
||||
testTHandleParams sessionId =
|
||||
THandleParams
|
||||
{ sessionId,
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
|
||||
module CoreTests.CryptoTests (cryptoTests) where
|
||||
|
||||
import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (bracket, evaluate)
|
||||
import Control.Exception (evaluate)
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
@@ -23,12 +22,9 @@ import Data.Time.Clock (UTCTime (..))
|
||||
import qualified Data.Text.Lazy as LT
|
||||
import qualified Data.Text.Lazy.Encoding as LE
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word8)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.CertificateStore as XS
|
||||
import qualified Data.X509.Validation as XV
|
||||
import Foreign (FunPtr, allocaBytes, fillBytes, freeHaskellFunPtr, nullPtr)
|
||||
import Foreign.C.Types (CInt, CSize (..))
|
||||
import qualified SMPClient
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
@@ -36,12 +32,9 @@ import Simplex.Messaging.Crypto.BBS
|
||||
import Simplex.Messaging.Crypto.Entitlement
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI (c_sntrup761_keypair)
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG (RNGFunc)
|
||||
import Simplex.Messaging.Encoding (Large (..), smpDecode, smpEncode)
|
||||
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
|
||||
import Simplex.Messaging.Transport.Client
|
||||
import System.Timeout (timeout)
|
||||
import Test.Hspec hiding (fit, it)
|
||||
import Test.Hspec.QuickCheck (modifyMaxSuccess)
|
||||
import Test.QuickCheck hiding (Large)
|
||||
@@ -120,7 +113,6 @@ cryptoTests = do
|
||||
describe "sntrup761" $ do
|
||||
it "should enc/dec key" testSNTRUP761
|
||||
it "should reject malformed KEM encodings" testSNTRUP761RejectsMalformedEncodings
|
||||
it "should fail key generation with degenerate RNG" testSNTRUP761KeypairDegenerateRNG
|
||||
describe "BBS+" $ do
|
||||
it "should sign and verify" testBBSSignVerify
|
||||
it "should derive public key from secret key" testBBSPublicKeyDerivation
|
||||
@@ -306,26 +298,6 @@ testSNTRUP761 = do
|
||||
KEMSharedKey k' <- sntrup761Dec c sk
|
||||
k' `shouldBe` k
|
||||
|
||||
foreign import ccall "wrapper"
|
||||
mkRNGFunc :: RNGFunc -> IO (FunPtr RNGFunc)
|
||||
|
||||
testSNTRUP761KeypairDegenerateRNG :: IO ()
|
||||
testSNTRUP761KeypairDegenerateRNG = do
|
||||
-- constant byte 0 draws invertible g = -(1 + x + ... + x^760), byte 0x20 draws g = 0
|
||||
keypairWithConstantRNG 0 `shouldReturn` Just 0
|
||||
keypairWithConstantRNG 0x20 `shouldReturn` Just (-1)
|
||||
where
|
||||
keypairWithConstantRNG :: Word8 -> IO (Maybe CInt)
|
||||
keypairWithConstantRNG b = do
|
||||
result <- newEmptyMVar
|
||||
-- timeout cannot interrupt a foreign call, so the call runs in another thread
|
||||
_ <- forkIO $
|
||||
bracket (mkRNGFunc $ \_ sz buf -> fillBytes buf b (fromIntegral sz)) freeHaskellFunPtr $ \rng ->
|
||||
allocaBytes c_SNTRUP761_PUBLICKEY_SIZE $ \pkPtr ->
|
||||
allocaBytes c_SNTRUP761_SECRETKEY_SIZE $ \skPtr ->
|
||||
c_sntrup761_keypair pkPtr skPtr nullPtr rng >>= putMVar result
|
||||
timeout 10000000 $ takeMVar result
|
||||
|
||||
testSNTRUP761RejectsMalformedEncodings :: IO ()
|
||||
testSNTRUP761RejectsMalformedEncodings = do
|
||||
smpDecode @KEMPublicKey (smpEncode $ Large shortPublicKey) `shouldSatisfy` isLeft
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- | Tests for secp256k1, BIP-39, BIP-32, Keccak-256 and EIP-55, checked against published vectors rather than against our own output.
|
||||
module CoreTests.EthCryptoTests (ethCryptoTests) where
|
||||
|
||||
import Control.Concurrent.STM (atomically)
|
||||
import Control.Monad (forM_)
|
||||
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.List (nub)
|
||||
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 qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Eth.Address
|
||||
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
|
||||
import Simplex.Messaging.Eth.Keccak (keccak256)
|
||||
import Test.Hspec hiding (fit, it)
|
||||
import Util
|
||||
|
||||
ethCryptoTests :: Spec
|
||||
ethCryptoTests = do
|
||||
describe "Keccak-256" keccakTests
|
||||
describe "secp256k1" secp256k1Tests
|
||||
describe "BIP-39" bip39Tests
|
||||
describe "BIP-32" bip32Tests
|
||||
describe "BIP-44 derivation" derivationTests
|
||||
describe "EIP-55 addresses" eip55Tests
|
||||
|
||||
-- helpers
|
||||
|
||||
hx :: ByteString -> ByteString
|
||||
hx s = either (const $ error $ "bad hex literal: " <> BC.unpack s) id $ BAE.convertFromBase BAE.Base16 s
|
||||
|
||||
toHex :: ByteString -> ByteString
|
||||
toHex = BAE.convertToBase BAE.Base16
|
||||
|
||||
right :: Either String a -> a
|
||||
right = either (error . ("unexpected Left: " <>)) id
|
||||
|
||||
hardened' :: Word32 -> Word32
|
||||
hardened' = B32.hardened
|
||||
|
||||
keccakTests :: Spec
|
||||
keccakTests = do
|
||||
it "hashes the empty string" $
|
||||
toHex (keccak256 "") `shouldBe` "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
|
||||
it "hashes abc" $
|
||||
toHex (keccak256 "abc") `shouldBe` "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"
|
||||
it "is Keccak-256, not SHA3-256" $
|
||||
-- SHA3-256 of the empty string, which differs only in the padding byte
|
||||
toHex (keccak256 "") `shouldNotBe` "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"
|
||||
|
||||
secp256k1Tests :: Spec
|
||||
secp256k1Tests = do
|
||||
it "derives the known address for a known key" $
|
||||
show (addressFromPrivateKey testKey) `shouldBe` "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"
|
||||
it "serializes a public key in both SEC1 forms" $ do
|
||||
let pk = S.secp256k1PublicKey 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
|
||||
B.head comp `shouldSatisfy` (`elem` [0x02, 0x03])
|
||||
-- both forms carry the same x, and the compressed prefix is y's parity
|
||||
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)
|
||||
it "rejects a zero private key" $
|
||||
isLeft (S.mkPrivateKey (B.replicate 32 0)) `shouldBe` True
|
||||
it "rejects a private key at the group order" $
|
||||
isLeft (S.mkPrivateKey (hx "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")) `shouldBe` True
|
||||
it "rejects a short private key" $
|
||||
isLeft (S.mkPrivateKey (B.replicate 31 1)) `shouldBe` True
|
||||
it "adds a tweak to a private key" $
|
||||
(toHex . S.unPrivateKey <$> S.privateKeyTweakAdd testKey (B.replicate 31 0 <> B.singleton 1))
|
||||
`shouldBe` Just "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362319"
|
||||
where
|
||||
testKey = right $ S.mkPrivateKey (hx "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318")
|
||||
|
||||
bip39Tests :: Spec
|
||||
bip39Tests = do
|
||||
describe "official English vectors" $
|
||||
forM_ (zip [0 :: Int ..] bip39Vectors) $ \(i, (entHex, phrase, seedHex)) ->
|
||||
it ("vector " <> show i) $ do
|
||||
let m = right $ B39.entropyToMnemonic (hx entHex)
|
||||
p = right $ B39.parseMnemonic phrase
|
||||
B39.mnemonicPhrase m `shouldBe` phrase
|
||||
toHex (B39.mnemonicToEntropy p) `shouldBe` entHex
|
||||
toHex (B39.mnemonicToSeed p "TREZOR") `shouldBe` seedHex
|
||||
it "embeds the upstream wordlist, unchanged" $
|
||||
-- sha256 of bitcoin/bips/bip-0039/english.txt
|
||||
C.sha256Hash (BC.unlines englishWordList)
|
||||
`shouldBe` hx "2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda"
|
||||
it "maps strengths to word counts" $
|
||||
map B39.strengthWordCount [minBound .. maxBound] `shouldBe` [12, 15, 18, 21, 24]
|
||||
it "rejects a bad checksum" $
|
||||
B39.parseMnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
|
||||
`shouldSatisfy` isLeft
|
||||
it "rejects a word outside the list" $
|
||||
B39.parseMnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon simplex"
|
||||
`shouldSatisfy` isLeft
|
||||
it "rejects an empty phrase, counting the words" $
|
||||
B39.parseMnemonic "" `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.parseMnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about."
|
||||
`shouldBe` Left "Failed reading: mnemonic: not in wordlist: about."
|
||||
it "rejects a wrong word count" $
|
||||
B39.parseMnemonic "abandon abandon about" `shouldSatisfy` isLeft
|
||||
it "accepts a capitalised phrase and normalises it" $
|
||||
(B39.mnemonicPhrase <$> B39.parseMnemonic "Abandon ABANDON abandon abandon abandon abandon abandon abandon abandon abandon abandon About")
|
||||
`shouldBe` Right canonicalPhrase
|
||||
it "accepts extra whitespace" $
|
||||
B39.parseMnemonic " abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about "
|
||||
`shouldBe` B39.parseMnemonic canonicalPhrase
|
||||
it "rejects an invalid entropy size" $
|
||||
B39.entropyToMnemonic (B.replicate 17 0) `shouldSatisfy` isLeft
|
||||
it "generates mnemonics that parse back" $ do
|
||||
g <- C.newRandom
|
||||
forM_ [minBound .. maxBound] $ \s -> do
|
||||
m <- atomically $ B39.randomMnemonic s g
|
||||
length (B39.mnemonicWords m) `shouldBe` B39.strengthWordCount s
|
||||
B39.parseMnemonic (B39.mnemonicPhrase m) `shouldBe` Right m
|
||||
it "redacts the mnemonic in Show" $ do
|
||||
g <- C.newRandom
|
||||
m <- atomically $ B39.randomMnemonic B39.MS128 g
|
||||
show m `shouldBe` "Mnemonic <12 words, redacted>"
|
||||
|
||||
canonicalPhrase :: ByteString
|
||||
canonicalPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
bip32Tests :: Spec
|
||||
bip32Tests = do
|
||||
describe "spec test vector 1" $
|
||||
forM_ vector1 $ \(name, path, keyHex, ccHex) ->
|
||||
it name $ do
|
||||
let xk = right $ B32.derivePath master1 path
|
||||
toHex (S.unPrivateKey $ B32.xkKey xk) `shouldBe` keyHex
|
||||
toHex (B32.xkChainCode xk) `shouldBe` ccHex
|
||||
it "spec test vector 2, chain m" $ do
|
||||
toHex (S.unPrivateKey $ B32.xkKey master2) `shouldBe` "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e"
|
||||
toHex (B32.xkChainCode master2) `shouldBe` "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689"
|
||||
it "rejects a seed shorter than 16 bytes" $
|
||||
B32.masterKey (B.replicate 15 1) `shouldSatisfy` isLeft
|
||||
it "rejects a seed longer than 64 bytes" $
|
||||
B32.masterKey (B.replicate 65 1) `shouldSatisfy` isLeft
|
||||
it "redacts the extended key in Show" $
|
||||
show master2 `shouldBe` "ExtendedKey <redacted>"
|
||||
describe "path rendering" $ do
|
||||
it "renders a path" $
|
||||
B32.renderPath [hardened' 44, hardened' 60, hardened' 0, 0, 0] `shouldBe` "m/44'/60'/0'/0/0"
|
||||
it "renders an Ethereum path for an account and address" $
|
||||
B32.renderPath (ethereumPath 7 3) `shouldBe` "m/44'/60'/7'/0/3"
|
||||
where
|
||||
master1 = right $ B32.masterKey (hx "000102030405060708090a0b0c0d0e0f")
|
||||
master2 =
|
||||
right . B32.masterKey $
|
||||
hx "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
|
||||
vector1 =
|
||||
[ ( "chain m",
|
||||
[],
|
||||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35",
|
||||
"873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"
|
||||
),
|
||||
( "chain m/0'",
|
||||
[hardened' 0],
|
||||
"edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea",
|
||||
"47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141"
|
||||
),
|
||||
( "chain m/0'/1",
|
||||
[hardened' 0, 1],
|
||||
"3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368",
|
||||
"2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19"
|
||||
),
|
||||
( "chain m/0'/1/2'",
|
||||
[hardened' 0, 1, hardened' 2],
|
||||
"cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca",
|
||||
"04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f"
|
||||
),
|
||||
( "chain m/0'/1/2'/2",
|
||||
[hardened' 0, 1, hardened' 2, 2],
|
||||
"0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4",
|
||||
"cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd"
|
||||
),
|
||||
( "chain m/0'/1/2'/2/1000000000",
|
||||
[hardened' 0, 1, hardened' 2, 2, 1000000000],
|
||||
"471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8",
|
||||
"c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e"
|
||||
)
|
||||
]
|
||||
|
||||
derivationTests :: Spec
|
||||
derivationTests = do
|
||||
it "derives the standard BIP-39 seed" $
|
||||
toHex seed
|
||||
`shouldBe` "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
|
||||
it "derives the well-known account 0 address" $
|
||||
show (addrAt 0) `shouldBe` "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
|
||||
it "derives account 1" $
|
||||
show (addrAt 1) `shouldBe` "0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265"
|
||||
it "derives account 2" $
|
||||
show (addrAt 2) `shouldBe` "0x07B5FdfEB4E11826D233403Fe8Db0611CCF4c231"
|
||||
it "gives each chat profile a distinct address" $
|
||||
map addrAt [0 .. 4] `shouldSatisfy` \as -> length as == length (nub as)
|
||||
where
|
||||
m = right $ B39.parseMnemonic canonicalPhrase
|
||||
seed = B39.mnemonicToSeed m ""
|
||||
master = right $ B32.masterKey seed
|
||||
addrAt i = addressFromPrivateKey . B32.xkKey . right $ B32.derivePath master (ethereumPath i 0)
|
||||
|
||||
eip55Tests :: Spec
|
||||
eip55Tests = do
|
||||
describe "spec vectors round-trip" $
|
||||
forM_ specAddresses $ \a ->
|
||||
it (BC.unpack a) $
|
||||
BC.pack (show . 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"
|
||||
-- between them these cover every byte value out through the hex encoder and every hex digit back through the decoder
|
||||
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 =
|
||||
[ "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
|
||||
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
|
||||
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
|
||||
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
|
||||
]
|
||||
|
||||
bip39Vectors :: [(ByteString, ByteString, ByteString)]
|
||||
bip39Vectors =
|
||||
[ ( "00000000000000000000000000000000",
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04" )
|
||||
, ( "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
"legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607" )
|
||||
, ( "80808080808080808080808080808080",
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8" )
|
||||
, ( "ffffffffffffffffffffffffffffffff",
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069" )
|
||||
, ( "000000000000000000000000000000000000000000000000",
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa" )
|
||||
, ( "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
|
||||
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd" )
|
||||
, ( "808080808080808080808080808080808080808080808080",
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
|
||||
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65" )
|
||||
, ( "ffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
|
||||
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528" )
|
||||
, ( "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8" )
|
||||
, ( "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
|
||||
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87" )
|
||||
, ( "8080808080808080808080808080808080808080808080808080808080808080",
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
|
||||
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f" )
|
||||
, ( "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
|
||||
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad" )
|
||||
, ( "9e885d952ad362caeb4efe34a8e91bd2",
|
||||
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
|
||||
"274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028" )
|
||||
, ( "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
|
||||
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
|
||||
"628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac" )
|
||||
, ( "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
|
||||
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
|
||||
"64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440" )
|
||||
, ( "c0ba5a8e914111210f2bd131f3d5e08d",
|
||||
"scheme spot photo card baby mountain device kick cradle pact join borrow",
|
||||
"ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612" )
|
||||
, ( "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
|
||||
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
|
||||
"fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d" )
|
||||
, ( "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
|
||||
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
|
||||
"72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d" )
|
||||
, ( "23db8160a31d3e0dca3688ed941adbf3",
|
||||
"cat swing flag economy stadium alone churn speed unique patch report train",
|
||||
"deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5" )
|
||||
, ( "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
|
||||
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
|
||||
"4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02" )
|
||||
, ( "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
|
||||
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
|
||||
"26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d" )
|
||||
, ( "f30f8c1da665478f49b001d94c5fc452",
|
||||
"vessel ladder alter error federal sibling chat ability sun glass valve picture",
|
||||
"2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f" )
|
||||
, ( "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
|
||||
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
|
||||
"7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88" )
|
||||
, ( "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
|
||||
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
|
||||
"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998" )
|
||||
]
|
||||
@@ -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, randomMsgId)
|
||||
import Simplex.Messaging.Protocol (EntityId (..), ErrorType, LinkId, Message (..), QueueLinkData, RecipientId, SParty (..), noMsgFlags)
|
||||
import Simplex.Messaging.Server (exportMessages, importMessages, printMessageStats)
|
||||
import Simplex.Messaging.Server.Env.STM (MsgStore (..), journalMsgStoreDepth, readWriteQueueStore)
|
||||
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..), expireBeforeEpoch)
|
||||
@@ -43,7 +43,6 @@ import Simplex.Messaging.Server.MsgStore.STM
|
||||
import Simplex.Messaging.Server.MsgStore.Types
|
||||
import Simplex.Messaging.Server.QueueStore
|
||||
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
import Simplex.Messaging.Server.QueueStore.Types
|
||||
import Simplex.Messaging.Server.StoreLog (closeStoreLog, logCreateQueue)
|
||||
import System.Directory (copyFile, createDirectoryIfMissing, listDirectory, removeFile, renameFile)
|
||||
import System.FilePath ((</>))
|
||||
@@ -58,6 +57,7 @@ import Simplex.Messaging.Agent.Store.Postgres.Common
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..))
|
||||
import Simplex.Messaging.Server.MsgStore.Postgres
|
||||
import Simplex.Messaging.Server.QueueStore.Postgres
|
||||
import Simplex.Messaging.Server.QueueStore.Types
|
||||
import SMPClient (postgressBracket, testServerDBConnectInfo, testStoreDBOpts)
|
||||
#endif
|
||||
|
||||
@@ -79,7 +79,6 @@ msgStoreTests = do
|
||||
someMsgStoreTests
|
||||
it "should correctly update message counts and canWrite flag" testUpdateMessageCounts
|
||||
it "tryDelPeekMsg (ACK not from NSE) should reset message counts when queue is empty" testResetMessageCounts
|
||||
it "should expire messages across commit batches" testExpireMessagesInBatches
|
||||
#endif
|
||||
describe "Journal message store: queue state backup expiration" $ do
|
||||
it "should remove old queue state backups" testRemoveQueueStateBackups
|
||||
@@ -101,7 +100,6 @@ msgStoreTests = do
|
||||
it "should get queue and store/read messages" testGetQueue
|
||||
it "should write/ack messages" testWriteAckMessages
|
||||
it "should not fail on EOF when changing read journal" testChangeReadJournal
|
||||
it "should not add link data to secured messaging queue" testLinkDataSecuredQueue
|
||||
|
||||
-- TODO constrain to STM stores?
|
||||
withMsgStore :: MsgStoreClass s => MsgStoreConfig s -> (s -> IO ()) -> IO ()
|
||||
@@ -147,7 +145,7 @@ mkMessage :: MonadIO m => ByteString -> m Message
|
||||
mkMessage body = liftIO $ do
|
||||
g <- C.newRandom
|
||||
msgTs <- getSystemTime
|
||||
msgId <- atomically $ randomMsgId 24 g
|
||||
msgId <- atomically $ C.randomBytes 24 g
|
||||
pure Message {msgId, msgTs, msgFlags = noMsgFlags, msgBody = C.unsafeMaxLenBS body}
|
||||
|
||||
pattern Msg :: ByteString -> Maybe Message
|
||||
@@ -268,33 +266,6 @@ testChangeReadJournal ms = do
|
||||
(Msg "message 5", Nothing) <- tryDelPeekMsg ms q mId5
|
||||
void $ ExceptT $ deleteQueue ms q
|
||||
|
||||
testLinkDataSecuredQueue :: MsgStoreClass s => s -> IO ()
|
||||
testLinkDataSecuredQueue ms = do
|
||||
g <- C.newRandom
|
||||
(sKey, _) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
let st = queueStore ms
|
||||
ld = (EncDataBytes "fixed data", EncDataBytes "user data")
|
||||
rndId = atomically $ EntityId <$> C.randomBytes 24 g
|
||||
(rId, qr) <- testNewQueueRec g QMMessaging
|
||||
(cId, cqr) <- testNewQueueRec g QMContact
|
||||
lnkId <- rndId
|
||||
cLnkId <- rndId
|
||||
runRight_ $ do
|
||||
q <- ExceptT $ addQueue ms rId qr
|
||||
-- the handle is read before SKEY, as in a command that raced with it
|
||||
staleQ <- ExceptT $ getQueue ms SRecipient rId
|
||||
ExceptT $ secureQueue st q sKey
|
||||
liftIO $ addQueueLinkData st staleQ lnkId ld `shouldReturn` Left AUTH
|
||||
freshQ <- ExceptT $ getQueue ms SRecipient rId
|
||||
liftIO $ getQueueLinkData st freshQ lnkId `shouldReturn` Left AUTH
|
||||
cq <- ExceptT $ addQueue ms cId cqr
|
||||
ExceptT $ secureQueue st cq sKey
|
||||
ExceptT $ addQueueLinkData st cq cLnkId ld
|
||||
ld' <- ExceptT $ getQueueLinkData st cq cLnkId
|
||||
liftIO $ ld' `shouldBe` ld
|
||||
void $ ExceptT $ deleteQueue ms q
|
||||
void $ ExceptT $ deleteQueue ms cq
|
||||
|
||||
testExportImportStore :: JournalMsgStore 'QSMemory -> IO ()
|
||||
testExportImportStore ms = do
|
||||
g <- C.newRandom
|
||||
@@ -356,34 +327,35 @@ testUpdateMessageCounts ms = do
|
||||
q <- ExceptT $ addQueue ms rId qr
|
||||
let write s = writeMsg ms q True =<< mkMessage s
|
||||
hasSize = checkQueueSize ms
|
||||
q `hasSize` (0, True)
|
||||
q `hasSize` (0, True, False)
|
||||
Just (Message {msgId = mId1}, True) <- write "message 1"
|
||||
q `hasSize` (1, True)
|
||||
q `hasSize` (1, True, True)
|
||||
Just (Message {msgId = mId2}, False) <- write "message 2"
|
||||
q `hasSize` (2, True)
|
||||
q `hasSize` (2, True, True)
|
||||
Just (Message {msgId = mId3}, False) <- write "message 3"
|
||||
q `hasSize` (3, True)
|
||||
q `hasSize` (3, True, True)
|
||||
Nothing <- write "message 4"
|
||||
q `hasSize` (4, False)
|
||||
q `hasSize` (4, False, True)
|
||||
Msg "message 1" <- tryPeekMsg ms q
|
||||
q `hasSize` (4, False)
|
||||
q `hasSize` (4, False, True)
|
||||
Msg "message 1" <- tryDelMsg ms q mId1
|
||||
q `hasSize` (3, False)
|
||||
q `hasSize` (3, False, True)
|
||||
Msg "message 2" <- tryPeekMsg ms q
|
||||
(Msg "message 2", Msg "message 3") <- tryDelPeekMsg ms q mId2
|
||||
q `hasSize` (2, False)
|
||||
q `hasSize` (2, False, True)
|
||||
(Msg "message 3", Just MessageQuota {msgId = mId4}) <- tryDelPeekMsg ms q mId3
|
||||
q `hasSize` (1, False)
|
||||
q `hasSize` (1, False, True)
|
||||
(Just MessageQuota {}, Nothing) <- tryDelPeekMsg ms q mId4
|
||||
q `hasSize` (0, True)
|
||||
q `hasSize` (0, True, False)
|
||||
|
||||
checkQueueSize :: PostgresMsgStore -> PostgresQueue -> (Int64, Bool) -> ExceptT ErrorType IO ()
|
||||
checkQueueSize ms q (size, canWrt) = liftIO $ do
|
||||
[(size', canWrt')] <-
|
||||
checkQueueSize :: PostgresMsgStore -> PostgresQueue -> (Int64, Bool, Bool) -> ExceptT ErrorType IO ()
|
||||
checkQueueSize ms q (size, canWrt, expire) = liftIO $ do
|
||||
[(size', canWrt', expire')] <-
|
||||
withTransaction (dbStore $ queueStore ms) $ \db ->
|
||||
DB.query db "SELECT msg_queue_size, msg_can_write FROM msg_queues WHERE recipient_id = ?" (Only (recipientId q))
|
||||
DB.query db "SELECT msg_queue_size, msg_can_write, msg_queue_expire FROM msg_queues WHERE recipient_id = ?" (Only (recipientId q))
|
||||
size' `shouldBe` size
|
||||
canWrt' `shouldBe` canWrt
|
||||
expire' `shouldBe` expire
|
||||
|
||||
testResetMessageCounts :: PostgresMsgStore -> IO ()
|
||||
testResetMessageCounts ms = do
|
||||
@@ -397,63 +369,25 @@ testResetMessageCounts ms = do
|
||||
Just (Message {msgId = mId2}, False) <- write "message 2"
|
||||
Just (Message {msgId = mId3}, False) <- write "message 3"
|
||||
Nothing <- write "message 4"
|
||||
q `hasSize` (4, False)
|
||||
q `hasSize` (4, False, True)
|
||||
liftIO $ setIncorrectSize q (10, True)
|
||||
Nothing <- write "message 5"
|
||||
q `hasSize` (11, False)
|
||||
q `hasSize` (11, False, True)
|
||||
(Msg "message 1", Msg "message 2") <- tryDelPeekMsg ms q mId1
|
||||
q `hasSize` (10, False)
|
||||
q `hasSize` (10, False, True)
|
||||
(Msg "message 2", Msg "message 3") <- tryDelPeekMsg ms q mId2
|
||||
q `hasSize` (9, False)
|
||||
q `hasSize` (9, False, True)
|
||||
(Msg "message 3", Just MessageQuota {msgId = mId4}) <- tryDelPeekMsg ms q mId3
|
||||
q `hasSize` (8, False)
|
||||
q `hasSize` (8, False, True)
|
||||
(Just MessageQuota {}, Just MessageQuota {msgId = mId5}) <- tryDelPeekMsg ms q mId4
|
||||
q `hasSize` (7, False)
|
||||
q `hasSize` (7, False, True)
|
||||
(Just MessageQuota {}, Nothing) <- tryDelPeekMsg ms q mId5
|
||||
q `hasSize` (0, True) -- reset
|
||||
q `hasSize` (0, True, False) -- reset
|
||||
where
|
||||
setIncorrectSize :: PostgresQueue -> (Int64, Bool) -> IO ()
|
||||
setIncorrectSize q (size, canWrt) =
|
||||
void $ withTransaction (dbStore $ queueStore ms) $ \db ->
|
||||
DB.execute db "UPDATE msg_queues SET msg_queue_size = ?, msg_can_write = ? WHERE recipient_id = ?" (size, canWrt, recipientId q)
|
||||
|
||||
testExpireMessagesInBatches :: PostgresMsgStore -> IO ()
|
||||
testExpireMessagesInBatches ms = do
|
||||
g <- C.newRandom
|
||||
emptiedQs <- replicateM emptiedCount $ newQueue g
|
||||
partialQs <- replicateM partialCount $ newQueue g
|
||||
overQuotaQs <- replicateM overQuotaCount $ newQueue g
|
||||
quotaMsgs <- runRight $ do
|
||||
forM_ (emptiedQs <> partialQs) $ \q -> void $ write q "old 1"
|
||||
forM_ emptiedQs $ \q -> void $ write q "old 2"
|
||||
mapM fillPastQuota overQuotaQs
|
||||
-- msg_ts has second granularity, so the recent messages need a new second to be kept
|
||||
threadDelay 1100000
|
||||
boundary <- systemSeconds <$> getSystemTime
|
||||
runRight_ $ forM_ partialQs $ \q -> void $ write q "recent"
|
||||
|
||||
MessageStats {expiredMsgsCount, storedMsgsCount, storedQueues} <- expireOldMessages False ms boundary 0
|
||||
expiredMsgsCount `shouldBe` (emptiedCount * 2 + partialCount + sum quotaMsgs)
|
||||
storedMsgsCount `shouldBe` (partialCount + overQuotaCount) -- recent messages and quota markers
|
||||
storedQueues `shouldBe` (emptiedCount + partialCount + overQuotaCount)
|
||||
runRight_ $ do
|
||||
forM_ emptiedQs $ \q -> checkQueueSize ms q (0, True)
|
||||
forM_ partialQs $ \q -> checkQueueSize ms q (1, True)
|
||||
-- the quota marker is never expired, and the queue stays blocked until it is acked
|
||||
forM_ overQuotaQs $ \q -> checkQueueSize ms q (1, False)
|
||||
where
|
||||
-- expire_old_messages pages through expired messages and commits per page, so these
|
||||
-- counts put a page boundary inside each group of queues.
|
||||
emptiedCount = 120 :: Int
|
||||
partialCount = 40 :: Int
|
||||
overQuotaCount = 10 :: Int
|
||||
newQueue g = do
|
||||
(rId, qr) <- testNewQueueRec g QMMessaging
|
||||
runRight $ ExceptT $ addQueue ms rId qr
|
||||
write q s = writeMsg ms q True =<< mkMessage s
|
||||
fillPastQuota q = go 0
|
||||
where
|
||||
go n = write q "fill" >>= maybe (pure n) (const $ go (n + 1))
|
||||
#endif
|
||||
|
||||
testQueueState :: JournalMsgStore s -> IO ()
|
||||
|
||||
@@ -14,11 +14,8 @@ import Util
|
||||
retryIntervalTests :: Spec
|
||||
retryIntervalTests = do
|
||||
describe "Retry interval with 2 modes and lock" $ do
|
||||
testRetryIntervalSameMode withRetryNewLock2
|
||||
testRetryIntervalSwitchMode withRetryNewLock2
|
||||
describe "Retry interval with 2 modes" $ do
|
||||
testRetryIntervalSameMode withRetryInterval2
|
||||
testRetryIntervalSwitchMode withRetryInterval2
|
||||
testRetryIntervalSameMode
|
||||
testRetryIntervalSwitchMode
|
||||
describe "Foreground retry interval" $ do
|
||||
testRetryForeground
|
||||
testRetryToBackground
|
||||
@@ -44,18 +41,14 @@ testFastRI =
|
||||
maxInterval = 40000
|
||||
}
|
||||
|
||||
type WithRetry2 = RetryInterval2 -> (RI2State -> (RetryIntervalMode -> IO ()) -> IO ()) -> IO ()
|
||||
|
||||
withRetryNewLock2 :: WithRetry2
|
||||
withRetryNewLock2 ri action = newEmptyTMVarIO >>= \lock -> withRetryLock2 ri lock action
|
||||
|
||||
testRetryIntervalSameMode :: WithRetry2 -> Spec
|
||||
testRetryIntervalSameMode withRetry =
|
||||
testRetryIntervalSameMode :: Spec
|
||||
testRetryIntervalSameMode =
|
||||
it "should increase elapased time and interval when the mode stays the same" $ do
|
||||
lock <- newEmptyTMVarIO
|
||||
intervals <- newTVarIO []
|
||||
reportedIntervals <- newTVarIO []
|
||||
ts <- newTVarIO =<< getCurrentTime
|
||||
withRetry testRI $ \(RI2State slow fast) loop -> do
|
||||
withRetryLock2 testRI lock $ \(RI2State slow fast) loop -> do
|
||||
ints <- addInterval intervals ts
|
||||
atomically $ modifyTVar' reportedIntervals ((slow, fast) :)
|
||||
when (length ints < 9) $ loop RIFast
|
||||
@@ -72,13 +65,14 @@ testRetryIntervalSameMode withRetry =
|
||||
(20000, 40000)
|
||||
]
|
||||
|
||||
testRetryIntervalSwitchMode :: WithRetry2 -> Spec
|
||||
testRetryIntervalSwitchMode withRetry =
|
||||
testRetryIntervalSwitchMode :: Spec
|
||||
testRetryIntervalSwitchMode =
|
||||
it "should increase elapased time and interval when the mode switches" $ do
|
||||
lock <- newEmptyTMVarIO
|
||||
intervals <- newTVarIO []
|
||||
reportedIntervals <- newTVarIO []
|
||||
ts <- newTVarIO =<< getCurrentTime
|
||||
withRetry testRI $ \(RI2State slow fast) loop -> do
|
||||
withRetryLock2 testRI lock $ \(RI2State slow fast) loop -> do
|
||||
ints <- addInterval intervals ts
|
||||
atomically $ modifyTVar' reportedIntervals ((slow, fast) :)
|
||||
when (length ints < 11) $ loop $ if length ints <= 5 then RIFast else RISlow
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module CoreTests.SOCKSSettings where
|
||||
|
||||
import Network.Socket (SockAddr (..), tupleToHostAddress)
|
||||
import Simplex.Messaging.Agent.Client (ipAddressProtected)
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ErrorType, pattern SMPServer)
|
||||
import Simplex.Messaging.Protocol (ErrorType)
|
||||
import Simplex.Messaging.Transport.Client
|
||||
import Test.Hspec hiding (fit, it)
|
||||
import Util
|
||||
@@ -22,7 +19,6 @@ socksSettingsTests :: Spec
|
||||
socksSettingsTests = do
|
||||
describe "hostMode and requiredHostMode settings" testHostMode
|
||||
describe "socksMode setting, independent of hostMode setting" testSocksMode
|
||||
describe "ipAddressProtected, consistent with chosen host and socksMode" testIPAddressProtected
|
||||
describe "socks proxy address encoding" testSocksProxyEncoding
|
||||
|
||||
testPublicHost :: TransportHost
|
||||
@@ -98,19 +94,6 @@ testSocksMode = do
|
||||
let TransportClientConfig {socksProxy} = transportClientConfig cfg NRMInteractive host False Nothing
|
||||
in socksProxy
|
||||
|
||||
testIPAddressProtected :: Spec
|
||||
testIPAddressProtected = do
|
||||
it "should be protected if SOCKS proxy is used for the chosen host" $ do
|
||||
protected SMAlways HMOnionViaSocks [testPublicHost] `shouldBe` True
|
||||
protected SMOnion HMOnionViaSocks [testPublicHost, testOnionHost] `shouldBe` True
|
||||
protected SMOnion HMPublic [testOnionHost] `shouldBe` True
|
||||
it "should not be protected if SOCKS proxy is not used for the chosen host" $ do
|
||||
protected SMOnion HMOnionViaSocks [testPublicHost] `shouldBe` False
|
||||
protected SMOnion HMPublic [testPublicHost, testOnionHost] `shouldBe` False
|
||||
where
|
||||
protected socksMode hostMode hosts =
|
||||
ipAddressProtected defaultNetworkConfig {socksProxy = Just defaultSocksProxyWithAuth, socksMode, hostMode} (SMPServer hosts "" (C.KeyHash ""))
|
||||
|
||||
testSocksProxyEncoding :: Spec
|
||||
testSocksProxyEncoding = do
|
||||
it "should decode SOCKS proxy with isolate-by-auth mode" $ do
|
||||
|
||||
@@ -22,7 +22,7 @@ import qualified Data.X509.Validation as XV
|
||||
import SMPClient
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Server.Store (FileRec (..), FileRecipient (..), FileStoreClass (..), RoundedFileTime, STMFileStore (..))
|
||||
import Simplex.FileTransfer.Server.StoreLog (FileStoreLogRecord (..), readWriteFileStore, serverFileName)
|
||||
import Simplex.FileTransfer.Server.StoreLog (FileStoreLogRecord (..), readWriteFileStore)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
@@ -37,7 +37,6 @@ import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.SystemTime
|
||||
import Simplex.Messaging.Transport (SMPServiceRole (..))
|
||||
import Simplex.Messaging.Transport.Credentials (genCredentials)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec hiding (fit, it)
|
||||
import Util
|
||||
|
||||
@@ -270,18 +269,6 @@ fileStoreLogTests = do
|
||||
saved = [AddFile sId file createdAt Nothing EntityActive, BlockFile sId blockedWithNotice],
|
||||
compacted = [AddFile sId file createdAt Nothing (EntityBlocked blockedWithNotice)],
|
||||
state = M.fromList [(sId, (file, createdAt, Nothing, EntityBlocked blockedWithNotice))]
|
||||
},
|
||||
SLTC
|
||||
{ name = "put file with wrong file name",
|
||||
saved = [AddFile sId file createdAt (Just expiresAt) EntityActive, PutFile sId "/etc/passwd"],
|
||||
compacted = [AddFile sId file createdAt (Just expiresAt) EntityActive],
|
||||
state = M.fromList [(sId, (file, createdAt, Just expiresAt, EntityActive))]
|
||||
},
|
||||
SLTC
|
||||
{ name = "put file in another directory",
|
||||
saved = [AddFile sId file createdAt (Just expiresAt) EntityActive, PutFile sId ("/srv/xftp" </> serverFileName sId)],
|
||||
compacted = [AddFile sId file createdAt (Just expiresAt) EntityActive, PutFile sId ("/srv/xftp" </> serverFileName sId)],
|
||||
state = M.fromList [(sId, (file, createdAt, Just expiresAt, EntityActive))]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import Data.Word (Word32)
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..), SFileParty (..))
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.FileTransfer.Server.Store.Postgres (PostgresFileStore, importFileStore, exportFileStore)
|
||||
import Simplex.FileTransfer.Server.StoreLog (closeStoreLog, readWriteFileStore, serverFileName, writeFileStore)
|
||||
import Simplex.FileTransfer.Server.StoreLog (closeStoreLog, readWriteFileStore, writeFileStore)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), EntityId (..))
|
||||
@@ -17,7 +17,6 @@ import Simplex.Messaging.Server.QueueStore (ServerEntityStatus (..))
|
||||
import Simplex.Messaging.Server.StoreLog (openWriteStoreLog)
|
||||
import Simplex.Messaging.SystemTime (RoundedSystemTime (..))
|
||||
import System.Directory (doesFileExist, removeFile)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec hiding (fit, it)
|
||||
import UnliftIO.STM
|
||||
import Util
|
||||
@@ -266,7 +265,7 @@ testMigrationRoundTrip = do
|
||||
sId2 = EntityId "migration_file_2"
|
||||
rId1 = EntityId "migration_rcp_1_"
|
||||
addFile stmStore sId1 fileInfo1 testCreatedAt (Just testExpiresAt) EntityActive `shouldReturn` Right ()
|
||||
void $ setFilePath stmStore sId1 ("/tmp" </> serverFileName sId1)
|
||||
void $ setFilePath stmStore sId1 "/tmp/file1"
|
||||
addRecipient stmStore sId1 (FileRecipient rId1 rcpKey1) `shouldReturn` Right ()
|
||||
let testBlockInfo = BlockingInfo {reason = BRSpam, notice = Nothing}
|
||||
addFile stmStore sId2 fileInfo2 testCreatedAt Nothing (EntityBlocked testBlockInfo) `shouldReturn` Right ()
|
||||
@@ -290,7 +289,7 @@ testMigrationRoundTrip = do
|
||||
case result1 of
|
||||
Right (FileRec {fileInfo = fi, filePath, expiresAt, fileStatus}, _) -> do
|
||||
size fi `shouldBe` 128000
|
||||
readTVarIO filePath `shouldReturn` Just ("/tmp" </> serverFileName sId1)
|
||||
readTVarIO filePath `shouldReturn` Just "/tmp/file1"
|
||||
expiresAt `shouldBe` Just testExpiresAt
|
||||
readTVarIO fileStatus `shouldReturn` EntityActive
|
||||
Left e -> expectationFailure $ "getFile sId1 failed: " <> show e
|
||||
|
||||
@@ -47,12 +47,12 @@ withResolverServerDelayed delayMs handler action = do
|
||||
let (st, body) = handler (pathInfo req)
|
||||
send $ responseLBS st [(hContentType, "application/json")] body
|
||||
|
||||
-- | The resolver API is versioned on its own: v2 answers with NameRegistration
|
||||
-- JSON, which is the only shape the server asks for.
|
||||
-- | The resolver API is versioned on its own: v2 answers a name with NameRegistration, an address with OwnedNames.
|
||||
resolveResp :: Status -> LB.ByteString -> [Text] -> (Status, LB.ByteString)
|
||||
resolveResp st body = \case
|
||||
["health"] -> (ok200, "{}")
|
||||
("v2" : "resolve" : _) -> (st, body)
|
||||
("v2" : "owned-by" : _) -> (st, body)
|
||||
_ -> (notFound404, "{}")
|
||||
|
||||
testNamesConfig :: Int -> NamesConfig
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user