Skip to content

Instantly share code, notes, and snippets.

@minrk
Created May 27, 2021 07:47
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save minrk/776ccc423e2c6131ec6f088ef247c9c3 to your computer and use it in GitHub Desktop.
Save minrk/776ccc423e2c6131ec6f088ef247c9c3 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "b0267f2f-1bf3-49bd-a466-3ac9a062de83",
"metadata": {},
"source": [
"# End-to-end encrypted communication with a Jupyter kernel\n",
"\n",
"Prompted by [this question](https://discourse.jupyter.org/t/how-do-i-properly-protect-my-data-access-passwords-not-jupyter-tokens-passwords-on-3rd-party-jupyter-hub-services/9277/2),\n",
"\n",
"Disclosure: I am doing exactly what people shouldn't, which is copying and pasting from [cryptography's hazmat docs](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/) without fully understanding everything involved.\n",
"\n",
"We need two computers for this:\n",
"\n",
"1. the remote JupyterHub host, where we don't want credentials at rest, and don't want to trust that all network traffic is secure.\n",
"2. a local machine that we do trust"
]
},
{
"cell_type": "markdown",
"id": "0fd90d66-7065-48a0-89af-c4301f58dc2c",
"metadata": {},
"source": [
"Step 1: Generate a public/private key pair for sending encrypted messages to the kernel.\n",
"\n",
"The private key only resides in memory, and is never displayed or shared, and it never leaves the kernel's memory.\n",
"\n",
"It is only used for the lifetime of the current kernel - a new key will be regenerated on kernel restart and we need to do the whole rigamarole again."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "015acc17-c6c9-42e5-b8bb-15bdda36bed0",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"-----BEGIN PUBLIC KEY-----\n",
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3ylCX42ZsgDYDqWG8R6c\n",
"KU6YPiHPekSy+OG8vc+crDiadwEKaZ1dakZvJWbz38r9WyCy1WA4pszChEynlqNj\n",
"g8VjeoxlWab6IwJtc44I8IqFoYmWPuKImaZ+kkRxQbZW4sIeMco7R8GBce2nK6PJ\n",
"DnfyIRJINqotXs9DVMEHgmJ/Lt39gZVibu6ZulbMUgoj/xcMRqNdlDeukjAMOrgY\n",
"smkilRtELdFZPR7Dl+8PKTOJ8KEMCapIsIkl89RxY0SuIokrrsrHROnIpSHVDow8\n",
"cyfR0/zO4CJahkO6Ymp7DzJq1+Yc8vEbWMx/7tuln+SgGFx16VxXPd5NyOWOu2Wv\n",
"7wIDAQAB\n",
"-----END PUBLIC KEY-----\n",
"\n"
]
}
],
"source": [
"# remote\n",
"from cryptography.hazmat.primitives import serialization\n",
"from cryptography.hazmat.primitives.asymmetric import rsa\n",
"\n",
"private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n",
"\n",
"pem = private_key.public_key().public_bytes(\n",
" encoding=serialization.Encoding.PEM,\n",
" format=serialization.PublicFormat.SubjectPublicKeyInfo\n",
")\n",
"print(pem.decode(\"ascii\"))"
]
},
{
"cell_type": "markdown",
"id": "1a085208-3c6f-428f-bfd8-e77ed0570a97",
"metadata": {},
"source": [
"On a local, trusted computer, encrypt your message with this public key.\n",
"\n",
"You need to get the public key to the local computer and load it:\n",
"\n",
"The public key itself is not sensitive, so this can be done via insecure channels:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "e3814e50-3d39-400c-9fce-0713196c2d8a",
"metadata": {},
"outputs": [],
"source": [
"# local\n",
"from cryptography.hazmat.primitives import serialization\n",
"\n",
"public_key = serialization.load_pem_public_key(\"\"\"\n",
"-----BEGIN PUBLIC KEY-----\n",
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3ylCX42ZsgDYDqWG8R6c\n",
"KU6YPiHPekSy+OG8vc+crDiadwEKaZ1dakZvJWbz38r9WyCy1WA4pszChEynlqNj\n",
"g8VjeoxlWab6IwJtc44I8IqFoYmWPuKImaZ+kkRxQbZW4sIeMco7R8GBce2nK6PJ\n",
"DnfyIRJINqotXs9DVMEHgmJ/Lt39gZVibu6ZulbMUgoj/xcMRqNdlDeukjAMOrgY\n",
"smkilRtELdFZPR7Dl+8PKTOJ8KEMCapIsIkl89RxY0SuIokrrsrHROnIpSHVDow8\n",
"cyfR0/zO4CJahkO6Ymp7DzJq1+Yc8vEbWMx/7tuln+SgGFx16VxXPd5NyOWOu2Wv\n",
"7wIDAQAB\n",
"-----END PUBLIC KEY-----\n",
"\"\"\".encode('ascii'))"
]
},
{
"cell_type": "markdown",
"id": "d4a69c17-c7a1-47f9-b9db-30de88310eb3",
"metadata": {},
"source": [
"Now retrieve our credentials on the local computer, however makes sense.\n",
"\n",
"We need it in bytes form (could be reading a file, but I'll use a json blob)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "11a387c8-18e2-48a7-a816-d9df488d6977",
"metadata": {},
"outputs": [],
"source": [
"# local\n",
"import json\n",
"\n",
"creds = {\"key\": \"value\"}\n",
"creds_bytes = json.dumps(creds).encode(\"utf8\")"
]
},
{
"cell_type": "markdown",
"id": "83e88b82-61ad-4892-b592-b7580c0d37b2",
"metadata": {},
"source": [
"Now serialize our credentials message using the kernel's public key"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "1df02342-d0b3-4c5d-810b-f8080d927b69",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"wqGCOglw+7qgDgMKYAg8E6qa1Y6icRfIPXZ18t9h0rST3+EJRcwg9t7wkrYaUPUloCoGdEksckiyfkEb01IHqYibVTYr9Ez5kbmu4T4g5rWV96yuRrYShBY8CTh2HD8uFcgbgRqGESvmnpMeejapJAP2RFAgHECQ+WUaG5YfLazoazLcWx+83IZVryMf/PxCyqn0xlyPboYQmc+ZPETMMAIvqmY+bv4f8F48euUehXd0Phj484ybdGhNRO8uhSZDbKVshOzHYboG3sP01drE7fvgLSREkSniGuyb0opPD0cA3RMoUAiMBlrT5Jc+H7c2UdmXITSw4LkE37f0BkXp2w==\n",
"\n"
]
}
],
"source": [
"# local\n",
"import binascii\n",
"from cryptography.hazmat.primitives import hashes\n",
"from cryptography.hazmat.primitives.asymmetric import padding\n",
"\n",
"encrypted_creds = public_key.encrypt(\n",
" creds_bytes,\n",
" padding.OAEP(\n",
" mgf=padding.MGF1(algorithm=hashes.SHA256()),\n",
" algorithm=hashes.SHA256(),\n",
" label=None,\n",
" ),\n",
")\n",
"\n",
"encrypted_creds\n",
"b64_encrypted_creds = binascii.b2a_base64(encrypted_creds).decode('ascii')\n",
"print(b64_encrypted_creds)"
]
},
{
"cell_type": "markdown",
"id": "4aed17bf-7964-4170-956c-13348ed1edc9",
"metadata": {},
"source": [
"At this point, we have an encrypted form of the credentials,\n",
"which should only be readable with the private key in memory of your remote kernel.\n",
"\n",
"\n",
"Back to the remote, shared system.\n",
"\n",
"We can input this encrypted message using `getpass()` to avoid it being at rest in your inputs or execution history."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "dfa032c4-6615-4b23-815d-3d613c9992d7",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"Base64 encrypted credentials: ························································································································································································································································································································································\n"
]
}
],
"source": [
"# remote\n",
"import binascii\n",
"from getpass import getpass\n",
"\n",
"b64_encrypted_creds = getpass(\"Base64 encrypted credentials: \")\n",
"encrypted_creds = binascii.a2b_base64(b64_encrypted_creds)"
]
},
{
"cell_type": "markdown",
"id": "b4a0f2fe-44b6-43c8-b861-4c4f3039fd73",
"metadata": {},
"source": [
"Now we can decrypt the credentials on the host:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "539678d1-db66-4fb2-9b5e-72fff7f9808c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'key': 'value'}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# remote\n",
"from cryptography.hazmat.primitives import hashes\n",
"from cryptography.hazmat.primitives.asymmetric import padding\n",
"\n",
"creds_bytes = private_key.decrypt(\n",
" encrypted_creds,\n",
" padding.OAEP(\n",
" mgf=padding.MGF1(algorithm=hashes.SHA256()),\n",
" algorithm=hashes.SHA256(),\n",
" label=None,\n",
" ),\n",
")\n",
"json.loads(creds_bytes.decode(\"utf8\"))"
]
},
{
"cell_type": "markdown",
"id": "46223443-7658-4475-9279-031c93a92663",
"metadata": {},
"source": [
"This is super tedious and I'm not sure I would recommend it to anyone, but it is an example of e2e communication with a Jupyter kernel.\n",
"\n",
"Also never take security advice from me, I don't know what I'm doing."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.8"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment