9 Commits

Author SHA1 Message Date
dependabot-preview[bot]
051f259fa7 Upgrade to GitHub-native Dependabot 2021-04-29 21:00:42 +00:00
r4sas
94023a986d remove unused exception variable
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-06-22 11:33:34 +00:00
r4sas
5909e7330b validate config file for empty lines (closes #24)
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-06-22 11:22:21 +00:00
r4sas
d0f23094bc update gitignore
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-06-19 14:35:51 +00:00
r4sas
86061e595c do not calculate tag size twice
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-06-04 15:31:36 +00:00
r4sas
49362c70a6 get encryption options from paste (closes #22)
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-06-04 15:29:04 +00:00
r4sas
70328903fa include license and requirements in source archive
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-04-11 17:59:19 +00:00
r4sas
29498b9315 make codacy happy
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-01-07 00:58:19 +00:00
r4sas
19f130feb1 add launch arguments for get and delete, check if pycrypto used
Signed-off-by: r4sas <r4sas@i2pmail.org>
2020-01-06 18:22:11 +00:00
9 changed files with 111 additions and 33 deletions

8
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "17:00"
open-pull-requests-limit: 10

3
.gitignore vendored
View File

@@ -131,3 +131,6 @@ venv.bak/
# PyCharm project settings
.idea
# PBinCLI downloaded paste text
paste-*.txt

3
MANIFEST.in Normal file
View File

@@ -0,0 +1,3 @@
include LICENSE
include README.rst
include requirements.txt

View File

@@ -2,6 +2,6 @@
# -*- coding: utf-8 -*-
__author__ = "R4SAS <r4sas@i2pmail.org>"
__version__ = "0.3.0"
__version__ = "0.3.1"
__copyright__ = "Copyright (c) R4SAS"
__license__ = "MIT"

View File

@@ -1,5 +1,13 @@
from pbincli.format import Paste
from pbincli.utils import PBinCLIError
import signal
def signal_handler(sig, frame):
print('Keyboard interrupt received, terminating...')
exit(0)
signal.signal(signal.SIGINT, signal_handler)
def send(args, api_client, settings=None):
from pbincli.api import Shortener

View File

@@ -3,7 +3,7 @@ import os, sys, argparse
import pbincli.actions
from pbincli.api import PrivateBin
from pbincli.utils import PBinCLIException, validate_url
from pbincli.utils import PBinCLIException, PBinCLIError, validate_url
CONFIG_PATHS = [os.path.join(".", "pbincli.conf", ),
os.path.join(os.getenv("HOME") or "~", ".config", "pbincli", "pbincli.conf") ]
@@ -13,8 +13,13 @@ def read_config(filename):
settings = {}
with open(filename) as f:
for l in f.readlines():
if len(l.strip()) == 0:
continue
try:
key, value = l.strip().split("=")
settings[key.strip()] = value.strip()
except ValueError:
PBinCLIError("Unable to parse config file, please check it for errors.")
return settings
@@ -36,19 +41,21 @@ def main():
send_parser.add_argument("-q", "--notext", default=False, action="store_true", help="don't send text in paste")
send_parser.add_argument("-c", "--compression", default="zlib", action="store",
choices=["zlib", "none"], help="set compression for paste (default: zlib). Note: works only on v2 paste format")
# URL shortener
## URL shortener
send_parser.add_argument("-S", "--short", default=False, action="store_true", help="use URL shortener")
send_parser.add_argument("--short-api", default=argparse.SUPPRESS, action="store", choices=["tinyurl", "clckru", "isgd", "vgd", "cuttly", "yourls"], help="API used by shortener service")
send_parser.add_argument("--short-api", default=argparse.SUPPRESS, action="store",
choices=["tinyurl", "clckru", "isgd", "vgd", "cuttly", "yourls"], help="API used by shortener service")
send_parser.add_argument("--short-url", default=argparse.SUPPRESS, help="URL of shortener service API")
send_parser.add_argument("--short-user", default=argparse.SUPPRESS, help="Shortener username")
send_parser.add_argument("--short-pass", default=argparse.SUPPRESS, help="Shortener password")
send_parser.add_argument("--short-token", default=argparse.SUPPRESS, help="Shortener token")
# Connection options
## Connection options
send_parser.add_argument("-s", "--server", default=argparse.SUPPRESS, help="PrivateBin service URL (default: https://paste.i2pd.xyz/)")
send_parser.add_argument("-x", "--proxy", default=argparse.SUPPRESS, help="Proxy server address (default: None)")
send_parser.add_argument("--no-check-certificate", default=False, action="store_true", help="disable certificate validation")
send_parser.add_argument("--no-insecure-warning", default=False, action="store_true", help="suppress InsecureRequestWarning (only with --no-check-certificate)")
#
send_parser.add_argument("--no-insecure-warning", default=False, action="store_true",
help="suppress InsecureRequestWarning (only with --no-check-certificate)")
##
send_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug")
send_parser.add_argument("--dry", default=False, action="store_true", help="invoke dry run")
send_parser.add_argument("stdin", help="input paste text from stdin", nargs="?", type=argparse.FileType("r"), default=sys.stdin)
@@ -58,8 +65,13 @@ def main():
get_parser = subparsers.add_parser("get", description="Get data from PrivateBin instance")
get_parser.add_argument("pasteinfo", help="example: aabb#cccddd")
get_parser.add_argument("-p", "--password", help="password for decrypting paste")
## Connection options
get_parser.add_argument("-s", "--server", default=argparse.SUPPRESS, help="PrivateBin service URL (default: https://paste.i2pd.xyz/)")
get_parser.add_argument("-x", "--proxy", default=argparse.SUPPRESS, help="Proxy server address (default: None)")
get_parser.add_argument("--no-check-certificate", default=False, action="store_true", help="disable certificate validation")
get_parser.add_argument("--no-insecure-warning", default=False, action="store_true", help="suppress InsecureRequestWarning (only with --no-check-certificate)")
get_parser.add_argument("--no-insecure-warning", default=False, action="store_true",
help="suppress InsecureRequestWarning (only with --no-check-certificate)")
##
get_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug")
get_parser.set_defaults(func=pbincli.actions.get)
@@ -67,8 +79,13 @@ def main():
delete_parser = subparsers.add_parser("delete", description="Delete paste from PrivateBin instance using token")
delete_parser.add_argument("-p", "--paste", required=True, help="paste id")
delete_parser.add_argument("-t", "--token", required=True, help="paste deletion token")
## Connection options
delete_parser.add_argument("-s", "--server", default=argparse.SUPPRESS, help="PrivateBin service URL (default: https://paste.i2pd.xyz/)")
delete_parser.add_argument("-x", "--proxy", default=argparse.SUPPRESS, help="Proxy server address (default: None)")
delete_parser.add_argument("--no-check-certificate", default=False, action="store_true", help="disable certificate validation")
delete_parser.add_argument("--no-insecure-warning", default=False, action="store_true", help="suppress InsecureRequestWarning (only with --no-check-certificate)")
delete_parser.add_argument("--no-insecure-warning", default=False, action="store_true",
help="suppress InsecureRequestWarning (only with --no-check-certificate)")
##
delete_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug")
delete_parser.set_defaults(func=pbincli.actions.delete)

View File

@@ -1,15 +1,33 @@
from Crypto.Random import get_random_bytes
from Crypto.Cipher import AES
from base64 import b64encode, b64decode
from pbincli.utils import PBinCLIError
import zlib
# try import AES cipher and check if it has GCM mode (prevent usage of pycrypto)
try:
from Crypto.Cipher import AES
if not hasattr(AES, 'MODE_GCM'):
try:
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes
except ImportError:
PBinCLIError("AES GCM mode is not found in imported crypto module.\n" +
"That can happen if you have installed pycrypto.\n\n" +
"We tried to import pycryptodomex but it is not available.\n" +
"Please install it via pip, if you still need pycrypto, by running:\n" +
"\tpip install pycryptodomex\n" +
"... otherwise use separate python environment or uninstall pycrypto:\n" +
"\tpip uninstall pycrypto")
else:
from Crypto.Random import get_random_bytes
except ImportError:
PBinCLIError("Unable import pycryptodome")
CIPHER_ITERATION_COUNT = 100000
CIPHER_SALT_BYTES = 8
CIPHER_BLOCK_BITS = 256
CIPHER_BLOCK_BYTES = int(CIPHER_BLOCK_BITS/8)
CIPHER_TAG_BITS = int(CIPHER_BLOCK_BITS/2)
CIPHER_TAG_BYTES = int(CIPHER_TAG_BITS/8)
CIPHER_TAG_BITS = 128
class Paste:
def __init__(self, debug=False):
@@ -19,9 +37,13 @@ class Paste:
self._text = ''
self._attachment = ''
self._attachment_name = ''
self._key = get_random_bytes(CIPHER_BLOCK_BYTES)
self._password = ''
self._debug = debug
self._iteration_count = CIPHER_ITERATION_COUNT
self._salt_bytes = CIPHER_SALT_BYTES
self._block_bits = CIPHER_BLOCK_BITS
self._tag_bits = CIPHER_TAG_BITS
self._key = get_random_bytes(int(self._block_bits / 8))
def setVersion(self, version):
@@ -105,8 +127,8 @@ class Paste:
return PBKDF2(
self._key + self._password.encode(),
salt,
dkLen = CIPHER_BLOCK_BYTES,
count = CIPHER_ITERATION_COUNT,
dkLen = int(self._block_bits / 8),
count = self._iteration_count,
prf = lambda password, salt: HMAC.new(
password,
salt,
@@ -115,10 +137,10 @@ class Paste:
@classmethod
def __initializeCipher(self, key, iv, adata):
def __initializeCipher(self, key, iv, adata, tagsize):
from pbincli.utils import json_encode
cipher = AES.new(key, AES.MODE_GCM, nonce=iv, mac_len=CIPHER_TAG_BYTES)
cipher = AES.new(key, AES.MODE_GCM, nonce=iv, mac_len=tagsize)
cipher.update(json_encode(adata))
return cipher
@@ -173,16 +195,22 @@ class Paste:
from json import loads as json_decode
iv = b64decode(self._data['adata'][0][0])
salt = b64decode(self._data['adata'][0][1])
self._iteration_count = self._data['adata'][0][2]
self._block_bits = self._data['adata'][0][3]
self._tag_bits = self._data['adata'][0][4]
cipher_tag_bytes = int(self._tag_bits / 8)
key = self.__deriveKey(salt)
# Get compression type from received paste
self._compression = self._data['adata'][0][7]
cipher = self.__initializeCipher(key, iv, self._data['adata'])
cipher = self.__initializeCipher(key, iv, self._data['adata'], cipher_tag_bytes)
# Cut the cipher text into message and tag
cipher_text_tag = b64decode(self._data['ct'])
cipher_text = cipher_text_tag[:-CIPHER_TAG_BYTES]
cipher_tag = cipher_text_tag[-CIPHER_TAG_BYTES:]
cipher_text = cipher_text_tag[:-cipher_tag_bytes]
cipher_tag = cipher_text_tag[-cipher_tag_bytes:]
cipher_message = json_decode(self.__decompress(cipher.decrypt_and_verify(cipher_text, cipher_tag)).decode())
self._text = cipher_message['paste'].encode()
@@ -233,8 +261,8 @@ class Paste:
def _encryptV2(self):
from pbincli.utils import json_encode
iv = get_random_bytes(CIPHER_TAG_BYTES)
salt = get_random_bytes(CIPHER_SALT_BYTES)
iv = get_random_bytes(int(self._tag_bits / 8))
salt = get_random_bytes(self._salt_bytes)
key = self.__deriveKey(salt)
# prepare encryption authenticated data and message
@@ -242,9 +270,9 @@ class Paste:
[
b64encode(iv).decode(),
b64encode(salt).decode(),
CIPHER_ITERATION_COUNT,
CIPHER_BLOCK_BITS,
CIPHER_TAG_BITS,
self._iteration_count,
self._block_bits,
self._tag_bits,
'aes',
'gcm',
self._compression
@@ -258,9 +286,12 @@ class Paste:
cipher_message['attachment'] = self._attachment
cipher_message['attachment_name'] = self._attachment_name
cipher = self.__initializeCipher(key, iv, adata)
cipher = self.__initializeCipher(key, iv, adata, int(self._tag_bits /8 ))
ciphertext, tag = cipher.encrypt_and_digest(self.__compress(json_encode(cipher_message)))
if self._debug: print("PBKDF2 Key:\t{}\nCipherText:\t{}\nCipherTag:\t{}"
.format(b64encode(key), b64encode(ciphertext), b64encode(tag)))
self._data = {'v':2,'adata':adata,'ct':b64encode(ciphertext + tag).decode(),'meta':{'expire':self._expiration}}
@@ -268,7 +299,8 @@ class Paste:
from sjcl import SJCL
from pbincli.utils import json_encode
self._data = {'expire':self._expiration,'formatter':self._formatter,'burnafterreading':int(self._burnafterreading),'opendiscussion':int(self._discussion)}
self._data = {'expire':self._expiration,'formatter':self._formatter,
'burnafterreading':int(self._burnafterreading),'opendiscussion':int(self._discussion)}
password = self.__preparePassKey()
if self._debug: print("Password:\t{}".format(password))

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[metadata]
license_files = LICENSE

View File

@@ -17,7 +17,7 @@ setup(
long_description_content_type='text/x-rst',
author='R4SAS',
author_email='r4sas@i2pmail.org',
url='https://github.com/r4sas/PBinCLI',
url='https://github.com/r4sas/PBinCLI/',
keywords='privatebin cryptography security',
license='MIT',
classifiers=[
@@ -31,9 +31,14 @@ setup(
],
packages=['pbincli'],
install_requires=install_requires,
python_requires='>=3',
entry_points={
'console_scripts': [
'pbincli=pbincli.cli:main',
],
}
},
project_urls={
'Bug Reports': 'https://github.com/r4sas/PBinCLI/issues',
'Source': 'https://github.com/r4sas/PBinCLI/',
},
)