diff --git a/notifications_client.py b/notifications_client.py index 8fbe45fa9..0d2ccb428 100644 --- a/notifications_client.py +++ b/notifications_client.py @@ -11,6 +11,7 @@ import html import time import sys import select +from random import randint from utils import getNicknameFromActor from utils import getDomainFromActor from utils import getFullDomain @@ -26,6 +27,8 @@ from follow import sendUnfollowRequestViaServer from posts import sendPostViaServer from announce import sendAnnounceViaServer from pgp import pgpDecrypt +from pgp import hasLocalPGPkey +from pgp import pgpEncryptToActor def _waitForKeypress(timeout: int, debug: bool) -> str: @@ -320,6 +323,32 @@ def _notificationNewDM(session, toHandle: str, subject = None commentsEnabled = True subject = None + + # if there is a local PGP key then attempt to encrypt the DM + # using the PGP public key of the recipient + if hasLocalPGPkey(): + sayStr = \ + 'Local PGP key detected...' + \ + 'Fetching PGP public key for ' + toHandle + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + paddedMessage = newMessage + if len(paddedMessage) < 32: + # add some padding before and after + # This is to guard against cribs based on small messages, like "Hi" + for before in range(randint(1, 16)): + paddedMessage = ' ' + paddedMessage + for after in range(randint(1, 16)): + paddedMessage += ' ' + cipherText = \ + pgpEncryptToActor(paddedMessage, toHandle) + if not cipherText: + sayStr = toHandle + ' has no PGP public key' + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + else: + newMessage = cipherText + sayStr = 'Message encrypted' + _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) + sayStr = 'Sending' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) if sendPostViaServer(__version__, diff --git a/pgp.py b/pgp.py index d3d8a6a01..73a7baab8 100644 --- a/pgp.py +++ b/pgp.py @@ -6,7 +6,9 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +import os import subprocess +from pathlib import Path from person import getActorJson @@ -300,7 +302,7 @@ def _pgpImportPubKey(recipientPubKey: str) -> str: return keyId -def pgpEncrypt(content: str, recipientPubKey: str) -> str: +def _pgpEncrypt(content: str, recipientPubKey: str) -> str: """ Encrypt using your default pgp key to the given recipient """ keyId = _pgpImportPubKey(recipientPubKey) @@ -321,18 +323,22 @@ def pgpEncrypt(content: str, recipientPubKey: str) -> str: return encryptResult -def _getPGPPublicKeyFromActor(handle: str) -> str: +def _getPGPPublicKeyFromActor(handle: str, actorJson=None) -> str: """Searches tags on the actor to see if there is any PGP public key specified """ - actorJson = getActorJson(handle, False, False, True) + if not actorJson: + actorJson = getActorJson(handle, False, False, True) if not actorJson: return None if not actorJson.get('attachment'): return None if not isinstance(actorJson['attachment'], list): return None + # search through the tags on the actor for tag in actorJson['attachment']: + if not isinstance(tag, dict): + continue if not tag.get('value'): continue if '--BEGIN PGP PUBLIC KEY BLOCK--' in tag['value']: @@ -340,6 +346,27 @@ def _getPGPPublicKeyFromActor(handle: str) -> str: return None +def hasLocalPGPkey() -> bool: + """Returns true if there is a local .gnupg directory + """ + homeDir = str(Path.home()) + gpgDir = homeDir + '/.gnupg' + if os.path.isfile(gpgDir): + return True + return False + + +def pgpEncryptToActor(content: str, toHandle: str) -> str: + """PGP encrypt a message to the given actor or handle + """ + # get the actor and extract the pgp public key from it + recipientPubKey = _getPGPPublicKeyFromActor(toHandle) + if not recipientPubKey: + return None + # encrypt using the recipient public key + return _pgpEncrypt(content, recipientPubKey) + + def pgpDecrypt(content: str, fromHandle: str) -> str: """ Encrypt using your default pgp key to the given recipient fromHandle can be a handle or actor url @@ -363,5 +390,5 @@ def pgpDecrypt(content: str, fromHandle: str) -> str: (decryptResult, err) = proc.communicate() if not decryptResult: return content - decryptResult = decryptResult.decode('utf-8') + decryptResult = decryptResult.decode('utf-8').strip() return decryptResult diff --git a/tests.py b/tests.py index b71a5ec47..a2adea0ea 100644 --- a/tests.py +++ b/tests.py @@ -3000,8 +3000,7 @@ def testFunctions(): 'E2EEremoveDevice', 'setOrganizationScheme', 'fill_headers', - '_nothing', - "pgpEncrypt" + '_nothing' ] excludeImports = [ 'link',