Set pgp public key from desktop client

main
Bob Mottram 2021-03-17 20:18:00 +00:00
parent 806209b3b5
commit 67f63c5119
7 changed files with 492 additions and 23 deletions

View File

@ -35,6 +35,7 @@ from announce import sendAnnounceViaServer
from pgp import pgpDecrypt
from pgp import hasLocalPGPkey
from pgp import pgpEncryptToActor
from pgp import pgpPublicKeyUpload
def _desktopHelp() -> None:
@ -850,8 +851,21 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str,
newRepliesExist = False
newDMsExist = False
currPostId = ''
pgpKeyUpload = False
while (1):
session = createSession(proxyType)
if not pgpKeyUpload:
pgpKey = \
pgpPublicKeyUpload(baseDir, session,
nickname, password,
domain, port, httpPrefix,
cachedWebfingers, personCache,
debug, False)
if pgpKey:
print('PGP public key uploaded')
pgpKeyUpload = True
notifyJson = None
speakerJson = \
getSpeakerFromServer(baseDir, session, nickname, password,

View File

@ -48,6 +48,7 @@ from follow import sendUnfollowRequestViaServer
from tests import testPostMessageBetweenServers
from tests import testFollowBetweenServers
from tests import testClientToServer
from tests import testUpdateActor
from tests import runAllTests
from auth import storeBasicCredentials
from auth import createPassword
@ -531,6 +532,7 @@ if args.testsnetwork:
testPostMessageBetweenServers()
testFollowBetweenServers()
testClientToServer()
testUpdateActor()
print('All tests succeeded')
sys.exit()
@ -2121,7 +2123,7 @@ if args.testdata:
testFollowersOnly = False
testSaveToFile = True
testClientToServer = False
testC2S = False
testCommentsEnabled = True
testAttachImageFilename = None
testMediaType = None
@ -2131,7 +2133,7 @@ if args.testdata:
"like this is totally just a #test man",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription)
@ -2139,7 +2141,7 @@ if args.testdata:
"Zoiks!!!",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription)
@ -2147,7 +2149,7 @@ if args.testdata:
"Hey scoob we need like a hundred more #milkshakes",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription)
@ -2155,7 +2157,7 @@ if args.testdata:
"Getting kinda spooky around here",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription,
@ -2165,7 +2167,7 @@ if args.testdata:
"if it wasn't for those pesky hackers",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
'img/logo.png', 'image/png',
'Description of image')
@ -2173,7 +2175,7 @@ if args.testdata:
"man these centralized sites are like the worst!",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription)
@ -2181,7 +2183,7 @@ if args.testdata:
"another mystery solved #test",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription)
@ -2189,7 +2191,7 @@ if args.testdata:
"let's go bowling",
testFollowersOnly,
testSaveToFile,
testClientToServer,
testC2S,
testCommentsEnabled,
testAttachImageFilename,
testMediaType, testImageDescription)

View File

@ -1028,7 +1028,7 @@ def sendFollowRequestViaServer(baseDir: str, session,
postJson(session, newFollowJson, [], inboxUrl, headers, 30, True)
if not postResult:
if debug:
print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
print('DEBUG: POST follow failed for c2s to ' + inboxUrl)
return 5
if debug:

122
outbox.py
View File

@ -21,6 +21,8 @@ from utils import removeIdEnding
from utils import getDomainFromActor
from utils import dangerousMarkup
from utils import isFeaturedWriter
from utils import loadJson
from utils import saveJson
from blocking import isBlockedDomain
from blocking import outboxBlock
from blocking import outboxUndoBlock
@ -42,6 +44,119 @@ from shares import outboxShareUpload
from shares import outboxUndoShareUpload
def _outboxPersonReceiveUpdate(recentPostsCache: {},
baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
messageJson: {}, debug: bool) -> None:
""" Receive an actor update from c2s
For example, setting the PGP key from the desktop client
"""
# these attachments are updatable via c2s
updatableAttachments = ('PGP', 'OpenPGP', 'Email')
if not messageJson.get('type'):
return
print("messageJson['type'] " + messageJson['type'])
if messageJson['type'] != 'Update':
return
if not messageJson.get('object'):
return
if not isinstance(messageJson['object'], dict):
if debug:
print('DEBUG: c2s actor update object is not dict')
return
if not messageJson['object'].get('type'):
if debug:
print('DEBUG: c2s actor update - no type')
return
if messageJson['object']['type'] != 'Person':
if debug:
print('DEBUG: not a c2s actor update')
return
if not messageJson.get('to'):
if debug:
print('DEBUG: c2s actor update has no "to" field')
return
if not messageJson.get('actor'):
if debug:
print('DEBUG: c2s actor update has no actor field')
return
if not messageJson.get('id'):
if debug:
print('DEBUG: c2s actor update has no id field')
return
actor = \
httpPrefix + '://' + getFullDomain(domain, port) + '/users/' + nickname
if len(messageJson['to']) != 1:
if debug:
print('DEBUG: c2s actor update - to does not contain one actor ' +
messageJson['to'])
return
if messageJson['to'][0] != actor:
if debug:
print('DEBUG: c2s actor update - to does not contain actor ' +
messageJson['to'] + ' ' + actor)
return
if not messageJson['id'].startswith(actor + '#updates/'):
if debug:
print('DEBUG: c2s actor update - unexpected id ' +
messageJson['id'])
return
updatedActorJson = messageJson['object']
# load actor from file
actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
if not os.path.isfile(actorFilename):
print('actorFilename not found: ' + actorFilename)
return
actorJson = loadJson(actorFilename)
if not actorJson:
return
actorChanged = False
# update fields within actor
if 'attachment' in updatedActorJson:
for newPropertyValue in updatedActorJson['attachment']:
if not newPropertyValue.get('name'):
continue
if newPropertyValue['name'] not in updatableAttachments:
continue
if not newPropertyValue.get('type'):
continue
if not newPropertyValue.get('value'):
continue
if newPropertyValue['type'] != 'PropertyValue':
continue
if 'attachment' in actorJson:
found = False
for propertyValue in actorJson['attachment']:
if propertyValue != 'PropertyValue':
continue
if propertyValue['name'] == newPropertyValue['name']:
if propertyValue['value'] != \
newPropertyValue['value']:
propertyValue['value'] = \
newPropertyValue['value']
actorChanged = True
found = True
break
if not found:
actorJson['attachment'].append({
"name": newPropertyValue['name'],
"type": "PropertyValue",
"value": newPropertyValue['value']
})
actorChanged = True
# save actor to file
if actorChanged:
saveJson(actorJson, actorFilename)
if debug:
print('actor saved: ' + actorFilename)
if debug:
print('New attachment: ' + str(actorJson['attachment']))
messageJson['object'] = actorJson
if debug:
print('DEBUG: actor update via c2s - ' + nickname + '@' + domain)
def postMessageToOutbox(session, translate: {},
messageJson: {}, postToNickname: str,
server, baseDir: str, httpPrefix: str,
@ -408,6 +523,13 @@ def postMessageToOutbox(session, translate: {},
postToNickname, domain,
port, messageJson, debug)
if debug:
print('DEBUG: handle actor updates from c2s')
_outboxPersonReceiveUpdate(recentPostsCache,
baseDir, httpPrefix,
postToNickname, domain, port,
messageJson, debug)
if debug:
print('DEBUG: sending c2s post to named addresses')
if messageJson.get('to'):

View File

@ -1106,6 +1106,8 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
debug: bool, quiet=False) -> {}:
"""Returns the actor json
"""
if debug:
print('getActorJson for ' + handle)
originalActor = handle
if '/@' in handle or \
'/users/' in handle or \
@ -1117,8 +1119,8 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
handle = handle.replace(prefix, '')
handle = handle.replace('/@', '/users/')
if not hasUsersPath(handle):
if not quiet:
print('Expected actor format: ' +
if not quiet or debug:
print('getActorJson: Expected actor format: ' +
'https://domain/@nick or https://domain/users/nick')
return None
if '/users/' in handle:
@ -1145,13 +1147,13 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
# format: @nick@domain
if '@' not in handle:
if not quiet:
print('Syntax: --actor nickname@domain')
print('getActorJson Syntax: --actor nickname@domain')
return None
if handle.startswith('@'):
handle = handle[1:]
if '@' not in handle:
if not quiet:
print('Syntax: --actor nickname@domain')
print('getActorJsonSyntax: --actor nickname@domain')
return None
nickname = handle.split('@')[0]
domain = handle.split('@')[1]
@ -1168,7 +1170,10 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
httpPrefix = 'gnunet'
proxyType = 'gnunet'
else:
httpPrefix = 'https'
if '127.0.' not in domain and '192.168.' not in domain:
httpPrefix = 'https'
else:
httpPrefix = 'http'
session = createSession(proxyType)
if nickname == 'inbox':
nickname = domain
@ -1179,12 +1184,12 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
None, __version__, debug)
if not wfRequest:
if not quiet:
print('Unable to webfinger ' + handle)
print('getActorJson Unable to webfinger ' + handle)
return None
if not isinstance(wfRequest, dict):
if not quiet:
print('Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
print('getActorJson Webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return None
if not quiet:
@ -1192,11 +1197,13 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
personUrl = None
if wfRequest.get('errors'):
if not quiet:
print('wfRequest error: ' + str(wfRequest['errors']))
if not quiet or debug:
print('getActorJson wfRequest error: ' + str(wfRequest['errors']))
if hasUsersPath(handle):
personUrl = originalActor
else:
if debug:
print('No users path in ' + handle)
return None
profileStr = 'https://www.w3.org/ns/activitystreams'
@ -1228,8 +1235,9 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
getJson(session, personUrl, asHeader, None,
debug, __version__, httpPrefix, None, 20, quiet)
if personJson:
if not quiet:
if not quiet or debug:
pprint(personJson)
return personJson
else:
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
@ -1238,8 +1246,9 @@ def getActorJson(handle: str, http: bool, gnunet: bool,
personJson = \
getJson(session, personUrl, asHeader, None,
debug, __version__, httpPrefix, None)
if not quiet:
if not quiet or debug:
if personJson:
print('getActorJson returned actor')
pprint(personJson)
else:
print('Failed to get ' + personUrl)

205
pgp.py
View File

@ -7,11 +7,18 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
import time
import subprocess
from pathlib import Path
from person import getActorJson
from utils import containsPGPPublicKey
from utils import isPGPEncrypted
from utils import getFullDomain
from utils import getStatusNumber
from webfinger import webfingerHandle
from posts import getPersonBox
from auth import createBasicAuthHeader
from session import postJson
def getEmailAddress(actorJson: {}) -> str:
@ -395,3 +402,201 @@ def pgpDecrypt(content: str, fromHandle: str) -> str:
return content
decryptResult = decryptResult.decode('utf-8').strip()
return decryptResult
def _pgpLocalPublicKeyId() -> str:
"""Gets the local pgp public key ID
"""
cmdStr = \
"gpgconf --list-options gpg | " + \
"awk -F: '$1 == \"default-key\" {print $10}'"
proc = subprocess.Popen([cmdStr],
stdout=subprocess.PIPE, shell=True)
(result, err) = proc.communicate()
if err:
return None
if not result:
return None
if len(result) < 5:
return None
return result.replace('"', '').strip()
def _pgpLocalPublicKey() -> str:
"""Gets the local pgp public key
"""
keyId = _pgpLocalPublicKey()
if not keyId:
return None
cmdStr = "gpg --armor --export " + keyId
proc = subprocess.Popen([cmdStr],
stdout=subprocess.PIPE, shell=True)
(result, err) = proc.communicate()
if err:
return None
if not result:
return None
return extractPGPPublicKey(result)
def pgpPublicKeyUpload(baseDir: str, session,
nickname: str, password: str,
domain: str, port: int,
httpPrefix: str,
cachedWebfingers: {}, personCache: {},
debug: bool, test: str) -> {}:
if debug:
print('pgpPublicKeyUpload')
if not session:
if debug:
print('WARN: No session for pgpPublicKeyUpload')
return None
if not test:
if debug:
print('Getting PGP public key')
PGPpubKey = _pgpLocalPublicKey()
if not PGPpubKey:
return None
PGPpubKeyId = _pgpLocalPublicKeyId()
else:
if debug:
print('Testing with PGP public key ' + test)
PGPpubKey = test
PGPpubKeyId = None
domainFull = getFullDomain(domain, port)
if debug:
print('PGP test domain: ' + domainFull)
handle = nickname + '@' + domainFull
if debug:
print('Getting actor for ' + handle)
actorJson = getActorJson(handle, False, False, True)
if not actorJson:
if debug:
print('No actor returned for ' + handle)
return None
if debug:
print('Actor for ' + handle + ' obtained')
actor = httpPrefix + '://' + domainFull + '/users/' + nickname
handle = actor.replace('/users/', '/@')
# check that this looks like the correct actor
if not actorJson.get('id'):
if debug:
print('Actor has no id')
return None
if not actorJson.get('url'):
if debug:
print('Actor has no url')
return None
if not actorJson.get('type'):
if debug:
print('Actor has no type')
return None
if actorJson['id'] != actor:
if debug:
print('Actor id is not ' + actor +
' instead is ' + actorJson['id'])
return None
if actorJson['url'] != handle:
if debug:
print('Actor url is not ' + handle)
return None
if actorJson['type'] != 'Person':
if debug:
print('Actor type is not Person')
return None
# set the pgp details
if PGPpubKeyId:
setPGPfingerprint(actorJson, PGPpubKeyId)
else:
if debug:
print('No PGP key Id. Continuing anyway.')
if debug:
print('Setting PGP key within ' + actor)
setPGPpubKey(actorJson, PGPpubKey)
# create an actor update
statusNumber, published = getStatusNumber()
actorUpdate = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': actor + '#updates/' + statusNumber,
'type': 'Update',
'actor': actor,
'to': [actor],
'cc': [],
'object': actorJson
}
if debug:
print('actor update is ' + str(actorUpdate))
# lookup the inbox for the To handle
wfRequest = \
webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
domain, __version__, debug)
if not wfRequest:
if debug:
print('DEBUG: pgp actor update webfinger failed for ' +
handle)
return None
if not isinstance(wfRequest, dict):
if debug:
print('WARN: Webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return None
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
__version__, httpPrefix, nickname,
domain, postToBox, 52025)
if not inboxUrl:
if debug:
print('DEBUG: No ' + postToBox + ' was found for ' + handle)
return None
if not fromPersonId:
if debug:
print('DEBUG: No actor was found for ' + handle)
return None
authHeader = createBasicAuthHeader(nickname, password)
headers = {
'host': domain,
'Content-type': 'application/json',
'Authorization': authHeader
}
tries = 0
quiet = not debug
while tries < 4:
postResult = \
postJson(session, actorUpdate, [], inboxUrl,
headers, 30, quiet)
if postResult:
break
time.sleep(2)
tries += 1
if postResult is None:
if debug:
print('DEBUG: POST pgp actor update failed for c2s to ' +
inboxUrl)
return None
if debug:
print('DEBUG: c2s POST pgp actor update success')
return actorUpdate

119
tests.py
View File

@ -53,6 +53,7 @@ from utils import getFollowersOfPerson
from utils import removeHtml
from utils import dangerousMarkup
from pgp import extractPGPPublicKey
from pgp import pgpPublicKeyUpload
from utils import containsPGPPublicKey
from follow import followerOfPerson
from follow import unfollowAccount
@ -1574,7 +1575,7 @@ def testClientToServer():
sessionAlice = createSession(proxyType)
followersOnly = False
attachedImageFilename = baseDir+'/img/logo.png'
attachedImageFilename = baseDir + '/img/logo.png'
mediaType = getAttachmentMediaType(attachedImageFilename)
attachedImageDescription = 'Logo'
isArticle = False
@ -3447,6 +3448,122 @@ def testExtractPGPPublicKey():
assert result == pubKey
def testUpdateActor():
print('Testing update of actor properties')
global testServerAliceRunning
testServerAliceRunning = False
httpPrefix = 'http'
proxyType = None
federationList = []
baseDir = os.getcwd()
if os.path.isdir(baseDir + '/.tests'):
shutil.rmtree(baseDir + '/.tests')
os.mkdir(baseDir + '/.tests')
# create the server
aliceDir = baseDir + '/.tests/alice'
aliceDomain = '127.0.0.11'
alicePort = 61792
aliceSendThreads = []
bobAddress = '127.0.0.84:6384'
global thrAlice
if thrAlice:
while thrAlice.is_alive():
thrAlice.stop()
time.sleep(1)
thrAlice.kill()
thrAlice = \
threadWithTrace(target=createServerAlice,
args=(aliceDir, aliceDomain, alicePort, bobAddress,
federationList, False, False,
aliceSendThreads),
daemon=True)
thrAlice.start()
assert thrAlice.is_alive() is True
# wait for server to be running
ctr = 0
while not testServerAliceRunning:
time.sleep(1)
ctr += 1
if ctr > 60:
break
print('Alice online: ' + str(testServerAliceRunning))
print('\n\n*******************************************************')
print('Alice updates her PGP key')
sessionAlice = createSession(proxyType)
cachedWebfingers = {}
personCache = {}
password = 'alicepass'
outboxPath = aliceDir + '/accounts/alice@' + aliceDomain + '/outbox'
actorFilename = aliceDir + '/accounts/' + 'alice@' + aliceDomain + '.json'
assert os.path.isfile(actorFilename)
assert len([name for name in os.listdir(outboxPath)
if os.path.isfile(os.path.join(outboxPath, name))]) == 0
pubKey = \
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n' + \
'mDMEWZBueBYJKwYBBAHaRw8BAQdAKx1t6wL0RTuU6/' + \
'IBjngMbVJJ3Wg/3UW73/PV\n' + \
'I47xKTS0IUJvYiBNb3R0cmFtIDxib2JAZnJlZWRvb' + \
'WJvbmUubmV0PoiQBBMWCAA4\n' + \
'FiEEmruCwAq/OfgmgEh9zCU2GR+nwz8FAlmQbngCG' + \
'wMFCwkIBwMFFQoJCAsFFgID\n' + \
'AQACHgECF4AACgkQzCU2GR+nwz/9sAD/YgsHnVszH' + \
'Nz1zlVc5EgY1ByDupiJpHj0\n' + \
'XsLYk3AbNRgBALn45RqgD4eWHpmOriH09H5Rc5V9i' + \
'N4+OiGUn2AzJ6oHuDgEWZBu\n' + \
'eBIKKwYBBAGXVQEFAQEHQPRBG2ZQJce475S3e0Dxe' + \
'b0Fz5WdEu2q3GYLo4QG+4Ry\n' + \
'AwEIB4h4BBgWCAAgFiEEmruCwAq/OfgmgEh9zCU2G' + \
'R+nwz8FAlmQbngCGwwACgkQ\n' + \
'zCU2GR+nwz+OswD+JOoyBku9FzuWoVoOevU2HH+bP' + \
'OMDgY2OLnST9ZSyHkMBAMcK\n' + \
'fnaZ2Wi050483Sj2RmQRpb99Dod7rVZTDtCqXk0J\n' + \
'=gv5G\n' + \
'-----END PGP PUBLIC KEY BLOCK-----'
actorUpdate = \
pgpPublicKeyUpload(aliceDir, sessionAlice,
'alice', password,
aliceDomain, alicePort,
httpPrefix,
cachedWebfingers, personCache,
True, pubKey)
print('actor update result: ' + str(actorUpdate))
assert actorUpdate
# load alice actor
print('Loading actor: ' + actorFilename)
actorJson = loadJson(actorFilename)
assert actorJson
if len(actorJson['attachment']) == 0:
print("actorJson['attachment'] has no contents")
assert len(actorJson['attachment']) > 0
propertyFound = False
for propertyValue in actorJson['attachment']:
if propertyValue['name'] == 'PGP':
print('PGP property set within attachment')
assert pubKey in propertyValue['value']
propertyFound = True
assert propertyFound
# stop the server
thrAlice.kill()
thrAlice.join()
assert thrAlice.is_alive() is False
os.chdir(baseDir)
if os.path.isdir(baseDir + '/.tests'):
shutil.rmtree(baseDir + '/.tests')
def runAllTests():
print('Running tests...')
testFunctions()