mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
[Mint] Fix key derivation (#187)
* fix private key derivation * add backwards compatilibity for old keysets * bump version * test pubkeys and private keys * make format * reset keys for tests * fix cli tests
This commit is contained in:
@@ -115,7 +115,7 @@ cashu info
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
```bash
|
```bash
|
||||||
Version: 0.11.2
|
Version: 0.12.0
|
||||||
Debug: False
|
Debug: False
|
||||||
Cashu dir: /home/user/.cashu
|
Cashu dir: /home/user/.cashu
|
||||||
Wallet: wallet
|
Wallet: wallet
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import json
|
|||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Any, Dict, List, Optional, TypedDict, Union
|
from typing import Any, Dict, List, Optional, TypedDict, Union
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
|
from ..core.crypto import (
|
||||||
|
derive_keys,
|
||||||
|
derive_keys_backwards_compatible_0_11_insecure,
|
||||||
|
derive_keyset_id,
|
||||||
|
derive_pubkeys,
|
||||||
|
)
|
||||||
from ..core.secp import PrivateKey, PublicKey
|
from ..core.secp import PrivateKey, PublicKey
|
||||||
|
|
||||||
# ------- PROOFS -------
|
# ------- PROOFS -------
|
||||||
@@ -241,6 +247,8 @@ class WalletKeyset:
|
|||||||
if public_keys:
|
if public_keys:
|
||||||
self.public_keys = public_keys
|
self.public_keys = public_keys
|
||||||
self.id = derive_keyset_id(self.public_keys)
|
self.id = derive_keyset_id(self.public_keys)
|
||||||
|
if id:
|
||||||
|
assert id == self.id, "id must match derived id from public keys"
|
||||||
|
|
||||||
|
|
||||||
class MintKeyset:
|
class MintKeyset:
|
||||||
@@ -267,7 +275,7 @@ class MintKeyset:
|
|||||||
active=None,
|
active=None,
|
||||||
seed: str = "",
|
seed: str = "",
|
||||||
derivation_path: str = "",
|
derivation_path: str = "",
|
||||||
version: str = "",
|
version: str = "1",
|
||||||
):
|
):
|
||||||
self.derivation_path = derivation_path
|
self.derivation_path = derivation_path
|
||||||
self.id = id
|
self.id = id
|
||||||
@@ -282,16 +290,26 @@ class MintKeyset:
|
|||||||
|
|
||||||
def generate_keys(self, seed):
|
def generate_keys(self, seed):
|
||||||
"""Generates keys of a keyset from a seed."""
|
"""Generates keys of a keyset from a seed."""
|
||||||
self.private_keys = derive_keys(seed, self.derivation_path)
|
backwards_compatibility_pre_0_12 = False
|
||||||
|
if (
|
||||||
|
self.version
|
||||||
|
and len(self.version.split(".")) > 1
|
||||||
|
and int(self.version.split(".")[0]) == 0
|
||||||
|
and int(self.version.split(".")[1]) <= 11
|
||||||
|
):
|
||||||
|
backwards_compatibility_pre_0_12 = True
|
||||||
|
# WARNING: Broken key derivation for backwards compatibility with < 0.12
|
||||||
|
self.private_keys = derive_keys_backwards_compatible_0_11_insecure(
|
||||||
|
seed, self.derivation_path
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.private_keys = derive_keys(seed, self.derivation_path)
|
||||||
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
|
||||||
self.id = derive_keyset_id(self.public_keys) # type: ignore
|
self.id = derive_keyset_id(self.public_keys) # type: ignore
|
||||||
|
if backwards_compatibility_pre_0_12:
|
||||||
def get_keybase(self):
|
logger.warning(
|
||||||
assert self.id is not None
|
f"WARNING: Using weak key derivation for keyset {self.id} (backwards compatibility < 0.12)"
|
||||||
return {
|
)
|
||||||
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
|
|
||||||
for k, v in self.public_keys.items() # type: ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MintKeysets:
|
class MintKeysets:
|
||||||
|
|||||||
@@ -19,6 +19,24 @@ def derive_keys(master_key: str, derivation_path: str = ""):
|
|||||||
Deterministic derivation of keys for 2^n values.
|
Deterministic derivation of keys for 2^n values.
|
||||||
TODO: Implement BIP32.
|
TODO: Implement BIP32.
|
||||||
"""
|
"""
|
||||||
|
return {
|
||||||
|
2
|
||||||
|
** i: PrivateKey(
|
||||||
|
hashlib.sha256(
|
||||||
|
(master_key + derivation_path + str(i)).encode("utf-8")
|
||||||
|
).digest()[:32],
|
||||||
|
raw=True,
|
||||||
|
)
|
||||||
|
for i in range(settings.max_order)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def derive_keys_backwards_compatible_0_11_insecure(
|
||||||
|
master_key: str, derivation_path: str = ""
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
WARNING: Broken key derivation for backwards compatibility with 0.11.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
2
|
2
|
||||||
** i: PrivateKey(
|
** i: PrivateKey(
|
||||||
@@ -33,7 +51,7 @@ def derive_keys(master_key: str, derivation_path: str = ""):
|
|||||||
|
|
||||||
def derive_pubkey(master_key: str):
|
def derive_pubkey(master_key: str):
|
||||||
return PrivateKey(
|
return PrivateKey(
|
||||||
hashlib.sha256((master_key).encode("utf-8")).hexdigest().encode("utf-8")[:32],
|
hashlib.sha256((master_key).encode("utf-8")).digest()[:32],
|
||||||
raw=True,
|
raw=True,
|
||||||
).pubkey
|
).pubkey
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field, validator
|
|||||||
|
|
||||||
env = Env()
|
env = Env()
|
||||||
|
|
||||||
VERSION = "0.11.2"
|
VERSION = "0.12.0"
|
||||||
|
|
||||||
|
|
||||||
def find_env_file():
|
def find_env_file():
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class LedgerAPI:
|
|||||||
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
|
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
|
||||||
proofs: List[Proof] = []
|
proofs: List[Proof] = []
|
||||||
for promise, secret, r in zip(promises, secrets, rs):
|
for promise, secret, r in zip(promises, secrets, rs):
|
||||||
logger.trace(f"Creating proof with keyset {self.keyset_id} =+ {promise.id}")
|
logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}")
|
||||||
assert (
|
assert (
|
||||||
self.keyset_id == promise.id
|
self.keyset_id == promise.id
|
||||||
), "our keyset id does not match promise id."
|
), "our keyset id does not match promise id."
|
||||||
@@ -278,7 +278,7 @@ class LedgerAPI:
|
|||||||
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
int(amt): PublicKey(bytes.fromhex(val), raw=True)
|
||||||
for amt, val in keys.items()
|
for amt, val in keys.items()
|
||||||
}
|
}
|
||||||
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
|
keyset = WalletKeyset(id=keyset_id, public_keys=keyset_keys, mint_url=url)
|
||||||
return keyset
|
return keyset
|
||||||
|
|
||||||
@async_set_requests
|
@async_set_requests
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
|
|||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="cashu",
|
name="cashu",
|
||||||
version="0.11.2",
|
version="0.12.0",
|
||||||
description="Ecash wallet and mint for Bitcoin Lightning",
|
description="Ecash wallet and mint for Bitcoin Lightning",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class UvicornServer(multiprocessing.Process):
|
|||||||
settings.mint_lightning_backend = "FakeWallet"
|
settings.mint_lightning_backend = "FakeWallet"
|
||||||
settings.mint_listen_port = 3337
|
settings.mint_listen_port = 3337
|
||||||
settings.mint_database = "data/test_mint"
|
settings.mint_database = "data/test_mint"
|
||||||
|
settings.mint_private_key = "TEST_PRIVATE_KEY"
|
||||||
settings.mint_derivation_path = "0/0/0/0"
|
settings.mint_derivation_path = "0/0/0/0"
|
||||||
settings.mint_private_key = "privatekeyofthemint"
|
|
||||||
|
|
||||||
dirpath = Path(settings.mint_database)
|
dirpath = Path(settings.mint_database)
|
||||||
if dirpath.exists() and dirpath.is_dir():
|
if dirpath.exists() and dirpath.is_dir():
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def test_send(mint, cli_prefix):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
def test_receive_tokenv3(mint, cli_prefix):
|
def test_receive_tokenv3(mint, cli_prefix):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIkhYUC1SSEZ2Tk5Cb2ZZT1FzY1RURnciLCAiQyI6ICIwMjQxOTgwYzk0NjY1ODU0MDZhODg3ZWI1Y2JkN2Y1N2U4NTc0MzdiNzE0MTkxYmUwOTQyNTJmYjAxZWViNGQ3OTQifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICItcXI2cF80bDNVQWQwSzBib2JSekdRIiwgIkMiOiAiMDIyOTA5YWJhYjg0N2RlODA0NjdmYjhjOTdiMDJiYjFjOGUwNWE0ZTFlYzBiYzI1MDY0MDUzMjg3YTNhYjViZDYyIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
|
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIld6TEF2VW53SDlRaFYwQU1rMy1oYWciLCAiQyI6ICIwMmZlMzUxYjAyN2FlMGY1ZDkyN2U2ZjFjMTljMjNjNTc3NzRhZTI2M2UyOGExN2E2MTUxNjY1ZjU3NWNhNjMyNWMifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJDamFTeTcyR2dVOGwzMGV6bE5zZnVBIiwgIkMiOiAiMDNjMzM0OTJlM2ZlNjI4NzFhMWEzMDhiNWUyYjVhZjBkNWI1Mjk5YzI0YmVkNDI2ZjQ1YzZmNDg5N2QzZjc4NGQ5In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli,
|
cli,
|
||||||
[
|
[
|
||||||
@@ -125,7 +125,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
|
|||||||
# this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token
|
# this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token
|
||||||
# therefore, we need to know the mint keyset already and have the mint URL in the db
|
# therefore, we need to know the mint keyset already and have the mint URL in the db
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIk1OQzRXM2k3NjNSMFYwdkFncEdJQ1EiLCAiQyI6ICIwMjg2YWIyNDZlMWViZTI5Yjk0ZTMzMDgzMjE0NDZhNTRkOGE3NWEwOTQ2MjU4YmYyMzM1ZmJhOTA5Y2ZjY2VhMWYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI5OFM5MGRjRGtKb3Q1V1Z4QVJ3VWdRIiwgIkMiOiAiMDJlMTE0OGRkNzQ3OWJlNzIwMmI5OWVmZDIzNjllZTFhYTBhYTVhYzMyYjM1ODczMzk0YmNjYWU1MmFkZTYzYmUxIn1dfV19"
|
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIi1oM0ZXMFFoX1FYLW9ac1V2c0RuNlEiLCAiQyI6ICIwMzY5Mzc4MzdlYjg5ZWI4NjMyNWYwOWUyOTIxMWQxYTI4OTRlMzQ2YmM1YzQwZTZhMThlNTk5ZmVjNjEwOGRmMGIifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI3d0VhNUgzZGhSRGRNZl94c1k3c3JnIiwgIkMiOiAiMDJiZmZkM2NlZDkxNjUyMzcxMDg2NjQxMzJiMjgxYjBhZjY1ZTNlZWVkNTY3MmFkZjM0Y2VhNzE5ODhhZWM1NWI1In1dfV19"
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli,
|
cli,
|
||||||
[
|
[
|
||||||
@@ -142,7 +142,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
def test_receive_tokenv2(mint, cli_prefix):
|
def test_receive_tokenv2(mint, cli_prefix):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ6dHRyMU9EdXBlZzA5Y1d2ckFVdWxRIiwgIkMiOiAiMDM3ZGQyNDYxZjFlOTg4Y2YxOWQyMmJhOTMxOTdlMmU2YmI5YTJmNjc5NDM4YTFiZjYwYmY0ZWJmZGJkNWUyYmM0In0sIHsiaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiekw5NDlTVzI4ZEpxaUMyOXl0bVZKQSIsICJDIjogIjAzOWIzOGIwM2QxY2NlNTU0NGZlYzM3YTM4ZGViZGZhMjUzMTc2ZTI3MWVlNDU3NjdkOTBkMWYwNWNmZGNhYzE2ZCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjI4ajhueVNMMU5kZCJdfV19"
|
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJhUmREbzlFdW9yZUVfOW90enRNVVpnIiwgIkMiOiAiMDNhMzY5ZmUyN2IxYmVmOTg4MzA3NDQyN2RjMzc1NmU0NThlMmMwYjQ1NWMwYmVmZGM4ZjVmNTA3YmM5MGQxNmU3In0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiTEZQbFp6Ui1MWHFfYXFDMGhUeDQyZyIsICJDIjogIjAzNGNiYzQxYWY0ODIxMGFmNjVmYjVjOWIzOTNkMjhmMmQ5ZDZhOWE5MzI2YmI3MzQ2YzVkZmRmMTU5MDk1MzI2YyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjFjQ05JQVoyWC93MSJdfV19"
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli,
|
cli,
|
||||||
[*cli_prefix, "receive", token],
|
[*cli_prefix, "receive", token],
|
||||||
@@ -155,7 +155,7 @@ def test_receive_tokenv2(mint, cli_prefix):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
def test_receive_tokenv1(mint, cli_prefix):
|
def test_receive_tokenv1(mint, cli_prefix):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
token = "W3siaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiZDRJQjY3LU1iOGpDS242clZoREMyUSIsICJDIjogIjAzM2E5M2NiNjhjZWZhZjFmNTJkN2NhZTMzNWVhN2ExYmQ4MDFiZTVmZDE5OGI5MWQzM2FmMDJlNjk3NWI0NzdmMiJ9LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInAtc0QzeW5PSE5ISmdjVlRUZDVVYVEiLCAiQyI6ICIwMzg5NGMwMzdiNGMyMWRmNmNiNzExMmJjZjE0OGMwOTlmYTM2ZDk4MTZhYjcwN2VmMTM0N2ZmODEyZjQ4N2MzNmEifV0="
|
token = "W3siaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiRnVsc2dzMktQV1FMcUlLX200SzgwQSIsICJDIjogIjAzNTc4OThlYzlhMjIxN2VhYWIxZDc3YmM1Mzc2OTUwMjJlMjU2YTljMmMwNjc0ZDJlM2FiM2JiNGI0ZDMzMWZiMSJ9LCB7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInJlRDBDazVNS2xBTUQ0dWk2OEtfbEEiLCAiQyI6ICIwMjNkODNkNDE0MDU0NWQ1NTg4NjUyMzU5YjJhMjFhODljODY1ZGIzMzAyZTkzMTZkYTM5NjA0YTA2ZDYwYWQzOGYifV0="
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli,
|
cli,
|
||||||
[*cli_prefix, "receive", token],
|
[*cli_prefix, "receive", token],
|
||||||
|
|||||||
@@ -52,11 +52,37 @@ async def ledger():
|
|||||||
yield ledger
|
yield ledger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pubkeys(ledger: Ledger):
|
||||||
|
assert ledger.keyset.public_keys
|
||||||
|
assert (
|
||||||
|
ledger.keyset.public_keys[1].serialize().hex()
|
||||||
|
== "03190ebc0c3e2726a5349904f572a2853ea021b0128b269b8b6906501d262edaa8"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
|
||||||
|
== "032dc008b88b85fdc2301a499bfaaef774c191a6307d8c9434838fc2eaa2e48d51"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_privatekeys(ledger: Ledger):
|
||||||
|
assert ledger.keyset.private_keys
|
||||||
|
assert (
|
||||||
|
ledger.keyset.private_keys[1].serialize()
|
||||||
|
== "67de62e1bf8b5ccf88dbad6768b7d13fa0f41433b0a89caf915039505f2e00a7"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
|
||||||
|
== "3b1340c703b02028a11025302d2d9e68d2a6dd721ab1a2770f0942d15eacb8d0"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_keysets(ledger: Ledger):
|
async def test_keysets(ledger: Ledger):
|
||||||
assert len(ledger.keysets.keysets)
|
assert len(ledger.keysets.keysets)
|
||||||
assert len(ledger.keysets.get_ids())
|
assert len(ledger.keysets.get_ids())
|
||||||
assert ledger.keyset.id == "XQM1wwtQbOXE"
|
assert ledger.keyset.id == "1cCNIAZ2X/w1"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -79,7 +105,7 @@ async def test_mint(ledger: Ledger):
|
|||||||
assert promises[0].amount == 8
|
assert promises[0].amount == 8
|
||||||
assert (
|
assert (
|
||||||
promises[0].C_
|
promises[0].C_
|
||||||
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
|
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -107,5 +133,5 @@ async def test_generate_promises(ledger: Ledger):
|
|||||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||||
assert (
|
assert (
|
||||||
promises[0].C_
|
promises[0].C_
|
||||||
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
|
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user