Removed from shared the functions duplicating protocol:
decryptAndCheckPubkeyPayload, isBitSetWithinBitfield
This commit is contained in:
parent
5b5ec2b63d
commit
3adadd398f
|
@ -400,7 +400,7 @@ class objectProcessor(threading.Thread):
|
||||||
|
|
||||||
# Let us try to decrypt the pubkey
|
# Let us try to decrypt the pubkey
|
||||||
toAddress, _ = state.neededPubkeys[tag]
|
toAddress, _ = state.neededPubkeys[tag]
|
||||||
if shared.decryptAndCheckPubkeyPayload(data, toAddress) == \
|
if protocol.decryptAndCheckPubkeyPayload(data, toAddress) == \
|
||||||
'successful':
|
'successful':
|
||||||
# At this point we know that we have been waiting on this
|
# At this point we know that we have been waiting on this
|
||||||
# pubkey. This function will command the workerThread
|
# pubkey. This function will command the workerThread
|
||||||
|
|
|
@ -758,7 +758,7 @@ class singleWorker(threading.Thread, StoppableThread):
|
||||||
for value in Inventory().by_type_and_tag(1, toTag):
|
for value in Inventory().by_type_and_tag(1, toTag):
|
||||||
# if valid, this function also puts it
|
# if valid, this function also puts it
|
||||||
# in the pubkeys table.
|
# in the pubkeys table.
|
||||||
if shared.decryptAndCheckPubkeyPayload(
|
if protocol.decryptAndCheckPubkeyPayload(
|
||||||
value.payload, toaddress
|
value.payload, toaddress
|
||||||
) == 'successful':
|
) == 'successful':
|
||||||
needToRequestPubkey = False
|
needToRequestPubkey = False
|
||||||
|
@ -860,7 +860,7 @@ class singleWorker(threading.Thread, StoppableThread):
|
||||||
# if receiver is a mobile device who expects that their
|
# if receiver is a mobile device who expects that their
|
||||||
# address RIPE is included unencrypted on the front of
|
# address RIPE is included unencrypted on the front of
|
||||||
# the message..
|
# the message..
|
||||||
if shared.isBitSetWithinBitfield(behaviorBitfield, 30):
|
if protocol.isBitSetWithinBitfield(behaviorBitfield, 30):
|
||||||
# if we are Not willing to include the receiver's
|
# if we are Not willing to include the receiver's
|
||||||
# RIPE hash on the message..
|
# RIPE hash on the message..
|
||||||
if not shared.BMConfigParser().safeGetBoolean(
|
if not shared.BMConfigParser().safeGetBoolean(
|
||||||
|
|
108
src/protocol.py
108
src/protocol.py
|
@ -9,21 +9,21 @@ Low-level protocol-related functions.
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from binascii import hexlify
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
from struct import pack, unpack, Struct
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from binascii import hexlify
|
||||||
|
from struct import pack, unpack, Struct
|
||||||
|
|
||||||
import defaults
|
import defaults
|
||||||
import highlevelcrypto
|
import highlevelcrypto
|
||||||
import state
|
import state
|
||||||
from addresses import encodeVarint, decodeVarint, decodeAddress, varintDecodeError
|
from addresses import (
|
||||||
|
encodeVarint, decodeVarint, decodeAddress, varintDecodeError)
|
||||||
from bmconfigparser import BMConfigParser
|
from bmconfigparser import BMConfigParser
|
||||||
from debug import logger
|
from debug import logger
|
||||||
from helper_sql import sqlExecute
|
from helper_sql import sqlExecute
|
||||||
|
@ -321,33 +321,41 @@ def assembleErrorMessage(fatal=0, banTime=0, inventoryVector='', errorText=''):
|
||||||
|
|
||||||
def decryptAndCheckPubkeyPayload(data, address):
|
def decryptAndCheckPubkeyPayload(data, address):
|
||||||
"""
|
"""
|
||||||
Version 4 pubkeys are encrypted. This function is run when we already have the
|
Version 4 pubkeys are encrypted. This function is run when we
|
||||||
address to which we want to try to send a message. The 'data' may come either
|
already have the address to which we want to try to send a message.
|
||||||
off of the wire or we might have had it already in our inventory when we tried
|
The 'data' may come either off of the wire or we might have had it
|
||||||
to send a msg to this particular address.
|
already in our inventory when we tried to send a msg to this
|
||||||
|
particular address.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=unused-variable
|
|
||||||
try:
|
try:
|
||||||
status, addressVersion, streamNumber, ripe = decodeAddress(address)
|
addressVersion, streamNumber, ripe = decodeAddress(address)[1:]
|
||||||
|
|
||||||
readPosition = 20 # bypass the nonce, time, and object type
|
readPosition = 20 # bypass the nonce, time, and object type
|
||||||
embeddedAddressVersion, varintLength = decodeVarint(data[readPosition:readPosition + 10])
|
embeddedAddressVersion, varintLength = decodeVarint(
|
||||||
|
data[readPosition:readPosition + 10])
|
||||||
readPosition += varintLength
|
readPosition += varintLength
|
||||||
embeddedStreamNumber, varintLength = decodeVarint(data[readPosition:readPosition + 10])
|
embeddedStreamNumber, varintLength = decodeVarint(
|
||||||
|
data[readPosition:readPosition + 10])
|
||||||
readPosition += varintLength
|
readPosition += varintLength
|
||||||
# We'll store the address version and stream number (and some more) in the pubkeys table.
|
# We'll store the address version and stream number
|
||||||
|
# (and some more) in the pubkeys table.
|
||||||
storedData = data[20:readPosition]
|
storedData = data[20:readPosition]
|
||||||
|
|
||||||
if addressVersion != embeddedAddressVersion:
|
if addressVersion != embeddedAddressVersion:
|
||||||
logger.info('Pubkey decryption was UNsuccessful due to address version mismatch.')
|
logger.info(
|
||||||
|
'Pubkey decryption was UNsuccessful'
|
||||||
|
' due to address version mismatch.')
|
||||||
return 'failed'
|
return 'failed'
|
||||||
if streamNumber != embeddedStreamNumber:
|
if streamNumber != embeddedStreamNumber:
|
||||||
logger.info('Pubkey decryption was UNsuccessful due to stream number mismatch.')
|
logger.info(
|
||||||
|
'Pubkey decryption was UNsuccessful'
|
||||||
|
' due to stream number mismatch.')
|
||||||
return 'failed'
|
return 'failed'
|
||||||
|
|
||||||
tag = data[readPosition:readPosition + 32]
|
tag = data[readPosition:readPosition + 32]
|
||||||
readPosition += 32
|
readPosition += 32
|
||||||
# the time through the tag. More data is appended onto signedData below after the decryption.
|
# the time through the tag. More data is appended onto
|
||||||
|
# signedData below after the decryption.
|
||||||
signedData = data[8:readPosition]
|
signedData = data[8:readPosition]
|
||||||
encryptedData = data[readPosition:]
|
encryptedData = data[readPosition:]
|
||||||
|
|
||||||
|
@ -355,13 +363,15 @@ def decryptAndCheckPubkeyPayload(data, address):
|
||||||
toAddress, cryptorObject = state.neededPubkeys[tag]
|
toAddress, cryptorObject = state.neededPubkeys[tag]
|
||||||
if toAddress != address:
|
if toAddress != address:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
'decryptAndCheckPubkeyPayload failed due to toAddress mismatch.'
|
'decryptAndCheckPubkeyPayload failed due to toAddress'
|
||||||
' This is very peculiar. toAddress: %s, address %s',
|
' mismatch. This is very peculiar.'
|
||||||
toAddress,
|
' toAddress: %s, address %s',
|
||||||
address)
|
toAddress, address
|
||||||
# the only way I can think that this could happen is if someone encodes their address data two different
|
)
|
||||||
# ways. That sort of address-malleability should have been caught by the UI or API and an error given to
|
# the only way I can think that this could happen
|
||||||
# the user.
|
# is if someone encodes their address data two different ways.
|
||||||
|
# That sort of address-malleability should have been caught
|
||||||
|
# by the UI or API and an error given to the user.
|
||||||
return 'failed'
|
return 'failed'
|
||||||
try:
|
try:
|
||||||
decryptedData = cryptorObject.decrypt(encryptedData)
|
decryptedData = cryptorObject.decrypt(encryptedData)
|
||||||
|
@ -372,17 +382,17 @@ def decryptAndCheckPubkeyPayload(data, address):
|
||||||
return 'failed'
|
return 'failed'
|
||||||
|
|
||||||
readPosition = 0
|
readPosition = 0
|
||||||
bitfieldBehaviors = decryptedData[readPosition:readPosition + 4]
|
# bitfieldBehaviors = decryptedData[readPosition:readPosition + 4]
|
||||||
readPosition += 4
|
readPosition += 4
|
||||||
publicSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64]
|
publicSigningKey = '\x04' + decryptedData[readPosition:readPosition + 64]
|
||||||
readPosition += 64
|
readPosition += 64
|
||||||
publicEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64]
|
publicEncryptionKey = '\x04' + decryptedData[readPosition:readPosition + 64]
|
||||||
readPosition += 64
|
readPosition += 64
|
||||||
specifiedNonceTrialsPerByte, specifiedNonceTrialsPerByteLength = decodeVarint(
|
specifiedNonceTrialsPerByteLength = decodeVarint(
|
||||||
decryptedData[readPosition:readPosition + 10])
|
decryptedData[readPosition:readPosition + 10])[1]
|
||||||
readPosition += specifiedNonceTrialsPerByteLength
|
readPosition += specifiedNonceTrialsPerByteLength
|
||||||
specifiedPayloadLengthExtraBytes, specifiedPayloadLengthExtraBytesLength = decodeVarint(
|
specifiedPayloadLengthExtraBytesLength = decodeVarint(
|
||||||
decryptedData[readPosition:readPosition + 10])
|
decryptedData[readPosition:readPosition + 10])[1]
|
||||||
readPosition += specifiedPayloadLengthExtraBytesLength
|
readPosition += specifiedPayloadLengthExtraBytesLength
|
||||||
storedData += decryptedData[:readPosition]
|
storedData += decryptedData[:readPosition]
|
||||||
signedData += decryptedData[:readPosition]
|
signedData += decryptedData[:readPosition]
|
||||||
|
@ -391,12 +401,15 @@ def decryptAndCheckPubkeyPayload(data, address):
|
||||||
readPosition += signatureLengthLength
|
readPosition += signatureLengthLength
|
||||||
signature = decryptedData[readPosition:readPosition + signatureLength]
|
signature = decryptedData[readPosition:readPosition + signatureLength]
|
||||||
|
|
||||||
if highlevelcrypto.verify(signedData, signature, hexlify(publicSigningKey)):
|
if not highlevelcrypto.verify(
|
||||||
logger.info('ECDSA verify passed (within decryptAndCheckPubkeyPayload)')
|
signedData, signature, hexlify(publicSigningKey)):
|
||||||
else:
|
logger.info(
|
||||||
logger.info('ECDSA verify failed (within decryptAndCheckPubkeyPayload)')
|
'ECDSA verify failed (within decryptAndCheckPubkeyPayload)')
|
||||||
return 'failed'
|
return 'failed'
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'ECDSA verify passed (within decryptAndCheckPubkeyPayload)')
|
||||||
|
|
||||||
sha = hashlib.new('sha512')
|
sha = hashlib.new('sha512')
|
||||||
sha.update(publicSigningKey + publicEncryptionKey)
|
sha.update(publicSigningKey + publicEncryptionKey)
|
||||||
ripeHasher = hashlib.new('ripemd160')
|
ripeHasher = hashlib.new('ripemd160')
|
||||||
|
@ -404,34 +417,37 @@ def decryptAndCheckPubkeyPayload(data, address):
|
||||||
embeddedRipe = ripeHasher.digest()
|
embeddedRipe = ripeHasher.digest()
|
||||||
|
|
||||||
if embeddedRipe != ripe:
|
if embeddedRipe != ripe:
|
||||||
# Although this pubkey object had the tag were were looking for and was
|
# Although this pubkey object had the tag were were looking for
|
||||||
# encrypted with the correct encryption key, it doesn't contain the
|
# and was encrypted with the correct encryption key,
|
||||||
# correct pubkeys. Someone is either being malicious or using buggy software.
|
# it doesn't contain the correct pubkeys. Someone is
|
||||||
logger.info('Pubkey decryption was UNsuccessful due to RIPE mismatch.')
|
# either being malicious or using buggy software.
|
||||||
|
logger.info(
|
||||||
|
'Pubkey decryption was UNsuccessful due to RIPE mismatch.')
|
||||||
return 'failed'
|
return 'failed'
|
||||||
|
|
||||||
# Everything checked out. Insert it into the pubkeys table.
|
# Everything checked out. Insert it into the pubkeys table.
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
os.linesep.join([
|
'within decryptAndCheckPubkeyPayload, '
|
||||||
'within decryptAndCheckPubkeyPayload,'
|
'addressVersion: %s, streamNumber: %s\nripe %s\n'
|
||||||
' addressVersion: %s, streamNumber: %s' % addressVersion, streamNumber,
|
'publicSigningKey in hex: %s\npublicEncryptionKey in hex: %s',
|
||||||
'ripe %s' % hexlify(ripe),
|
addressVersion, streamNumber, hexlify(ripe),
|
||||||
'publicSigningKey in hex: %s' % hexlify(publicSigningKey),
|
hexlify(publicSigningKey), hexlify(publicEncryptionKey)
|
||||||
'publicEncryptionKey in hex: %s' % hexlify(publicEncryptionKey),
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
t = (address, addressVersion, storedData, int(time.time()), 'yes')
|
t = (address, addressVersion, storedData, int(time.time()), 'yes')
|
||||||
sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t)
|
sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t)
|
||||||
return 'successful'
|
return 'successful'
|
||||||
except varintDecodeError:
|
except varintDecodeError:
|
||||||
logger.info('Pubkey decryption was UNsuccessful due to a malformed varint.')
|
logger.info(
|
||||||
|
'Pubkey decryption was UNsuccessful due to a malformed varint.')
|
||||||
return 'failed'
|
return 'failed'
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
'Pubkey decryption was UNsuccessful because of an unhandled exception! This is definitely a bug! \n%s',
|
'Pubkey decryption was UNsuccessful because of'
|
||||||
traceback.format_exc())
|
' an unhandled exception! This is definitely a bug! \n%s',
|
||||||
|
traceback.format_exc()
|
||||||
|
)
|
||||||
return 'failed'
|
return 'failed'
|
||||||
|
|
||||||
|
|
||||||
|
|
151
src/shared.py
151
src/shared.py
|
@ -6,10 +6,8 @@ import sys
|
||||||
import stat
|
import stat
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import subprocess
|
import subprocess
|
||||||
from struct import unpack
|
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from pyelliptic import arithmetic
|
from pyelliptic import arithmetic
|
||||||
|
|
||||||
|
@ -18,10 +16,8 @@ import state
|
||||||
import highlevelcrypto
|
import highlevelcrypto
|
||||||
from bmconfigparser import BMConfigParser
|
from bmconfigparser import BMConfigParser
|
||||||
from debug import logger
|
from debug import logger
|
||||||
from addresses import (
|
from addresses import decodeAddress, encodeVarint
|
||||||
decodeAddress, encodeVarint, decodeVarint, varintDecodeError
|
from helper_sql import sqlQuery
|
||||||
)
|
|
||||||
from helper_sql import sqlQuery, sqlExecute
|
|
||||||
|
|
||||||
|
|
||||||
verbose = 1
|
verbose = 1
|
||||||
|
@ -281,149 +277,6 @@ def fixSensitiveFilePermissions(filename, hasEnabledKeys):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def isBitSetWithinBitfield(fourByteString, n):
|
|
||||||
# Uses MSB 0 bit numbering across 4 bytes of data
|
|
||||||
n = 31 - n
|
|
||||||
x, = unpack('>L', fourByteString)
|
|
||||||
return x & 2**n != 0
|
|
||||||
|
|
||||||
|
|
||||||
def decryptAndCheckPubkeyPayload(data, address):
|
|
||||||
"""
|
|
||||||
Version 4 pubkeys are encrypted. This function is run when we
|
|
||||||
already have the address to which we want to try to send a message.
|
|
||||||
The 'data' may come either off of the wire or we might have had it
|
|
||||||
already in our inventory when we tried to send a msg to this
|
|
||||||
particular address.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# status
|
|
||||||
_, addressVersion, streamNumber, ripe = decodeAddress(address)
|
|
||||||
|
|
||||||
readPosition = 20 # bypass the nonce, time, and object type
|
|
||||||
embeddedAddressVersion, varintLength = \
|
|
||||||
decodeVarint(data[readPosition:readPosition + 10])
|
|
||||||
readPosition += varintLength
|
|
||||||
embeddedStreamNumber, varintLength = \
|
|
||||||
decodeVarint(data[readPosition:readPosition + 10])
|
|
||||||
readPosition += varintLength
|
|
||||||
# We'll store the address version and stream number
|
|
||||||
# (and some more) in the pubkeys table.
|
|
||||||
storedData = data[20:readPosition]
|
|
||||||
|
|
||||||
if addressVersion != embeddedAddressVersion:
|
|
||||||
logger.info(
|
|
||||||
'Pubkey decryption was UNsuccessful'
|
|
||||||
' due to address version mismatch.')
|
|
||||||
return 'failed'
|
|
||||||
if streamNumber != embeddedStreamNumber:
|
|
||||||
logger.info(
|
|
||||||
'Pubkey decryption was UNsuccessful'
|
|
||||||
' due to stream number mismatch.')
|
|
||||||
return 'failed'
|
|
||||||
|
|
||||||
tag = data[readPosition:readPosition + 32]
|
|
||||||
readPosition += 32
|
|
||||||
# the time through the tag. More data is appended onto
|
|
||||||
# signedData below after the decryption.
|
|
||||||
signedData = data[8:readPosition]
|
|
||||||
encryptedData = data[readPosition:]
|
|
||||||
|
|
||||||
# Let us try to decrypt the pubkey
|
|
||||||
toAddress, cryptorObject = state.neededPubkeys[tag]
|
|
||||||
if toAddress != address:
|
|
||||||
logger.critical(
|
|
||||||
'decryptAndCheckPubkeyPayload failed due to toAddress'
|
|
||||||
' mismatch. This is very peculiar.'
|
|
||||||
' toAddress: %s, address %s',
|
|
||||||
toAddress, address
|
|
||||||
)
|
|
||||||
# the only way I can think that this could happen
|
|
||||||
# is if someone encodes their address data two different ways.
|
|
||||||
# That sort of address-malleability should have been caught
|
|
||||||
# by the UI or API and an error given to the user.
|
|
||||||
return 'failed'
|
|
||||||
try:
|
|
||||||
decryptedData = cryptorObject.decrypt(encryptedData)
|
|
||||||
except:
|
|
||||||
# Someone must have encrypted some data with a different key
|
|
||||||
# but tagged it with a tag for which we are watching.
|
|
||||||
logger.info('Pubkey decryption was unsuccessful.')
|
|
||||||
return 'failed'
|
|
||||||
|
|
||||||
readPosition = 0
|
|
||||||
# bitfieldBehaviors = decryptedData[readPosition:readPosition + 4]
|
|
||||||
readPosition += 4
|
|
||||||
publicSigningKey = \
|
|
||||||
'\x04' + decryptedData[readPosition:readPosition + 64]
|
|
||||||
readPosition += 64
|
|
||||||
publicEncryptionKey = \
|
|
||||||
'\x04' + decryptedData[readPosition:readPosition + 64]
|
|
||||||
readPosition += 64
|
|
||||||
specifiedNonceTrialsPerByte, specifiedNonceTrialsPerByteLength = \
|
|
||||||
decodeVarint(decryptedData[readPosition:readPosition + 10])
|
|
||||||
readPosition += specifiedNonceTrialsPerByteLength
|
|
||||||
specifiedPayloadLengthExtraBytes, \
|
|
||||||
specifiedPayloadLengthExtraBytesLength = \
|
|
||||||
decodeVarint(decryptedData[readPosition:readPosition + 10])
|
|
||||||
readPosition += specifiedPayloadLengthExtraBytesLength
|
|
||||||
storedData += decryptedData[:readPosition]
|
|
||||||
signedData += decryptedData[:readPosition]
|
|
||||||
signatureLength, signatureLengthLength = \
|
|
||||||
decodeVarint(decryptedData[readPosition:readPosition + 10])
|
|
||||||
readPosition += signatureLengthLength
|
|
||||||
signature = decryptedData[readPosition:readPosition + signatureLength]
|
|
||||||
|
|
||||||
if not highlevelcrypto.verify(
|
|
||||||
signedData, signature, hexlify(publicSigningKey)):
|
|
||||||
logger.info(
|
|
||||||
'ECDSA verify failed (within decryptAndCheckPubkeyPayload)')
|
|
||||||
return 'failed'
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'ECDSA verify passed (within decryptAndCheckPubkeyPayload)')
|
|
||||||
|
|
||||||
sha = hashlib.new('sha512')
|
|
||||||
sha.update(publicSigningKey + publicEncryptionKey)
|
|
||||||
ripeHasher = hashlib.new('ripemd160')
|
|
||||||
ripeHasher.update(sha.digest())
|
|
||||||
embeddedRipe = ripeHasher.digest()
|
|
||||||
|
|
||||||
if embeddedRipe != ripe:
|
|
||||||
# Although this pubkey object had the tag were were looking for
|
|
||||||
# and was encrypted with the correct encryption key,
|
|
||||||
# it doesn't contain the correct pubkeys. Someone is
|
|
||||||
# either being malicious or using buggy software.
|
|
||||||
logger.info(
|
|
||||||
'Pubkey decryption was UNsuccessful due to RIPE mismatch.')
|
|
||||||
return 'failed'
|
|
||||||
|
|
||||||
# Everything checked out. Insert it into the pubkeys table.
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'within decryptAndCheckPubkeyPayload, '
|
|
||||||
'addressVersion: %s, streamNumber: %s\nripe %s\n'
|
|
||||||
'publicSigningKey in hex: %s\npublicEncryptionKey in hex: %s',
|
|
||||||
addressVersion, streamNumber, hexlify(ripe),
|
|
||||||
hexlify(publicSigningKey), hexlify(publicEncryptionKey)
|
|
||||||
)
|
|
||||||
|
|
||||||
t = (address, addressVersion, storedData, int(time.time()), 'yes')
|
|
||||||
sqlExecute('''INSERT INTO pubkeys VALUES (?,?,?,?,?)''', *t)
|
|
||||||
return 'successful'
|
|
||||||
except varintDecodeError:
|
|
||||||
logger.info(
|
|
||||||
'Pubkey decryption was UNsuccessful due to a malformed varint.')
|
|
||||||
return 'failed'
|
|
||||||
except Exception:
|
|
||||||
logger.critical(
|
|
||||||
'Pubkey decryption was UNsuccessful because of'
|
|
||||||
' an unhandled exception! This is definitely a bug! \n%s' %
|
|
||||||
traceback.format_exc()
|
|
||||||
)
|
|
||||||
return 'failed'
|
|
||||||
|
|
||||||
|
|
||||||
def openKeysFile():
|
def openKeysFile():
|
||||||
if 'linux' in sys.platform:
|
if 'linux' in sys.platform:
|
||||||
subprocess.call(["xdg-open", state.appdata + 'keys.dat'])
|
subprocess.call(["xdg-open", state.appdata + 'keys.dat'])
|
||||||
|
|
Reference in New Issue
Block a user