-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add KVStoreClient, examples #28
Merged
Merged
Changes from 3 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
f63e78c
add KVStoreClient, 6_totp example.
mmou 1dfff39
address PR comments
mmou 929b075
add 4_simple_storage example
mmou 8bb7ec5
fixes
mmou e7887bd
wip
mmou 3d63ed2
clean up
mmou a535682
address pr coments
mmou 9183b58
fixes
mmou db367bf
fix
mmou 6916555
fix
mmou 883d067
did it
mmou 9b67d07
fixes
mmou 93ad5b4
some fixes
mmou e93ee36
fix
mmou b54c145
fix
mmou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
dist/ | ||
__pycache__/ | ||
.mypy_cache/ | ||
*.egg-info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
#!/usr/bin/env python3 | ||
|
||
################################### | ||
# WHAT IS IN THIS EXAMPLE? | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# | ||
# Keybase has added an encrypted key-value store intended to support | ||
# security-conscious bot development with persistent state. It is a place to | ||
# store small bits of data that are | ||
# (1) encrypted for a team or user (via the user's implicit self-team: e.g. | ||
# alice,alice), | ||
# (2) persistent across logins | ||
# (3) fast and durable. | ||
# | ||
# It supports putting, getting, listing and deleting. There is also a concurrency | ||
# primitive, but check out the other example for that. A team has many | ||
# namespaces, a namespace has many entryKeys, and an entryKey has one current | ||
# entryValue. Namespaces and entryKeys are in cleartext, and the Keybase client | ||
# service will encrypt and sign the entryValue on the way in (as well as | ||
# decrypt and verify on the way out) so keybase servers cannot see it or lie | ||
# about it. | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# | ||
# This example shows how you can use KVStoreClient to interact with the team | ||
# encrypted key-value store. | ||
# | ||
# This example propagates errors to the chat user and is not concurrency safe. | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
################################### | ||
|
||
import asyncio | ||
import logging | ||
import os | ||
import sys | ||
from enum import Enum | ||
|
||
from pykeybasebot import Bot, EventType | ||
|
||
logging.basicConfig(level=logging.DEBUG) | ||
|
||
if "win32" in sys.platform: | ||
# Windows specific event-loop policy | ||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) | ||
|
||
|
||
class KVMsg(Enum): | ||
PUT = "put" | ||
GET = "get" | ||
DELETE = "delete" | ||
LIST = "list" | ||
|
||
|
||
class KVHandler: | ||
""" | ||
KVHandler handles commands sent via chat to use the team key-value store. | ||
|
||
KVHandler listens to chat messages of the form: | ||
`!storage put <namespace> <key> <value> (<revision>)` | ||
`!storage {get|delete} <namespace> <key>` | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`!storage list` | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`!storage list <namespace>` | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
It expects properly formatted commands. | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
|
||
MSG_PREFIX = "!storage" | ||
|
||
async def __call__(self, bot, event): | ||
members_type = event.msg.channel.members_type | ||
if not ( | ||
event.type == EventType.CHAT | ||
and (members_type == "team" or members_type == "impteamnative") | ||
): | ||
return | ||
|
||
channel = event.msg.channel | ||
|
||
# support teams and implicit self teams | ||
team = ( | ||
channel.name if members_type == "team" else "{0},{0}".format(channel.name) | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
msg = event.msg.content.text.body.split(" ") | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if msg[0] != self.MSG_PREFIX: | ||
return | ||
|
||
if len(msg) == 2 and msg[1] == KVMsg.LIST.value: | ||
# !storage list | ||
res = await bot.kvstore.list_namespaces(team) | ||
await bot.chat.send(channel, str(res)) | ||
return | ||
if len(msg) == 3 and msg[1] == KVMsg.LIST.value: | ||
# !storage list <namespace> | ||
namespace = msg[2] | ||
res = await bot.kvstore.list_entrykeys(team, namespace) | ||
await bot.chat.send(channel, str(res)) | ||
return | ||
if len(msg) == 4: | ||
(action, namespace, key) = (msg[1], msg[2], msg[3]) | ||
if action == KVMsg.GET.value: | ||
# !storage get <namespace> <key> | ||
res = await bot.kvstore.get(team, namespace, key) | ||
await bot.chat.send(channel, str(res)) | ||
return | ||
if action == KVMsg.DELETE.value: | ||
# !storage delete <namespace> <key> | ||
try: | ||
res = await bot.kvstore.delete(team, namespace, key) | ||
await bot.chat.send(channel, str(res)) | ||
except Exception as e: | ||
await bot.chat.send(channel, str(e)) | ||
return | ||
if len(msg) == 5 or len(msg) == 6: | ||
# !storage put <namespace> <key> <value> (<revision>) | ||
(action, namespace, key, value) = (msg[1], msg[2], msg[3], msg[4]) | ||
revision = int(msg[5]) if len(msg) == 6 else None | ||
if action == KVMsg.PUT.value: | ||
try: | ||
res = await bot.kvstore.put( | ||
team, namespace, key, value, revision=revision | ||
) | ||
await bot.chat.send(channel, str(res)) | ||
except Exception as e: | ||
await bot.chat.send(channel, str(e)) | ||
return | ||
|
||
|
||
username = "user3" # "yourbot" | ||
|
||
bot = Bot( | ||
username=username, | ||
paperkey=os.environ["KEYBASE_PAPERKEY"], | ||
handler=KVHandler(), | ||
keybase="/home/user/Documents/repos/go/src/github.com/keybase/client/go/keybase/keybase", | ||
) | ||
|
||
asyncio.run(bot.start({})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
#!/usr/bin/env python3 | ||
|
||
################################### | ||
# WHAT IS IN THIS EXAMPLE? | ||
# | ||
# Keybase has added an encrypted key-value store; see 4_simple_storage.py for | ||
# more information. | ||
# | ||
# This example shows how you can build a simple TOTP bot that makes use of | ||
# the team encrypted key-value store, which we interact with using KVStoreClient. | ||
# | ||
# This example does minimal error handling and is not concurrency safe. | ||
################################### | ||
|
||
import asyncio | ||
import json | ||
import logging | ||
import os | ||
import sys | ||
from enum import Enum | ||
|
||
import pyotp | ||
|
||
from pykeybasebot import Bot, EventType | ||
|
||
logging.basicConfig(level=logging.DEBUG) | ||
|
||
if "win32" in sys.platform: | ||
# Windows specific event-loop policy | ||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) | ||
|
||
|
||
class TotpMsg(Enum): | ||
PROVISION = "provision" | ||
REPROVISION = "reprovision" | ||
REMOVE = "remove" | ||
NOW = "now" | ||
URI = "uri" | ||
LIST = "list" | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class TotpHandler: | ||
""" | ||
TotpHandler handles commands sent via chat and uses the team key-value store to | ||
provide TOTP client functionality. | ||
|
||
TotpHandler listens to chat messages of the form: | ||
|
||
`!totp {provision|reprovision|remove|now|uri} <issuer>` and | ||
`!totp list` | ||
|
||
For each provisioned key, the handler stores in the namespace "totp" one | ||
row, with the key "<issuer" and the json blob value "{"secret": <16 char base32 | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
secret>}". | ||
|
||
This Keybase bot can be used in place of MFA apps like Google Authenticator and Authy | ||
for login processes that require two-step verification. This bot uses the | ||
pyotp library, so it is also compatible with other MFA apps. | ||
|
||
A Keybase TOTP bot could be useful for logins where you're already using a strong | ||
password, but are required to also use TOTP. Or it could be perfect for adding | ||
replay attack resistance to the physical keypad lock protecting your team's ice | ||
cream freezer.... Note that depending on the login process and how you use this bot | ||
with your Keybase team setup, this bot may not be appropriate for threat models | ||
that require a physical "second factor" device. | ||
|
||
For more information on TOTP, see https://pyotp.readthedocs.io/en/latest/, | ||
https://tools.ietf.org/html/rfc6238. | ||
""" | ||
|
||
MSG_PREFIX = "!totp" | ||
NAMESPACE = "totp" | ||
|
||
def to_json(self, secret): | ||
return {"secret": secret} | ||
|
||
async def __provision(self, bot, team, issuer, force: bool = False): | ||
secret = pyotp.random_base32() | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
val = json.dumps(self.to_json(secret)) | ||
if not force: | ||
try: | ||
# throws exception if 1 is not the latest revision + 1 | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await bot.kvstore.put(team, self.NAMESPACE, issuer, val, revision=1) | ||
except Exception as e: | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
res = await bot.kvstore.get(team, self.NAMESPACE, issuer) | ||
if bot.kvstore.is_deleted(res): | ||
# then TOTP for this issuer was previously provisioned but | ||
# was deleted. insert as latest revision + 1 | ||
await bot.kvstore.put(team, self.NAMESPACE, issuer) | ||
else: | ||
# then TOTP for this issuer was previously provisioned and | ||
# might still be in use, so raise the exception | ||
raise e | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else: | ||
# reprovisioning; "force" provisioning by inserting as latest revision + 1 | ||
await bot.kvstore.put(team, self.NAMESPACE, issuer, val) | ||
return pyotp.TOTP(secret).provisioning_uri(team, issuer_name=issuer) | ||
|
||
async def __remove(self, bot, team, issuer): | ||
# throws exception if nothing to delete | ||
await bot.kvstore.delete(team, self.NAMESPACE, issuer) | ||
|
||
async def __list(self, bot, team): | ||
# returns all namespaces in this team; assumes that all namespaces are | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# used for storing TOTP credentials | ||
res = await bot.kvstore.list_entrykeys(team, self.NAMESPACE) | ||
if res.entry_keys: | ||
return [e.entry_key for e in res.entry_keys] | ||
else: | ||
return [] | ||
|
||
async def __uri(self, bot, team, issuer): | ||
totp = await self.__totp(bot, team, issuer) | ||
return totp.provisioning_uri(team, issuer_name=issuer) if totp else None | ||
|
||
async def __now(self, bot, team, issuer): | ||
totp = await self.__totp(bot, team, issuer) | ||
return totp.now() if totp else None | ||
|
||
async def __totp(self, bot, team, issuer): | ||
mmou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
res = await bot.kvstore.get(team, self.NAMESPACE, issuer) | ||
if bot.kvstore.is_present(res): | ||
# if secret is present | ||
secret = json.loads(res.entry_value)["secret"] | ||
return pyotp.TOTP(secret) | ||
else: | ||
return None | ||
|
||
async def __call__(self, bot, event): | ||
members_type = event.msg.channel.members_type | ||
if not ( | ||
event.type == EventType.CHAT | ||
and (members_type == "team" or members_type == "impteamnative") | ||
): | ||
return | ||
|
||
channel = event.msg.channel | ||
|
||
# support teams and implicit self teams | ||
team = ( | ||
channel.name if members_type == "team" else "{0},{0}".format(channel.name) | ||
) | ||
|
||
msg = event.msg.content.text.body.split(" ") | ||
if len(msg) == 2 and msg[0] == self.MSG_PREFIX and msg[1] == TotpMsg.LIST.value: | ||
# chat: "!totp list" | ||
ns = await self.__list(bot, team) | ||
await bot.chat.send(channel, str(ns)) | ||
return | ||
|
||
if not (len(msg) == 3 and msg[0] == self.MSG_PREFIX): | ||
return | ||
|
||
action, issuer = msg[1], msg[2] | ||
if action == TotpMsg.PROVISION.value: | ||
# chat: "!totp provision <issuer>" | ||
send_msg = "Error provisioning TOTP for {0}. If this issuer was previously provisioned, confirm reprovisioning with command `totp {1} {0}`.".format( | ||
issuer, TotpMsg.REPROVISION.value | ||
) | ||
try: | ||
uri = await self.__provision(bot, team, issuer, force=False) | ||
send_msg = "TOTP provisioned for {}. provisioning_uri={}".format( | ||
issuer, uri | ||
) | ||
except Exception as e: | ||
print(e) | ||
finally: | ||
await bot.chat.send(channel, send_msg) | ||
return | ||
if action == TotpMsg.REPROVISION.value: | ||
# chat: "!totp reprovision <issuer>" | ||
send_msg = "Error reprovisioning TOTP for {}".format(issuer) | ||
try: | ||
uri = await self.__provision(bot, team, issuer, force=True) | ||
send_msg = "TOTP reprovisioned for {}. provisioning_uri={}".format( | ||
issuer, uri | ||
) | ||
except Exception as e: | ||
print(e) | ||
finally: | ||
await bot.chat.send(channel, send_msg) | ||
return | ||
if action == TotpMsg.NOW.value: | ||
# chat: "!totp now <issuer>" | ||
send_msg = "Error getting current TOTP for {}".format(issuer) | ||
code = await self.__now(bot, team, issuer) | ||
if code: | ||
send_msg = "Current TOTP for {}: {}".format(issuer, code) | ||
await bot.chat.send(channel, send_msg) | ||
return | ||
if action == TotpMsg.URI.value: | ||
# chat: "!totp uri <issuer>" | ||
send_msg = "Error getting provisioning_uri for {}".format(issuer) | ||
uri = await self.__uri(bot, team, issuer) | ||
if uri: | ||
send_msg = uri | ||
await bot.chat.send(channel, send_msg) | ||
return | ||
if action == TotpMsg.REMOVE.value: | ||
# chat: "!totp remove <issuer>" | ||
send_msg = "No keys to remove for {}".format(issuer) | ||
try: | ||
await self.__remove(bot, team, issuer) | ||
send_msg = "Removed TOTP keys for {}".format(issuer) | ||
except Exception as e: | ||
print(e) | ||
finally: | ||
await bot.chat.send(channel, send_msg) | ||
return | ||
|
||
|
||
username = "yourbot" | ||
|
||
bot = Bot( | ||
username=username, paperkey=os.environ["KEYBASE_PAPERKEY"], handler=TotpHandler() | ||
) | ||
|
||
asyncio.run(bot.start({})) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was getting "No directory found" for **/*.py. Also I think this should match with what is in the Makefile for
make test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could do
poetry run mypy pykeybasebot examples tests
(though i think i was getting errors with thetests
dir)