Python Wrapper
Requirements
The Keychain Core Python wrapper requirements are:
-
Python v3.7 or newer
-
A version of Keychain Core compatible with the 2.4.5 wrapper
Installation
As of v2.4.x of Keychain Core and the Python wrapper, there is no seamless installation or pip install
support. To use the wrapper it is recommended to place it alongside the Keychain Core library, and then reference that with PYTHONPATH
.
For example, suppose you installed Keychain Core into ~/.keychain
. You may have a directory structure as below:
~/.keychain/
bin/
config/
data/
include/
lib/
libkeychain.so
scripts/
The Python wrapper you have downloaded has a directory structure like this:
keychain/
__init__.py
core/
# modules
__init__.py
util/
# modules
__init__.py
The easiest thing to do then is copy this to the Keychain Core lib folder under a python
directory:
mkdir -p ~/.keychain/lib/python && cp -r keychain ~/.keychain/lib/python/
Resulting in a new structure like this:
~/.keychain/
bin/
config/
data/
include/
lib/
libkeychain.so
python/
keychain/
__init__.py
core/
__init__.py
util/
__init__.py
scripts/
Now to use this, set PYTHONPATH
so that it includes ~/.keychain/lib/python
.
The above is only a suggestion. Feel free to use your own folder structure, just make sure |
Reference
Full reference docs for the Python wrapper are available in the Python reference section of this site. Go here to get more details about specific classes and methods.
Usage
To use Keychain Core, a Gateway
is required. It is created in 3 steps.
-
Validate the Keychain license with the Keychain server (requires internet access) and optionally (re)create the database
-
Load the database and create the
Gateway
-
Seed the
Gateway
so it generates appropriate pseudo-random keys
These 3 steps are summarized with 3 lines of Python code:
from keychain import Gateway
# Use 'True' to delete and recreate the DB
settings = keychain.Gateway.init(config_path, db_path, False, drop_path, create_path)
gateway = keychain.Gateway(settings, db_path)
gateway.seed()
A Monitor
is also highly recommended. It is a watcher for the blockchain which updates the Keychain database automatically, and works in concert with the Gateway
. You create it with the same settings
variable from keychain.Gateway.init
above. Remember to start the thread.
from keychain import Monitor
monitor = keychain.Monitor(settings, db_path)
# create the thread
monitor.start()
# resume the thread - put this in the appropriate place
monitor.resume()
Examples and Patterns
Here are some quick patterns that you will find yourself using quite frequently.
Managing Personas
The Gateway
is most useful with a persona - in fact, without a persona created and confirmed most features are not available. Therefore we want to create a persona immediately if one does not already exist.
If there is at least 1 mature persona in the |
Creating a persona is a very quick operation, but in order to use it it must be confirmed, which means the underlying blockchain upon which the persona’s DID is stored has had enough transaction confirmations to trust that the data is impervious to rollback. Therefore our pattern is to check for an active persona, then if none found we create and wait for confirmation.
# building on the previous example with 'gateway' as the Gateway object
if not gateway.mature_persona_exists():
active_persona = gateway.create_persona("python", "test", keychain.SecurityLevel.MEDIUM)
# wait until the new persona is mature
while not active_persona.is_mature():
time.sleep(WAIT_INTERVAL)
# now we have an active persona that is mature
active_persona = gateway.get_active_persona()
Make sure to catch any exceptions that can be thrown in the methods above. See the Python wrapper reference for more details. |
Managing Contacts
With an active persona, you will often find yourself wanting to modify the contacts for it - add, remove, and get all are common operations.
Interestingly, Facade
, of which contacts are a type, does not have a public constructor for developers. So to add a contact, you do it by Uri
, which can be created by a string representation. We use this in the example below.
The string below is a non-functional example and will fail if you use it. Always only use strings from |
# Create 2 new contacts from a Uri string
alice = gateway.add_contact("Alice", "crypto", Uri("1234:1;5678:1"))
bob = gateway.add_contact("Bob", "hacker", Uri("4545:1;7878:1"))
# Should be a list[Facade] of len 2
contacts = gateway.get_contacts()
gateway.remove_contact(bob)
# Bob is gone, only 1 friend now
contacts = gateway.get_contacts()
Signing and Verifying
A critical part of any cryptographic application is signing and verifying signatures. In Keychain you always sign with the active persona’s signature, and you verify against the active persona’s contacts and other personas that are in the Gateway
. Let’s see a few simple examples!
Adding Signatures
First, let’s have Alice create and sign a message.
message = 'Hello, world!'
signed_data = gateway.sign(message)
Next, Bob receives it and verifies the signatures (there’s only 1)
msg, encoding, verifications = gateway.verify(signed_data)
print(len(verifications))
# 1 - Alice
for verification in verifications:
if not verification.is_verified():
# handle the case where it was not a valid signature
continue
if not verification.is_signer_known():
# handle the case where it was signed, but you don't know by whom
continue
signer = verification.get_facade()
if signer.is_null():
# handle the case where the signer is NULL in the library - an error
continue
print(f'The message was signed by {signer.name()}!')
Imagine we are in a system where multiple people must attest to a message for it to be treated by some logic - a consensus-driven network. Then maybe Bob wants to add his signature to the message and pass it along to the next person or broadcast to the network as a whole:
bob_signed_too = gateway.add_signature(signed_data)
The resulting message is now 'Hello, world!'
with 2 signatures on it. You can envision a validator contact who does similar logic as Bob did when verifying, only he will only act when he sees 3 valid signatures in the verifications
list.
msg, encoding, verifications = gateway.verify(bob_signed_too)
if len([v for v in verifications if v.is_verified() and v.is_signer_known()]) >= 3:
# do biz logic here!
Hierarchical Signing
Let’s reframe Alice’s original message. Now we’ll say she’s creating a time off request and wants to send it to Bob, her manager. In this case it makes sense for her to sign the request (so her manager knows it is valid), but for him to then sign the whole thing as a package rather than the request itself.
vacation_request = '{"from": "2024-05-01", "to": "2024-05-08", "reason": "I work too hard"}'
signed_vacation_request = gateway.sign(vacation_request)
# Note: Bob invokes 'sign', not 'add_signature'
bob_approval = gateway.sign(signed_vacation_request)
# len(verifications) == 1, but now 'msg' is actually another Keychain msg!
msg, encoding, verifications = gateway.verify(bob_approval)
# 1 signer and it's bob - let's see what he signed
if len(verifications) == 1 and verifications[0].get_facade() == bob_facade:
# vac_request is the JSON string from Alice, and vac_verifications has her signature
vac_request, vac_encoding, vac_verifications = gateway.verify(msg)
In this way you can imagine a richer set of business logic whereby participants can add signatures to messages alongside others (e.g. voting) or over top of messages (e.g. approving) and any combination thereof.
Encrypting and Decrypting
Lastly let’s show a couple quick encryption/decryption patterns.
Basic String
The first place to start is with a simple message, like Alice’s 'Hello, world!'
. Let’s encrypt it for all of her contacts to read:
message = 'Hello, world!'
encrypted_data = gateway.encrypt(gateway.get_contacts(), message)
Now her contacts, for example Bob from above, can decrypt and read it:
# encoding is helpful if it wasn't UTF8 or UTF16, otherwise 'alice_msg'
# will be converted to a string properly
alice_msg, encoding = gateway.decrypt(encrypted_data)
Bob should of course verify the message came from Alice too, for example by either using |
Alice can also encrypt for just a select few contacts.
# Quick and dirty
contacts = gateway.get_contacts()
charlie_facade = next(f for f in contacts if f.name() == "Charlie")
encrypted_data = gateway.encrypt(charlie_facade)
Now Bob will be unable to decrypt.
Binary Data
Binary data can also be encrypted and decrypted. The encrypt and decrypt methods accept either str
or bytearray
, so simply make sure you pass a bytearray
in and the rest is taken care of for you.
Alice wants to send someone a cool new song below.
with open("listen_to_this.mp3", "rb") as song:
f = song.read()
b = bytearray(f)
encrypted_song = gateway.encrypt(b)
The encrypted song object knows it was a bytearray
originally, rather than a string. When Bob gets it:
decrypted_song, encoding = gateway.decrypt(encrypted_song)
if encoding == CharEncoding.BINARY.value:
# it must be a song