commit 78f77e6387ed25dd826afed43df0d3ae9dd83332
parent 32abeea5648f659a1f891cfc9238aa48293de7a2
Author: Carlosokumu <carlosokumu254@gmail.com>
Date: Fri, 13 Mar 2026 20:01:44 +0300
merge lash/store-key
Diffstat:
16 files changed, 268 insertions(+), 57 deletions(-)
diff --git a/dummy/tests/account.py b/dummy/tests/account.py
@@ -20,21 +20,24 @@ class TestAccount(unittest.TestCase):
def test_account_lock(self):
idx = AccountIndex(self.uidx)
- idx.add('FOO', 'bar.baz')
- idx.add('FOO', 'bar.baz')
+ # missing account type
+ with self.assertRaises(AttributeError):
+ idx.add('bar/baz', sym='FOO')
+ idx.add('liability/bar/baz', sym='FOO')
+ idx.add('liability/bar/baz', sym='FOO')
with self.assertRaises(AccountError):
- idx.add('FOO', 'bar.baz-')
+ idx.add('asset/bar/baz-', sym='FOO')
with self.assertRaises(AccountError):
- idx.add('BAZ', 'foo.bar')
- self.assertFalse(idx.check('BAR', 'foo.baz'))
- self.assertTrue(idx.check('FOO', 'bar.baz'))
+ idx.add('asset/foo/bar', sym='BAZ')
+ self.assertFalse(idx.check('BAR', 'liability/foo/baz'))
+ self.assertTrue(idx.check('FOO', 'liability/bar/baz'))
def test_account_list(self):
idx = AccountIndex(self.uidx)
- idx.add('FOO', 'bar.bar')
- idx.add('FOO', 'bar.baz')
- idx.add('BAR', 'foo.baz')
+ idx.add('asset/bar/bar', sym='FOO')
+ idx.add('liability/bar/baz', sym='FOO')
+ idx.add('asset/foo/baz', sym='BAR')
v = list(idx)
logg.debug('results {}'.format(v))
self.assertEqual(len(v), 3)
diff --git a/dummy/tests/store.py b/dummy/tests/store.py
@@ -121,5 +121,41 @@ class TestStore(unittest.TestCase):
self.assertEqual(len(ledger.entries), 2)
+ def test_store_restore(self):
+ uidx = UnitIndex('FOO')
+ wallet = DemoWallet()
+ acl = ACL.from_wallet(wallet)
+ ledger = Ledger(uidx, base=self.parent, acl=acl, wallet=wallet)
+ store = LedgerStore(self.store, ledger)
+
+ dst = EntryPart('FOO', 'asset', 'foo', 1337)
+ src = EntryPart('FOO', 'income', 'foo', 1337, debit=True)
+ o = Entry(ledger.next_serial(), datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg, unitindex=uidx)
+ o.add_part(src, debit=True)
+ o.add_part(dst)
+ o.sign(wallet)
+ store.add_entry(o)
+
+ ref = str(uuid.uuid4())
+ parent = o.sum()[0]
+ description = 'barbarbar'
+ dtreg = datetime.datetime.now()
+ dst = EntryPart('FOO', 'expense', 'bar', 4200)
+ src = EntryPart('FOO', 'liability', 'bar', 4200, debit=True)
+ o = Entry(ledger.next_serial(), datetime.datetime.strptime('2025-11-12', '%Y-%m-%d'), parent=parent, ref=ref, description=description, tx_datereg=dtreg, unitindex=uidx)
+ o.add_part(src, debit=True)
+ o.add_part(dst)
+ o.sign(wallet)
+ store.add_entry(o)
+
+ ledger.truncate()
+ s = ledger.to_string()
+ ledger = Ledger.from_string(s)
+ store = LedgerStore(self.store, ledger)
+ store.restore()
+
+ self.assertEqual(len(ledger.entries), 2)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/dummy/usawa/account.py b/dummy/usawa/account.py
@@ -1,3 +1,4 @@
+import enum
import logging
from .error import AccountError
@@ -5,32 +6,81 @@ from .error import AccountError
logg = logging.getLogger('account')
-def default_check(path):
- parts = path.split('.')
+def check_path_parts(path):
+ parts = path.split('/')
for v in parts:
if not v.isalnum():
raise AccountError('invalid part: ' + v)
- return True
+ #return True
+ typ = getattr(AccountType, parts[0].lower())
+ return (typ, parts,)
+
+
+def from_account_path(p, sym=None, typ=None):
+ if sym != None:
+ p = sym + '.' + p
+ o = p.split('.')
+ logg.debug('have {} {} {}'.format(p, sym, typ))
+ if len(o) != 2:
+ raise ValueError('account path should have zero or one symbol specifier')
+ sym = o[0]
+ o = check_path_parts(o[1])
+ typ = o[0]
+ path = o[1]
+
+ return (sym, typ, path,)
+
+
+class AccountType(enum.Enum):
+ liability = 'Liability'
+ asset = 'Asset'
+ income = 'Income'
+ expense = 'Expense'
+ imprt = 'Import'
+ export = 'Export'
+
+
+class Account:
+
+ path_parser = from_account_path
+
+ def __init__(self, sym, typ, segments):
+ if not isinstance(typ, AccountType):
+ raise ValueError('invalid account type')
+ self.sym = sym
+ self.typ = typ
+ self.segments = segments
+
+
+ @staticmethod
+ def from_path(path, sym=None, typ=None):
+ o = Account.path_parser(path, sym=sym, typ=typ)
+ return Account(o[0], o[1], o[2])
+
+
+ def to_path(self):
+ path = self.segments.join('/')
+ path = '{}.{}/{}'.format(self.sym, self.typ, path)
class AccountIndex:
- def __init__(self, unitindex, pathvalidator=default_check):
+ def __init__(self, unitindex): #, pathvalidator=default_check):
self.uidx = unitindex
self.accounts = {}
self.locked = False
- self.validate = pathvalidator
+ #self.validate = pathvalidator
self.iterval = None
- def add(self, sym, path):
+ def add(self, path, sym=None, typ=None):
+ account = Account.from_path(path, sym=sym, typ=typ)
try:
- sym = self.uidx.sym(sym)
+ sym = self.uidx.sym(account.sym)
except KeyError:
raise AccountError('unknown unit ' + sym)
if self.locked:
raise AccountError('account index locked')
- self.validate(path)
if self.accounts.get(sym) == None:
self.accounts[sym] = []
elif path in self.accounts[sym]:
diff --git a/dummy/usawa/config.py b/dummy/usawa/config.py
@@ -9,6 +9,8 @@ logger = logging.getLogger(__name__)
def load():
cfg = confini.Config(__datadir, env_prefix='USAWA')
+ cfg.censor('SIGS_KEY_PASSPHRASE')
+ cfg.censor('SIGS_DEFAULT_PRIVATE_KEY')
cfg.process()
if "VALKEY_HOST" in cfg.store:
diff --git a/dummy/usawa/crypto.py b/dummy/usawa/crypto.py
@@ -1,11 +1,15 @@
import logging
+import hashlib
import rencode
import lxml.etree
import nacl.signing
+import nacl.secret
import nacl.exceptions
+from usawa.error import VerifyError
+
AXX_ALL = 0xffffffff
AXX_ANY = 0x01
@@ -44,6 +48,21 @@ class DID:
return 'did:' + self.m + ':' + self.v
+def key_from_export(v, passphrase='', did=None):
+ if passphrase == None:
+ passphrase = b''
+ if isinstance(passphrase, str):
+ passphrase = passphrase.encode('utf-8')
+ h = hashlib.sha256()
+ h.update(passphrase)
+ z = h.digest()
+ o = nacl.secret.SecretBox(z)
+ r = None
+ try:
+ r = o.decrypt(v)
+ except nacl.exceptions.CryptoError:
+ raise VerifyError('decrypt fail')
+ return r
class Wallet:
"""Wallet is an unimplemented class defining the interface for wallet operations.
@@ -146,6 +165,29 @@ class Wallet:
raise NotImplementedError
+ def export(self, passphrase=None):
+ if passphrase == None:
+ passphrase = b''
+ elif isinstance(passphrase, str):
+ passphrase = passphrase.encode('utf-8')
+ if len(passphrase) == 0:
+ logg.warning('exporting key with no passphrase')
+ h = hashlib.sha256()
+ h.update(passphrase)
+ z = h.digest()
+ o = nacl.secret.SecretBox(z)
+ k = self.privkey()
+ r = o.encrypt(k)
+ if len(r) != len(k) + o.NONCE_SIZE + o.MACBYTES:
+ raise VerifyError()
+ return r
+
+
+ @staticmethod
+ def from_export(v, passphrase=None):
+ raise NotImplementedError()
+
+
"""Generate an identity XML tree entry from the wallet.
The element generated is valid to be inserted as an identity sub-element in the ledger element.
@@ -202,6 +244,8 @@ class DemoWallet(Wallet):
publickey = nacl.signing.VerifyKey(publickey)
self.pubk = publickey
self.didval = DID(v=self.pubkey().hex())
+ logg.debug('wallet created {}'.format(self.pubkey().hex()))
+
"""Implements usawa.Wallet.sign
"""
@@ -236,6 +280,12 @@ class DemoWallet(Wallet):
return r
+ @staticmethod
+ def from_export(v, passphrase=None):
+ k = key_from_export(v, passphrase=passphrase)
+ return DemoWallet(privatekey=k)
+
+
class ACL:
"""ACL defines public keys to accept signatures from, and for which purpose.
diff --git a/dummy/usawa/data/usawa.ini b/dummy/usawa/data/usawa.ini
@@ -6,5 +6,6 @@ socket_file_path=
[sigs]
default_public_key=
default_private_key =
+key_passphrase =
[fs_resolver]
store_path=
diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py
@@ -720,6 +720,15 @@ class Ledger:
return self.cur
+ """Returns the serial of the latest entry added to the ledger.
+
+ :return: Serial.
+ :rtype: int
+ """
+ def current_serial(self):
+ return self.serial
+
+
"""Generate canonical XML for signature material.
:return: Signature material.
diff --git a/dummy/usawa/runnable/add.py b/dummy/usawa/runnable/add.py
@@ -6,6 +6,7 @@ import argparse
import uuid
import datetime
+import usawa.config
from usawa import Ledger, Entry, EntryPart, DemoWallet, UnitIndex, load, Asset
from usawa.constant import CATEGORIES
from usawa.store import LedgerStore
@@ -148,10 +149,12 @@ ledger_tree = load(arg.ledger_xml_file)
uidx = UnitIndex.from_tree(ledger_tree)
ledger = Ledger.from_tree(ledger_tree)
+cfg = usawa.config.load()
db = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
store = LedgerStore(db, ledger)
-pk = store.get_key()
-wallet = DemoWallet(privatekey=pk)
+#pk = store.get_key()
+#wallet = DemoWallet(privatekey=pk)
+wallet = store.get_key(DemoWallet, passphrase=cfg.get('SIGS_KEY_PASSPHRASE'))
ledger.set_wallet(wallet)
dt = datetime.datetime.now()
diff --git a/dummy/usawa/runnable/create.py b/dummy/usawa/runnable/create.py
@@ -7,6 +7,7 @@ import datetime
import uuid
from usawa import Ledger, DemoWallet, UnitIndex, ACL
+import usawa.config
from usawa.store import LedgerStore
from whee.valkey import ValkeyStore
@@ -142,12 +143,15 @@ pk = None
wallet = None
dt = datetime.datetime.now()
+cfg = usawa.config.load()
+
try:
- pk = store.get_key()
+ #pk = store.get_key()
+ wallet = store.get_key(DemoWallet, passphrase=cfg.get('SIGS_KEY_PASSPHRASE'))
except FileNotFoundError:
logg.info('no default key found')
wallet = DemoWallet()
- store.add_key(wallet)
+ store.add_key(wallet, passphrase=cfg.get('SIGS_KEY_PASSPHRASE'))
if wallet == None:
wallet = DemoWallet(privatekey=pk)
logg.info('loaded existing key. {}'.format(wallet.pubkey().hex()))
diff --git a/dummy/usawa/runnable/export.py b/dummy/usawa/runnable/export.py
@@ -6,6 +6,7 @@ import argparse
import uuid
import datetime
+import usawa.config
from usawa import Ledger, Entry, EntryPart, DemoWallet, UnitIndex, load, ACL
from usawa.constant import CATEGORIES
from usawa.store import LedgerStore
@@ -67,10 +68,12 @@ ctx = Context.from_args(arg)
#ledger = Ledger.from_tree(ledger_tree)
ledger = Ledger.from_file(arg.ledger_xml_file)
+cfg = usawa.config.load()
storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
store = LedgerStore(storedb, ledger)
-pk = store.get_key()
-wallet = DemoWallet(privatekey=pk)
+#pk = store.get_key()
+#wallet = DemoWallet(privatekey=pk)
+wallet = store.get_key(DemoWallet, passphrase=cfg.get('SIGS_KEY_PASSPHRASE'))
acl = ACL.from_wallet(wallet)
store.load(acl=acl)
resolve = FSResolver(ctx.output)
diff --git a/dummy/usawa/runnable/import.py b/dummy/usawa/runnable/import.py
@@ -6,6 +6,7 @@ import argparse
import uuid
import datetime
+import usawa.config
from usawa import Ledger, Entry, EntryPart, DemoWallet, load, ACL
from usawa.constant import CATEGORIES
from usawa.store import LedgerStore
@@ -63,6 +64,7 @@ ctx = Context.from_args(arg)
ledger = Ledger.from_file(arg.ledger_xml_file)
+cfg = usawa.config.load()
storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
store = LedgerStore(storedb, ledger)
#pk = store.get_key()
diff --git a/dummy/usawa/runnable/server.py b/dummy/usawa/runnable/server.py
@@ -3,6 +3,7 @@ import argparse
import signal
#import threading
+import usawa.config
from usawa import Ledger, Entry, EntryPart, DemoWallet, ACL, UnitIndex, load
from usawa.context import Context
from usawa.service import Handler, UnixServer
diff --git a/dummy/usawa/runnable/view.py b/dummy/usawa/runnable/view.py
@@ -6,6 +6,7 @@ import argparse
import uuid
import datetime
+import usawa.config
from usawa import Ledger, Entry, EntryPart, DemoWallet, UnitIndex, load, ACL
from usawa.constant import CATEGORIES
from usawa.store import LedgerStore
@@ -67,10 +68,12 @@ ctx = Context.from_args(arg)
#ledger = Ledger.from_tree(ledger_tree)
ledger = Ledger.from_file(arg.ledger_xml_file)
+cfg = usawa.config.load()
storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
store = LedgerStore(storedb, ledger)
-pk = store.get_key()
-wallet = DemoWallet(privatekey=pk)
+#pk = store.get_key()
+#wallet = DemoWallet(privatekey=pk)
+wallet = store.get_key(DemoWallet, passphrase=cfg.get('SIGS_KEY_PASSPHRASE'))
acl = ACL.from_wallet(wallet)
store.load(acl=acl)
sys.stdout.write(ledger.to_string())
diff --git a/dummy/usawa/service.py b/dummy/usawa/service.py
@@ -1,6 +1,7 @@
import logging
import socket
import os
+import threading
from whee import Interface
from usawa.store import LedgerStore
@@ -241,14 +242,8 @@ class SocketServer:
break
if sckc != None:
logg.info('connect: {}'.format(address))
- #th = threading.Thread(target=self.receive, args=(sckc, address))
- #th.start()
- try:
- self.receive(sckc, address)
- except ConnectionResetError:
- logg.warning('connection reset')
- sckc.close()
- break
+ th = threading.Thread(target=self.receive, args=(sckc, address))
+ th.start()
"""Implements whee.Interface
@@ -316,6 +311,7 @@ class SocketServer:
v = b'\x01'
logg.debug('harvest {}'.format(v.hex()))
sckc.sendall(v)
+ sckc.close()
def __str__(self):
diff --git a/dummy/usawa/storage/ledger_repository.py b/dummy/usawa/storage/ledger_repository.py
@@ -12,7 +12,7 @@ from usawa.storage.xml_utils import (
resolve_namespace,
)
from usawa.asset import Asset
-from usawa.crypto import ACL, DemoWallet
+from usawa.crypto import ACL, DemoWallet, Wallet
from usawa.error import VerifyError
from usawa.ledger import Ledger
from usawa.resolve.fs import FSResolver
@@ -54,6 +54,8 @@ def sha256_verify(k, v=None):
class LedgerRepository:
"""Repository that wraps LedgerStore and handles mapping"""
+ wallet_class = DemoWallet
+
def __init__(
self,
ledger_path=None,
@@ -81,7 +83,7 @@ class LedgerRepository:
self.resolver = FSResolver(self.cfg.get("FS_RESOLVER_STORE_PATH"))
logg.info("Initailized wallet, pubkey: %s", self._wallet.pubkey().hex())
- def _init_store(self, write=False) -> tuple[LedgerStore, Ledger, DemoWallet]:
+ def _init_store(self, write=False) -> tuple[LedgerStore, Ledger, Wallet]:
ledger_tree = load(self.ledger_path)
ledger = Ledger.from_tree(ledger_tree)
@@ -91,19 +93,38 @@ class LedgerRepository:
logg.info("init store for write")
self.store = LedgerStore(self.valkey_store, ledger)
+ if self._wallet is None:
+ # pk = self.store.get_key()
+ # if pk is None:
+ # raise ValueError("No private key found in store")
+ # self._wallet = DemoWallet(privatekey=pk)
+ self._wallet = self.store.get_key(wallet_class)
else:
logg.info("init store for read")
self.store = LedgerStore(self.valkey_store, ledger)
- try:
- _ = self.store.get_key()
- logg.info(f"Loaded wallet, pubkey: {self._wallet.pubkey().hex()[:16]}...")
- except FileNotFoundError:
- logg.warning("No private key found in store, initializing a a default one")
- try:
- self.store.add_key(wallet=self._wallet)
- logg.info("Stored new private key successfully")
- except Exception as e:
- logg.warning(f"Could not store new key: {e}")
+
+ if self._wallet is None:
+ try:
+ # pk = self.store.get_key()
+ # self._wallet = DemoWallet(privatekey=pk)
+ self._wallet = self.store.get_key(wallet_class)
+ logg.info(
+ f"Loaded wallet, pubkey: {self._wallet.pubkey().hex()[:16]}..."
+ )
+
+ except FileNotFoundError:
+ logg.warning(
+ "No private key found in store, initializing a a default one"
+ )
+ privkey = bytes.fromhex(self.cfg.get("SIGS_DEFAULT_PRIVATE_KEY"))
+ self._wallet = DemoWallet(privatekey=privkey)
+
+ # Add key to store
+ try:
+ self.store.add_key(wallet=self._wallet)
+ logg.info("Stored new private key successfully")
+ except Exception as e:
+ logg.warning(f"Could not store new key: {e}")
logg.debug(
"wallet pk: %s pubk: %s",
diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py
@@ -210,11 +210,9 @@ class LedgerStore(Interface):
return Asset.deserialize(v, digest)
- """Flush ledger and load all entries from store.
+ """Load all entries from store, oldest to newest.
- The existing state will always be lost.
-
- If the load fails, the ledger will be reset before returning.
+ Must be called on an unused ledger instance. Using with a ledger that contains or has contains entries is undefined.
:raises FileNotFoundError: If an entry cannot be found.
"""
@@ -229,6 +227,25 @@ class LedgerStore(Interface):
self.ledger.add_entry(o)
+ """Load all entries from store, newest to oldest.
+
+ Must be called on an unused ledger instance. Using with a ledger that contains or has contains entries is undefined.
+
+ :raises FileNotFoundError: If an entry cannot be found.
+ """
+ def restore(self, until=0, acl=None):
+ logg.debug('restore ledger from store {}'.format(self.ledger))
+ i = self.ledger.current_serial()
+ while i > until:
+ logg.debug('get entry serial {} ledger {}'.format(i, self.ledger))
+ #try:
+ o = self.get_entry(i, acl=acl)
+ #except FileNotFoundError:
+ # break
+ self.ledger.add_entry(o, check_parent=False)
+ i -= 1
+
+
"""Add signing key to the store.
If this is the first key in the store, it will be set as default.
@@ -239,10 +256,11 @@ class LedgerStore(Interface):
:type acl: usawa.ACL
:param default: If True, this key will be set as default key.
:type default: bool
- :todo: Currently the signing key is stored literally. It needs encryption!
+ :param passphrase: Passphrase to encrypt the key with.
+ :type passphrase: bytes
:todo: Implement the ACL lookup
"""
- def add_key(self, wallet, acl=None, default=False):
+ def add_key(self, wallet, acl=None, default=False, passphrase=None):
k = pfx_key()
try:
self.__o.get(k)
@@ -252,24 +270,33 @@ class LedgerStore(Interface):
if default:
self.__o.put(k, pubkey, exist_ok=True)
k = pfx_key(pubkey=pubkey)
- self.__o.put(k, wallet.privkey())
+ v = wallet.export(passphrase=passphrase)
+ self.__o.put(k, v)
- """Get corresponding private key from the store.
+ """Get a newly instantiated wallet object from a private key in the store.
If public key is not supplied, will retrieve the default private key.
+ :param wallet_class: Wallet class to use to instantiate a Wallet object from private key material.
+ :type: usawa.crypto.Wallet
:param pubkey: Public key to retrieve private key for.
:type pubkey: bytes
- :return: Resulting key
- :rtype: bytes
+ :param passphrase: Passphrase to decrypt the key with.
+ :type passphrase: bytes
+ :raises FileNotFoundError: No key exists.
+ :raises usawa.error.VerifyError: Key decryption failed.
+ :return: Resulting wallet
+ :rtype: usawa.crypto.Wallet
"""
- def get_key(self, pubkey=None):
+ def get_key(self, wallet_class, pubkey=None, passphrase=None):
if pubkey == None:
k = pfx_key()
pubkey = self.__o.get(k)
k = pfx_key(pubkey=pubkey)
- return self.__o.get(k)
+ #return self.__o.get(k)
+ r = self.__o.get(k)
+ return wallet_class.from_export(r, passphrase=passphrase)
"""Implements whee.Interface.put