diff --git a/README.md b/README.md index b9cdda0..0c4f404 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ cashu info Returns: ```bash -Version: 0.11.2 +Version: 0.12.0 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/base.py b/cashu/core/base.py index fa3a08d..d8b3e9f 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -3,9 +3,15 @@ import json from sqlite3 import Row from typing import Any, Dict, List, Optional, TypedDict, Union +from loguru import logger 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 # ------- PROOFS ------- @@ -241,6 +247,8 @@ class WalletKeyset: if public_keys: self.public_keys = 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: @@ -267,7 +275,7 @@ class MintKeyset: active=None, seed: str = "", derivation_path: str = "", - version: str = "", + version: str = "1", ): self.derivation_path = derivation_path self.id = id @@ -282,16 +290,26 @@ class MintKeyset: def generate_keys(self, 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.id = derive_keyset_id(self.public_keys) # type: ignore - - def get_keybase(self): - assert self.id is not None - return { - k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) - for k, v in self.public_keys.items() # type: ignore - } + if backwards_compatibility_pre_0_12: + logger.warning( + f"WARNING: Using weak key derivation for keyset {self.id} (backwards compatibility < 0.12)" + ) class MintKeysets: diff --git a/cashu/core/crypto.py b/cashu/core/crypto.py index bab19a2..44cd8f5 100644 --- a/cashu/core/crypto.py +++ b/cashu/core/crypto.py @@ -19,6 +19,24 @@ def derive_keys(master_key: str, derivation_path: str = ""): Deterministic derivation of keys for 2^n values. 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 { 2 ** i: PrivateKey( @@ -33,7 +51,7 @@ def derive_keys(master_key: str, derivation_path: str = ""): def derive_pubkey(master_key: str): 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, ).pubkey diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 1daf8ea..1ae0129 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field, validator env = Env() -VERSION = "0.11.2" +VERSION = "0.12.0" def find_env_file(): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index d3fdccf..6c4f4db 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -116,7 +116,7 @@ class LedgerAPI: """Returns proofs of promise from promises. Wants secrets and blinding factors rs.""" proofs: List[Proof] = [] 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 ( self.keyset_id == promise.id ), "our keyset id does not match promise id." @@ -278,7 +278,7 @@ class LedgerAPI: int(amt): PublicKey(bytes.fromhex(val), raw=True) 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 @async_set_requests diff --git a/setup.py b/setup.py index 47bf67d..7542c3c 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]} setuptools.setup( name="cashu", - version="0.11.2", + version="0.12.0", description="Ecash wallet and mint for Bitcoin Lightning", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/conftest.py b/tests/conftest.py index 35b17b0..b823bc0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,8 +30,8 @@ class UvicornServer(multiprocessing.Process): settings.mint_lightning_backend = "FakeWallet" settings.mint_listen_port = 3337 settings.mint_database = "data/test_mint" + settings.mint_private_key = "TEST_PRIVATE_KEY" settings.mint_derivation_path = "0/0/0/0" - settings.mint_private_key = "privatekeyofthemint" dirpath = Path(settings.mint_database) if dirpath.exists() and dirpath.is_dir(): diff --git a/tests/test_cli.py b/tests/test_cli.py index eecdde4..fdbfdc2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -106,7 +106,7 @@ def test_send(mint, cli_prefix): @pytest.mark.asyncio def test_receive_tokenv3(mint, cli_prefix): runner = CliRunner() - token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIkhYUC1SSEZ2Tk5Cb2ZZT1FzY1RURnciLCAiQyI6ICIwMjQxOTgwYzk0NjY1ODU0MDZhODg3ZWI1Y2JkN2Y1N2U4NTc0MzdiNzE0MTkxYmUwOTQyNTJmYjAxZWViNGQ3OTQifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICItcXI2cF80bDNVQWQwSzBib2JSekdRIiwgIkMiOiAiMDIyOTA5YWJhYjg0N2RlODA0NjdmYjhjOTdiMDJiYjFjOGUwNWE0ZTFlYzBiYzI1MDY0MDUzMjg3YTNhYjViZDYyIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19" + token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIld6TEF2VW53SDlRaFYwQU1rMy1oYWciLCAiQyI6ICIwMmZlMzUxYjAyN2FlMGY1ZDkyN2U2ZjFjMTljMjNjNTc3NzRhZTI2M2UyOGExN2E2MTUxNjY1ZjU3NWNhNjMyNWMifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJDamFTeTcyR2dVOGwzMGV6bE5zZnVBIiwgIkMiOiAiMDNjMzM0OTJlM2ZlNjI4NzFhMWEzMDhiNWUyYjVhZjBkNWI1Mjk5YzI0YmVkNDI2ZjQ1YzZmNDg5N2QzZjc4NGQ5In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19" result = runner.invoke( 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 # therefore, we need to know the mint keyset already and have the mint URL in the db runner = CliRunner() - token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIk1OQzRXM2k3NjNSMFYwdkFncEdJQ1EiLCAiQyI6ICIwMjg2YWIyNDZlMWViZTI5Yjk0ZTMzMDgzMjE0NDZhNTRkOGE3NWEwOTQ2MjU4YmYyMzM1ZmJhOTA5Y2ZjY2VhMWYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI5OFM5MGRjRGtKb3Q1V1Z4QVJ3VWdRIiwgIkMiOiAiMDJlMTE0OGRkNzQ3OWJlNzIwMmI5OWVmZDIzNjllZTFhYTBhYTVhYzMyYjM1ODczMzk0YmNjYWU1MmFkZTYzYmUxIn1dfV19" + token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIi1oM0ZXMFFoX1FYLW9ac1V2c0RuNlEiLCAiQyI6ICIwMzY5Mzc4MzdlYjg5ZWI4NjMyNWYwOWUyOTIxMWQxYTI4OTRlMzQ2YmM1YzQwZTZhMThlNTk5ZmVjNjEwOGRmMGIifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI3d0VhNUgzZGhSRGRNZl94c1k3c3JnIiwgIkMiOiAiMDJiZmZkM2NlZDkxNjUyMzcxMDg2NjQxMzJiMjgxYjBhZjY1ZTNlZWVkNTY3MmFkZjM0Y2VhNzE5ODhhZWM1NWI1In1dfV19" result = runner.invoke( cli, [ @@ -142,7 +142,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix): @pytest.mark.asyncio def test_receive_tokenv2(mint, cli_prefix): runner = CliRunner() - token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ6dHRyMU9EdXBlZzA5Y1d2ckFVdWxRIiwgIkMiOiAiMDM3ZGQyNDYxZjFlOTg4Y2YxOWQyMmJhOTMxOTdlMmU2YmI5YTJmNjc5NDM4YTFiZjYwYmY0ZWJmZGJkNWUyYmM0In0sIHsiaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiekw5NDlTVzI4ZEpxaUMyOXl0bVZKQSIsICJDIjogIjAzOWIzOGIwM2QxY2NlNTU0NGZlYzM3YTM4ZGViZGZhMjUzMTc2ZTI3MWVlNDU3NjdkOTBkMWYwNWNmZGNhYzE2ZCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjI4ajhueVNMMU5kZCJdfV19" + token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJhUmREbzlFdW9yZUVfOW90enRNVVpnIiwgIkMiOiAiMDNhMzY5ZmUyN2IxYmVmOTg4MzA3NDQyN2RjMzc1NmU0NThlMmMwYjQ1NWMwYmVmZGM4ZjVmNTA3YmM5MGQxNmU3In0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiTEZQbFp6Ui1MWHFfYXFDMGhUeDQyZyIsICJDIjogIjAzNGNiYzQxYWY0ODIxMGFmNjVmYjVjOWIzOTNkMjhmMmQ5ZDZhOWE5MzI2YmI3MzQ2YzVkZmRmMTU5MDk1MzI2YyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjFjQ05JQVoyWC93MSJdfV19" result = runner.invoke( cli, [*cli_prefix, "receive", token], @@ -155,7 +155,7 @@ def test_receive_tokenv2(mint, cli_prefix): @pytest.mark.asyncio def test_receive_tokenv1(mint, cli_prefix): runner = CliRunner() - token = "W3siaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiZDRJQjY3LU1iOGpDS242clZoREMyUSIsICJDIjogIjAzM2E5M2NiNjhjZWZhZjFmNTJkN2NhZTMzNWVhN2ExYmQ4MDFiZTVmZDE5OGI5MWQzM2FmMDJlNjk3NWI0NzdmMiJ9LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInAtc0QzeW5PSE5ISmdjVlRUZDVVYVEiLCAiQyI6ICIwMzg5NGMwMzdiNGMyMWRmNmNiNzExMmJjZjE0OGMwOTlmYTM2ZDk4MTZhYjcwN2VmMTM0N2ZmODEyZjQ4N2MzNmEifV0=" + token = "W3siaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiRnVsc2dzMktQV1FMcUlLX200SzgwQSIsICJDIjogIjAzNTc4OThlYzlhMjIxN2VhYWIxZDc3YmM1Mzc2OTUwMjJlMjU2YTljMmMwNjc0ZDJlM2FiM2JiNGI0ZDMzMWZiMSJ9LCB7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInJlRDBDazVNS2xBTUQ0dWk2OEtfbEEiLCAiQyI6ICIwMjNkODNkNDE0MDU0NWQ1NTg4NjUyMzU5YjJhMjFhODljODY1ZGIzMzAyZTkzMTZkYTM5NjA0YTA2ZDYwYWQzOGYifV0=" result = runner.invoke( cli, [*cli_prefix, "receive", token], diff --git a/tests/test_mint.py b/tests/test_mint.py index e50e6be..534d2b1 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -52,11 +52,37 @@ async def 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 async def test_keysets(ledger: Ledger): assert len(ledger.keysets.keysets) assert len(ledger.keysets.get_ids()) - assert ledger.keyset.id == "XQM1wwtQbOXE" + assert ledger.keyset.id == "1cCNIAZ2X/w1" @pytest.mark.asyncio @@ -79,7 +105,7 @@ async def test_mint(ledger: Ledger): assert promises[0].amount == 8 assert ( promises[0].C_ - == "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0" + == "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15" ) @@ -107,5 +133,5 @@ async def test_generate_promises(ledger: Ledger): promises = await ledger._generate_promises(blinded_messages_mock) assert ( promises[0].C_ - == "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0" + == "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15" )