Python Wrapper

Requirements

The Keychain Core Python wrapper requirements are:

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 PYTHONPATH is aware of it.

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.

  1. Validate the Keychain license with the Keychain server (requires internet access) and optionally (re)create the database

  2. Load the database and create the Gateway

  3. 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 Gateway, one will be active already

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 Facade#uri() string representations.

# 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.

A potential validator
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.

Alice’s Message
vacation_request = '{"from": "2024-05-01", "to": "2024-05-08", "reason": "I work too hard"}'
signed_vacation_request = gateway.sign(vacation_request)
Bob signing it
# Note: Bob invokes 'sign', not 'add_signature'
bob_approval = gateway.sign(signed_vacation_request)
HR checks
# 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 decrypt_and_verify or calling verify on the encrypted data. Don’t forget these methods exist!

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

Notes