Skip to content

Commit

Permalink
allow passing nkey seed as string (#468)
Browse files Browse the repository at this point in the history
* add nkeys str option
  • Loading branch information
databasedav authored Aug 28, 2023
1 parent 709a2c6 commit efe0c47
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 42 deletions.
36 changes: 21 additions & 15 deletions nats/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def __init__(self) -> None:

# file that contains the nkeys seed and its public key as a string.
self._nkeys_seed: Optional[str] = None
self._nkeys_seed_str: Optional[str] = None
self._public_nkey: Optional[str] = None

self.options: Dict[str, Any] = {}
Expand Down Expand Up @@ -312,6 +313,7 @@ async def connect(
user_jwt_cb: Optional[JWTCallback] = None,
user_credentials: Optional[Credentials] = None,
nkeys_seed: Optional[str] = None,
nkeys_seed_str: Optional[str] = None,
inbox_prefix: Union[str, bytes] = DEFAULT_INBOX_PREFIX,
pending_size: int = DEFAULT_PENDING_SIZE,
flush_timeout: Optional[float] = None,
Expand Down Expand Up @@ -428,6 +430,7 @@ async def subscribe_handler(msg):
self._user_jwt_cb = user_jwt_cb
self._user_credentials = user_credentials
self._nkeys_seed = nkeys_seed
self._nkeys_seed_str = nkeys_seed_str

# Customizable options
self.options["verbose"] = verbose
Expand Down Expand Up @@ -461,7 +464,7 @@ async def subscribe_handler(msg):
if user or password or token or server_auth_configured:
self._auth_configured = True

if self._user_credentials is not None or self._nkeys_seed is not None:
if self._user_credentials is not None or self._nkeys_seed is not None or self._nkeys_seed_str is not None:
self._auth_configured = True
self._setup_nkeys_connect()

Expand Down Expand Up @@ -595,35 +598,38 @@ def sig_cb(nonce: str) -> bytes:
self._signature_cb = sig_cb

def _setup_nkeys_seed_connect(self) -> None:
assert self._nkeys_seed, "Client.connect must be called first"
assert self._nkeys_seed or self._nkeys_seed_str, "Client.connect must be called first"
import os

import nkeys

seed = None
creds = self._nkeys_seed
with open(creds, 'rb') as f:
seed = bytearray(os.fstat(f.fileno()).st_size)
f.readinto(seed) # type: ignore[attr-defined]
kp = nkeys.from_seed(seed)
def _get_nkeys_seed() -> nkeys.KeyPair:
import os

if self._nkeys_seed_str:
seed = bytearray(self._nkeys_seed_str.encode())
else:
creds = self._nkeys_seed
with open(creds, 'rb') as f:
seed = bytearray(os.fstat(f.fileno()).st_size)
f.readinto(seed) # type: ignore[attr-defined]
key_pair = nkeys.from_seed(seed)
del seed
return key_pair

kp = _get_nkeys_seed()
self._public_nkey = kp.public_key.decode()
kp.wipe()
del kp
del seed

def sig_cb(nonce: str) -> bytes:
seed = None
with open(creds, 'rb') as f:
seed = bytearray(os.fstat(f.fileno()).st_size)
f.readinto(seed) # type: ignore[attr-defined]
kp = nkeys.from_seed(seed)
kp = _get_nkeys_seed()
raw_signed = kp.sign(nonce.encode())
sig = base64.b64encode(raw_signed)

# Best effort attempt to clear from memory.
kp.wipe()
del kp
del seed
return sig

self._signature_cb = sig_cb
Expand Down
2 changes: 1 addition & 1 deletion nats/aio/msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async def nak(self, delay: Union[int, float, None] = None) -> None:
payload = Msg.Ack.Nak
json_args = dict()
if delay:
json_args['delay'] = int(delay * 10**9) # to seconds to ns
json_args['delay'] = int(delay * 10**9) # from seconds to ns
if json_args:
payload += (b' ' + json.dumps(json_args).encode())
await self._client.publish(self.reply, payload)
Expand Down
66 changes: 40 additions & 26 deletions tests/test_client_nkeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,56 @@ class ClientNkeysAuthTest(NkeysServerTestCase):

@async_test
async def test_nkeys_connect(self):
if not nkeys_installed:
pytest.skip("nkeys not installed")
import os

config_file = get_config_file("nkeys/foo-user.nk")
seed = None
with open(config_file, 'rb') as f:
seed = bytearray(os.fstat(f.fileno()).st_size)
f.readinto(seed)
args_list = [
{
'nkeys_seed': config_file
},
{
'nkeys_seed_str': seed.decode()
},
]
for nkeys_args in args_list:
if not nkeys_installed:
pytest.skip("nkeys not installed")

nc = NATS()
nc = NATS()

future = asyncio.Future()
future = asyncio.Future()

async def error_cb(e):
nonlocal future
future.set_result(True)
async def error_cb(e):
nonlocal future
future.set_result(True)

await nc.connect(
["tls://127.0.0.1:4222"],
error_cb=error_cb,
connect_timeout=10,
nkeys_seed=get_config_file("nkeys/foo-user.nk"),
allow_reconnect=False,
)
await nc.connect(["tls://127.0.0.1:4222"],
error_cb=error_cb,
connect_timeout=10,
allow_reconnect=False,
**nkeys_args)

async def help_handler(msg):
await nc.publish(msg.reply, b'OK!')
async def help_handler(msg):
await nc.publish(msg.reply, b'OK!')

await nc.subscribe("help", cb=help_handler)
await nc.flush()
msg = await nc.request("help", b'I need help')
self.assertEqual(msg.data, b'OK!')
await nc.subscribe("help", cb=help_handler)
await nc.flush()
msg = await nc.request("help", b'I need help')
self.assertEqual(msg.data, b'OK!')

await nc.subscribe("bar", cb=help_handler)
await nc.flush()
await nc.subscribe("bar", cb=help_handler)
await nc.flush()

await asyncio.wait_for(future, 1)
await asyncio.wait_for(future, 1)

msg = await nc.request("help", b'I need help')
self.assertEqual(msg.data, b'OK!')
msg = await nc.request("help", b'I need help')
self.assertEqual(msg.data, b'OK!')

await nc.close()
await nc.close()


class ClientJWTAuthTest(TrustedServerTestCase):
Expand Down

0 comments on commit efe0c47

Please # to comment.